From e53bf5839b625e5ab92ffc4859331f4f46161bc6 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 20 Jun 2018 13:30:31 +0200 Subject: [PATCH] [Translation] Improved the performance of the lint:xliff command --- .../Translation/Command/XliffLintCommand.php | 13 +- .../Translation/Loader/XliffFileLoader.php | 125 +------------- .../Component/Translation/Util/XliffUtils.php | 163 ++++++++++++++++++ 3 files changed, 173 insertions(+), 128 deletions(-) create mode 100644 src/Symfony/Component/Translation/Util/XliffUtils.php diff --git a/src/Symfony/Component/Translation/Command/XliffLintCommand.php b/src/Symfony/Component/Translation/Command/XliffLintCommand.php index 2caa6abefb..80e7b7f2cc 100644 --- a/src/Symfony/Component/Translation/Command/XliffLintCommand.php +++ b/src/Symfony/Component/Translation/Command/XliffLintCommand.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Util\XliffUtils; /** * Validates XLIFF files syntax and outputs encountered errors. @@ -127,18 +128,14 @@ EOF } } - $document->schemaValidate(__DIR__.'/../Resources/schemas/xliff-core-1.2-strict.xsd'); - foreach (libxml_get_errors() as $xmlError) { + foreach (XliffUtils::validateSchema($document) as $xmlError) { $errors[] = array( - 'line' => $xmlError->line, - 'column' => $xmlError->column, - 'message' => trim($xmlError->message), + 'line' => $xmlError['line'], + 'column' => $xmlError['column'], + 'message' => $xmlError['message'], ); } - libxml_clear_errors(); - libxml_use_internal_errors(false); - return array('file' => $file, 'valid' => 0 === count($errors), 'messages' => $errors); } diff --git a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php index b20d4909a3..492287c34d 100644 --- a/src/Symfony/Component/Translation/Loader/XliffFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/XliffFileLoader.php @@ -15,8 +15,8 @@ use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; -use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Translation\Util\XliffUtils; /** * XliffFileLoader loads translations from XLIFF files. @@ -56,8 +56,10 @@ class XliffFileLoader implements LoaderInterface throw new InvalidResourceException(sprintf('Unable to load "%s": %s', $resource, $e->getMessage()), $e->getCode(), $e); } - $xliffVersion = $this->getVersionNumber($dom); - $this->validateSchema($xliffVersion, $dom, $this->getSchema($xliffVersion)); + $xliffVersion = XliffUtils::getVersionNumber($dom); + if ($errors = XliffUtils::validateSchema($dom)) { + throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $xliffVersion, XliffUtils::getErrorsAsString($errors))); + } if ('1.2' === $xliffVersion) { $this->extractXliff1($dom, $catalogue, $domain); @@ -169,123 +171,6 @@ class XliffFileLoader implements LoaderInterface return $content; } - /** - * Validates and parses the given file into a DOMDocument. - * - * @throws InvalidResourceException - */ - private function validateSchema(string $file, \DOMDocument $dom, string $schema) - { - $internalErrors = libxml_use_internal_errors(true); - - $disableEntities = libxml_disable_entity_loader(false); - - if (!@$dom->schemaValidateSource($schema)) { - libxml_disable_entity_loader($disableEntities); - - throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $file, implode("\n", $this->getXmlErrors($internalErrors)))); - } - - libxml_disable_entity_loader($disableEntities); - - $dom->normalizeDocument(); - - libxml_clear_errors(); - libxml_use_internal_errors($internalErrors); - } - - private function getSchema($xliffVersion) - { - if ('1.2' === $xliffVersion) { - $schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd'); - $xmlUri = 'http://www.w3.org/2001/xml.xsd'; - } elseif ('2.0' === $xliffVersion) { - $schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-2.0.xsd'); - $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'; - } else { - throw new InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion)); - } - - return $this->fixXmlLocation($schemaSource, $xmlUri); - } - - /** - * Internally changes the URI of a dependent xsd to be loaded locally. - */ - private function fixXmlLocation(string $schemaSource, string $xmlUri): string - { - $newPath = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd'; - $parts = explode('/', $newPath); - $locationstart = 'file:///'; - if (0 === stripos($newPath, 'phar://')) { - $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); - if ($tmpfile) { - copy($newPath, $tmpfile); - $parts = explode('/', str_replace('\\', '/', $tmpfile)); - } else { - array_shift($parts); - $locationstart = 'phar:///'; - } - } - - $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; - $newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); - - return str_replace($xmlUri, $newPath, $schemaSource); - } - - /** - * Returns the XML errors of the internal XML parser. - */ - private function getXmlErrors(bool $internalErrors): array - { - $errors = array(); - foreach (libxml_get_errors() as $error) { - $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', - LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', - $error->code, - trim($error->message), - $error->file ?: 'n/a', - $error->line, - $error->column - ); - } - - libxml_clear_errors(); - libxml_use_internal_errors($internalErrors); - - return $errors; - } - - /** - * Gets xliff file version based on the root "version" attribute. - * Defaults to 1.2 for backwards compatibility. - * - * @throws InvalidArgumentException - */ - private function getVersionNumber(\DOMDocument $dom): string - { - /** @var \DOMNode $xliff */ - foreach ($dom->getElementsByTagName('xliff') as $xliff) { - $version = $xliff->attributes->getNamedItem('version'); - if ($version) { - return $version->nodeValue; - } - - $namespace = $xliff->attributes->getNamedItem('xmlns'); - if ($namespace) { - if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) { - throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s"', $namespace)); - } - - return substr($namespace, 34); - } - } - - // Falls back to v1.2 - return '1.2'; - } - private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, string $encoding = null): array { $notes = array(); diff --git a/src/Symfony/Component/Translation/Util/XliffUtils.php b/src/Symfony/Component/Translation/Util/XliffUtils.php new file mode 100644 index 0000000000..6d11e197ab --- /dev/null +++ b/src/Symfony/Component/Translation/Util/XliffUtils.php @@ -0,0 +1,163 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Util; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\InvalidResourceException; + +/** + * Provides some utility methods for XLIFF translation files, such as validating + * their contents according to the XSD schema. + * + * @author Fabien Potencier + */ +class XliffUtils +{ + /** + * Gets xliff file version based on the root "version" attribute. + * + * Defaults to 1.2 for backwards compatibility. + * + * @throws InvalidArgumentException + */ + public static function getVersionNumber(\DOMDocument $dom): string + { + /** @var \DOMNode $xliff */ + foreach ($dom->getElementsByTagName('xliff') as $xliff) { + $version = $xliff->attributes->getNamedItem('version'); + if ($version) { + return $version->nodeValue; + } + + $namespace = $xliff->attributes->getNamedItem('xmlns'); + if ($namespace) { + if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) { + throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s"', $namespace)); + } + + return substr($namespace, 34); + } + } + + // Falls back to v1.2 + return '1.2'; + } + + /** + * Validates and parses the given file into a DOMDocument. + * + * @throws InvalidResourceException + */ + public static function validateSchema(\DOMDocument $dom): array + { + $xliffVersion = static::getVersionNumber($dom); + $internalErrors = libxml_use_internal_errors(true); + $disableEntities = libxml_disable_entity_loader(false); + + $isValid = @$dom->schemaValidateSource(self::getSchema($xliffVersion)); + if (!$isValid) { + libxml_disable_entity_loader($disableEntities); + + return self::getXmlErrors($internalErrors); + } + + libxml_disable_entity_loader($disableEntities); + + $dom->normalizeDocument(); + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return array(); + } + + public static function getErrorsAsString(array $xmlErrors): string + { + $errorsAsString = ''; + + foreach ($xmlErrors as $error) { + $errorsAsString .= sprintf("[%s %s] %s (in %s - line %d, column %d)\n", + LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR', + $error['code'], + $error['message'], + $error['file'], + $error['line'], + $error['column'] + ); + } + + return $errorsAsString; + } + + private static function getSchema(string $xliffVersion): string + { + if ('1.2' === $xliffVersion) { + $schemaSource = file_get_contents(__DIR__.'/../Loader/schema/dic/xliff-core/xliff-core-1.2-strict.xsd'); + $xmlUri = 'http://www.w3.org/2001/xml.xsd'; + } elseif ('2.0' === $xliffVersion) { + $schemaSource = file_get_contents(__DIR__.'/../Loader/schema/dic/xliff-core/xliff-core-2.0.xsd'); + $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'; + } else { + throw new InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion)); + } + + return self::fixXmlLocation($schemaSource, $xmlUri); + } + + /** + * Internally changes the URI of a dependent xsd to be loaded locally. + */ + private static function fixXmlLocation(string $schemaSource, string $xmlUri): string + { + $newPath = str_replace('\\', '/', __DIR__).'/../Loader/schema/dic/xliff-core/xml.xsd'; + $parts = explode('/', $newPath); + $locationstart = 'file:///'; + if (0 === stripos($newPath, 'phar://')) { + $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); + if ($tmpfile) { + copy($newPath, $tmpfile); + $parts = explode('/', str_replace('\\', '/', $tmpfile)); + } else { + array_shift($parts); + $locationstart = 'phar:///'; + } + } + + $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; + $newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); + + return str_replace($xmlUri, $newPath, $schemaSource); + } + + /** + * Returns the XML errors of the internal XML parser. + */ + private static function getXmlErrors(bool $internalErrors): array + { + $errors = array(); + foreach (libxml_get_errors() as $error) { + $errors[] = array( + 'level' => LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', + 'code' => $error->code, + 'message' => trim($error->message), + 'file' => $error->file ?: 'n/a', + 'line' => $error->line, + 'column' => $error->column, + ); + } + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return $errors; + } +}