%PDF- %PDF-
Direktori : /var/www/projetos/suporte.iigd.com.br/src/Console/Diagnostic/ |
Current File : //var/www/projetos/suporte.iigd.com.br/src/Console/Diagnostic/CheckHtmlEncodingCommand.php |
<?php /** * --------------------------------------------------------------------- * * GLPI - Gestionnaire Libre de Parc Informatique * * http://glpi-project.org * * @copyright 2015-2024 Teclib' and contributors. * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- * * LICENSE * * This file is part of GLPI. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * --------------------------------------------------------------------- */ namespace Glpi\Console\Diagnostic; use CommonDBTM; use Glpi\Console\AbstractCommand; use ITILFollowup; use Search; use Session; use Ticket; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; /** * Prior from GLPI 10.0, some HTML contents were not properly encoded. * * This CLI tool helps to fix items encoding issues. */ final class CheckHtmlEncodingCommand extends AbstractCommand { /** * Error code returned when invalid items are found and are not fixed. * * @var integer */ public const ERROR_INVALID_ITEMS_FOUND = 1; /** * Error code returned when update of an item failed. * * @var integer */ public const ERROR_UPDATE_FAILED = 2; /** * Error code returned when rollback file could not be created. * * @var integer */ public const ERROR_ROLLBACK_FILE_FAILED = 3; /** * Items with invalid HTML. * * @var array */ private array $invalid_items = []; /** * Count of items with invalid HTML that have NOT been fixed. * * @var int */ private int $failed_items_count = 0; /** * Columns which contains rich text, populated by analyzing search options. * * @var array */ private array $text_fields = []; protected function configure() { parent::configure(); $this->setName('diagnostic:check_html_encoding'); $this->setDescription(__('Check for badly HTML encoded content in database.')); $this->addOption( 'fix', 'f', InputOption::VALUE_NONE | InputOption::VALUE_NEGATABLE, __('Fix detected issues') ); $this->addOption( 'dump', 'd', InputOption::VALUE_REQUIRED, __('Path of file where will be stored SQL queries that can be used to rollback changes') ); } protected function execute(InputInterface $input, OutputInterface $output) { $this->warnAboutExecutionTime(); $this->findTextFields(); $this->scanItems(); $count = $this->countItems($this->invalid_items); if ($count === 0) { $output->writeln('<info>' . __('No item to fix.') . '</info>'); return 0; } $output->writeln('<info>' . sprintf(_n('Found %d item to fix.', 'Found %d items to fix.', $count), $count) . '</info>'); $fix = $input->getOption('fix'); if ($fix === null && !$this->input->getOption('no-interaction')) { $question_helper = $this->getHelper('question'); $fix = $question_helper->ask( $input, $output, new ConfirmationQuestion( _n('Do you want to fix it?', 'Do you want to fix them?', $count) . ' [Yes/no]' ) ); } if ($fix !== true) { return self::ERROR_INVALID_ITEMS_FOUND; } if ($input->getOption('dump')) { $this->dumpObjects(); } $this->fixItems(); if ($this->failed_items_count > 0) { $this->output->writeln( '<error>' . sprintf(__('Unable to update %s items'), $this->failed_items_count) . '</error>', OutputInterface::VERBOSITY_QUIET ); return self::ERROR_UPDATE_FAILED; } $output->writeln('<info>' . __('HTML encoding has been fixed.') . '</info>'); return 0; } /** * Dump items * * @return void */ private function dumpObjects(): void { /** @var \DBmysql $DB */ global $DB; $dump_content = ''; foreach ($this->invalid_items as $itemtype => $items) { foreach ($items as $item_id => $fields) { // Get the item to save $item = new $itemtype(); $item->getFromDB($item_id); // read the fields to save $object_state = []; foreach ($fields as $field) { $object_state[$field] = $DB->escape($item->fields[$field]); } // Build the SQL query $dump_content .= $DB->buildUpdate( $item::getTable(), $object_state, ['id' => $item_id], ) . ';' . PHP_EOL; } } // Save the rollback SQL queries dump $dump_file_name = $this->input->getOption('dump'); if (@file_put_contents($dump_file_name, $dump_content) == strlen($dump_content)) { $this->output->writeln( '<comment>' . sprintf(__('File %s contains SQL queries that can be used to rollback command.'), $dump_file_name) . '</comment>', OutputInterface::VERBOSITY_QUIET ); } else { throw new \Glpi\Console\Exception\EarlyExitException( '<comment>' . sprintf(__('Failed to write rollback SQL queries in "%s" file.'), $dump_file_name) . '</comment>', self::ERROR_ROLLBACK_FILE_FAILED ); } } /** * Fix encoding issues. * * @return void */ private function fixItems(): void { foreach ($this->invalid_items as $itemtype => $items) { /* @var \CommonDBTM $item */ $item = new $itemtype(); $this->outputMessage( '<comment>' . sprintf(__('Fixing %s...'), $item::getTypeName(Session::getPluralNumber())) . '</comment>', ); $progress_message = function (array $fields, int $id) use ($item) { return sprintf(__('Fixing %s with ID %s...'), $item::getTypeName(1), $id); }; foreach ($this->iterate($items, $progress_message) as $item_id => $fields) { if (!$item->getFromDB($item_id)) { $this->outputMessage( '<error>' . sprintf(__('Unable to fix %s with ID %s.'), $item::getTypeName(1), $item_id) . '</error>', OutputInterface::VERBOSITY_QUIET ); $this->failed_items_count++; continue; } $this->fixOneItem($item, $fields); } } } /** * Fix a single item, on specified fields. * * @param CommonDBTM $item item to fix * @param array $fields fields names to fix * @return void */ private function fixOneItem(CommonDBTM $item, array $fields): void { /** @var \DBmysql $DB */ global $DB; $itemtype = $item::getType(); // update the item $update = []; foreach ($fields as $field) { $update[$field] = $this->fixOneField($item, $field); $update[$field] = $DB->escape($update[$field]); } $success = $DB->update( $itemtype::getTable(), $update, ['id' => $item->fields['id']], ); if (!$success) { $this->outputMessage( '<error>' . sprintf(__('Unable to fix %s with ID %s.'), $itemtype::getTypeName(1), $item->getID()) . '</error>', OutputInterface::VERBOSITY_QUIET ); $this->failed_items_count++; } } /** * Fix a single field of an item. * * @param CommonDBTM $item * @param string $field * @return string */ private function fixOneField(CommonDBTM $item, string $field): string { $new_value = $item->fields[$field]; if (in_array($item::getType(), [Ticket::getType(), ITILFollowup::getType()]) && $field == 'content') { $new_value = $this->fixEmailHeadersEncoding($new_value); } $new_value = $this->fixQuoteEntityWithoutSemicolon($new_value); return $new_value; } /** * Fix double encoded HTML entities in old followups * @see https://github.com/glpi-project/glpi/issues/8330 * * @param string $input * @return string */ private function fixEmailHeadersEncoding(string $input): string { $output = $input; // Not very strict pattern for emails, but should be enough // Capturing parentheses: // 1: Triple encoded < character // 2: email address // 3: Triple encoded > character $pattern = '/(&amp;lt;)(?<email>[^@]*?@[a-zA-Z0-9\-.]*?)(&amp;gt;)/'; $replace = '&lt;${2}&gt;'; $output = preg_replace($pattern, $replace, $output); // Triple encoded should be now double encoded (this double encoding is expected) return $output; } /** * Fix " HTML entity without its final semicolon. * @see https://github.com/glpi-project/glpi/pull/6084 * * @param string $input * @return string */ private function fixQuoteEntityWithoutSemicolon(string $input): string { $output = $input; // Add the missing semicolon to " HTML entity $pattern = '/"(?!;)/'; $replace = '"'; $output = preg_replace($pattern, $replace, $output); return $output; } /** * Find rich text fields for itemtypes given as CLI argument. * * @return void */ private function findTextFields(): void { /** @var \DBmysql $DB */ global $DB; $table_iterator = $DB->listTables(); foreach ($table_iterator as $table_data) { $table = $table_data['TABLE_NAME']; $itemtype = getItemTypeForTable($table); if (!is_a($itemtype, CommonDBTM::class, true)) { continue; } $search_options = Search::getOptions($itemtype); foreach ($search_options as $search_option) { if (!isset($search_option['table'])) { continue; } if ( $search_option['table'] === $table && ($search_option['datatype'] ?? '') === 'text' && ($search_option['htmltext'] ?? false) === true ) { $this->text_fields[$itemtype][] = $search_option['field']; } } } } /** * Search in all items of an itemtype for bad HTML. * * @return void */ private function scanItems(): void { $this->outputMessage( '<comment>' . __('Scanning database for items to fix...') . '</comment>' ); foreach ($this->text_fields as $itemtype => $fields) { foreach ($fields as $field) { $this->scanField($itemtype, $field); } } } /** * Search for bad HTML in a single column of a table * * @param string $itemtype * @param string $field * @return void */ private function scanField(string $itemtype, string $field): void { /** @var \DBmysql $DB */ global $DB; $searches = [ [$field => ['LIKE', '%"(?!;)/%']], ]; if (in_array($itemtype, [Ticket::getType(), ITILFollowup::getType()]) && $field == 'content') { $searches[] = [ $field => ['REGEXP', $DB->escape('(&amp;lt;)(?<email>[^@]*?@[a-zA-Z0-9\-.]*?)(&amp;gt;)')] ]; } $iterator = $DB->request([ 'SELECT' => 'id', 'FROM' => $itemtype::getTable(), 'WHERE' => [ 'OR' => $searches, ], ]); foreach ($iterator as $row) { $this->invalid_items[$itemtype][$row['id']][] = $field; } } /** * Count items in list of invalid idems * * @return integer */ private function countItems(array $items_array): int { $count = 0; if (count($items_array) === 0) { return 0; } foreach ($items_array as $items) { $count += count($items); } return $count; } }