4e2b7c182ba38510fbdd061c2eeb9eb9ea535b7e
[yaffs-website] / vendor / symfony / translation / Loader / XliffFileLoader.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\Translation\Loader;
13
14 use Symfony\Component\Config\Util\XmlUtils;
15 use Symfony\Component\Translation\MessageCatalogue;
16 use Symfony\Component\Translation\Exception\InvalidResourceException;
17 use Symfony\Component\Translation\Exception\NotFoundResourceException;
18 use Symfony\Component\Config\Resource\FileResource;
19
20 /**
21  * XliffFileLoader loads translations from XLIFF files.
22  *
23  * @author Fabien Potencier <fabien@symfony.com>
24  */
25 class XliffFileLoader implements LoaderInterface
26 {
27     /**
28      * {@inheritdoc}
29      */
30     public function load($resource, $locale, $domain = 'messages')
31     {
32         if (!stream_is_local($resource)) {
33             throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
34         }
35
36         if (!file_exists($resource)) {
37             throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
38         }
39
40         $catalogue = new MessageCatalogue($locale);
41         $this->extract($resource, $catalogue, $domain);
42
43         if (class_exists('Symfony\Component\Config\Resource\FileResource')) {
44             $catalogue->addResource(new FileResource($resource));
45         }
46
47         return $catalogue;
48     }
49
50     private function extract($resource, MessageCatalogue $catalogue, $domain)
51     {
52         try {
53             $dom = XmlUtils::loadFile($resource);
54         } catch (\InvalidArgumentException $e) {
55             throw new InvalidResourceException(sprintf('Unable to load "%s": %s', $resource, $e->getMessage()), $e->getCode(), $e);
56         }
57
58         $xliffVersion = $this->getVersionNumber($dom);
59         $this->validateSchema($xliffVersion, $dom, $this->getSchema($xliffVersion));
60
61         if ('1.2' === $xliffVersion) {
62             $this->extractXliff1($dom, $catalogue, $domain);
63         }
64
65         if ('2.0' === $xliffVersion) {
66             $this->extractXliff2($dom, $catalogue, $domain);
67         }
68     }
69
70     /**
71      * Extract messages and metadata from DOMDocument into a MessageCatalogue.
72      *
73      * @param \DOMDocument     $dom       Source to extract messages and metadata
74      * @param MessageCatalogue $catalogue Catalogue where we'll collect messages and metadata
75      * @param string           $domain    The domain
76      */
77     private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, $domain)
78     {
79         $xml = simplexml_import_dom($dom);
80         $encoding = strtoupper($dom->encoding);
81
82         $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
83         foreach ($xml->xpath('//xliff:trans-unit') as $translation) {
84             $attributes = $translation->attributes();
85
86             if (!(isset($attributes['resname']) || isset($translation->source))) {
87                 continue;
88             }
89
90             $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source;
91             // If the xlf file has another encoding specified, try to convert it because
92             // simple_xml will always return utf-8 encoded values
93             $target = $this->utf8ToCharset((string) (isset($translation->target) ? $translation->target : $source), $encoding);
94
95             $catalogue->set((string) $source, $target, $domain);
96
97             $metadata = array();
98             if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) {
99                 $metadata['notes'] = $notes;
100             }
101             if (isset($translation->target) && $translation->target->attributes()) {
102                 $metadata['target-attributes'] = array();
103                 foreach ($translation->target->attributes() as $key => $value) {
104                     $metadata['target-attributes'][$key] = (string) $value;
105                 }
106             }
107
108             $catalogue->setMetadata((string) $source, $metadata, $domain);
109         }
110     }
111
112     /**
113      * @param \DOMDocument     $dom
114      * @param MessageCatalogue $catalogue
115      * @param string           $domain
116      */
117     private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, $domain)
118     {
119         $xml = simplexml_import_dom($dom);
120         $encoding = strtoupper($dom->encoding);
121
122         $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0');
123
124         foreach ($xml->xpath('//xliff:unit/xliff:segment') as $segment) {
125             $source = $segment->source;
126
127             // If the xlf file has another encoding specified, try to convert it because
128             // simple_xml will always return utf-8 encoded values
129             $target = $this->utf8ToCharset((string) (isset($segment->target) ? $segment->target : $source), $encoding);
130
131             $catalogue->set((string) $source, $target, $domain);
132
133             $metadata = array();
134             if (isset($segment->target) && $segment->target->attributes()) {
135                 $metadata['target-attributes'] = array();
136                 foreach ($segment->target->attributes() as $key => $value) {
137                     $metadata['target-attributes'][$key] = (string) $value;
138                 }
139             }
140
141             $catalogue->setMetadata((string) $source, $metadata, $domain);
142         }
143     }
144
145     /**
146      * Convert a UTF8 string to the specified encoding.
147      *
148      * @param string $content  String to decode
149      * @param string $encoding Target encoding
150      *
151      * @return string
152      */
153     private function utf8ToCharset($content, $encoding = null)
154     {
155         if ('UTF-8' !== $encoding && !empty($encoding)) {
156             return mb_convert_encoding($content, $encoding, 'UTF-8');
157         }
158
159         return $content;
160     }
161
162     /**
163      * Validates and parses the given file into a DOMDocument.
164      *
165      * @param string       $file
166      * @param \DOMDocument $dom
167      * @param string       $schema source of the schema
168      *
169      * @throws \RuntimeException
170      * @throws InvalidResourceException
171      */
172     private function validateSchema($file, \DOMDocument $dom, $schema)
173     {
174         $internalErrors = libxml_use_internal_errors(true);
175
176         $disableEntities = libxml_disable_entity_loader(false);
177
178         if (!@$dom->schemaValidateSource($schema)) {
179             libxml_disable_entity_loader($disableEntities);
180
181             throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: %s', $file, implode("\n", $this->getXmlErrors($internalErrors))));
182         }
183
184         libxml_disable_entity_loader($disableEntities);
185
186         $dom->normalizeDocument();
187
188         libxml_clear_errors();
189         libxml_use_internal_errors($internalErrors);
190     }
191
192     private function getSchema($xliffVersion)
193     {
194         if ('1.2' === $xliffVersion) {
195             $schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd');
196             $xmlUri = 'http://www.w3.org/2001/xml.xsd';
197         } elseif ('2.0' === $xliffVersion) {
198             $schemaSource = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-2.0.xsd');
199             $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd';
200         } else {
201             throw new \InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion));
202         }
203
204         return $this->fixXmlLocation($schemaSource, $xmlUri);
205     }
206
207     /**
208      * Internally changes the URI of a dependent xsd to be loaded locally.
209      *
210      * @param string $schemaSource Current content of schema file
211      * @param string $xmlUri       External URI of XML to convert to local
212      *
213      * @return string
214      */
215     private function fixXmlLocation($schemaSource, $xmlUri)
216     {
217         $newPath = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd';
218         $parts = explode('/', $newPath);
219         if (0 === stripos($newPath, 'phar://')) {
220             $tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
221             if ($tmpfile) {
222                 copy($newPath, $tmpfile);
223                 $parts = explode('/', str_replace('\\', '/', $tmpfile));
224             }
225         }
226         $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
227         $newPath = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
228
229         return str_replace($xmlUri, $newPath, $schemaSource);
230     }
231
232     /**
233      * Returns the XML errors of the internal XML parser.
234      *
235      * @param bool $internalErrors
236      *
237      * @return array An array of errors
238      */
239     private function getXmlErrors($internalErrors)
240     {
241         $errors = array();
242         foreach (libxml_get_errors() as $error) {
243             $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
244                 LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
245                 $error->code,
246                 trim($error->message),
247                 $error->file ?: 'n/a',
248                 $error->line,
249                 $error->column
250             );
251         }
252
253         libxml_clear_errors();
254         libxml_use_internal_errors($internalErrors);
255
256         return $errors;
257     }
258
259     /**
260      * Gets xliff file version based on the root "version" attribute.
261      * Defaults to 1.2 for backwards compatibility.
262      *
263      * @param \DOMDocument $dom
264      *
265      * @throws \InvalidArgumentException
266      *
267      * @return string
268      */
269     private function getVersionNumber(\DOMDocument $dom)
270     {
271         /** @var \DOMNode $xliff */
272         foreach ($dom->getElementsByTagName('xliff') as $xliff) {
273             $version = $xliff->attributes->getNamedItem('version');
274             if ($version) {
275                 return $version->nodeValue;
276             }
277
278             $namespace = $xliff->attributes->getNamedItem('xmlns');
279             if ($namespace) {
280                 if (substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34) !== 0) {
281                     throw new \InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s"', $namespace));
282                 }
283
284                 return substr($namespace, 34);
285             }
286         }
287
288         // Falls back to v1.2
289         return '1.2';
290     }
291
292     /*
293      * @param \SimpleXMLElement|null $noteElement
294      * @param string|null            $encoding
295      *
296      * @return array
297      */
298     private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, $encoding = null)
299     {
300         $notes = array();
301
302         if (null === $noteElement) {
303             return $notes;
304         }
305
306         foreach ($noteElement as $xmlNote) {
307             $noteAttributes = $xmlNote->attributes();
308             $note = array('content' => $this->utf8ToCharset((string) $xmlNote, $encoding));
309             if (isset($noteAttributes['priority'])) {
310                 $note['priority'] = (int) $noteAttributes['priority'];
311             }
312
313             if (isset($noteAttributes['from'])) {
314                 $note['from'] = (string) $noteAttributes['from'];
315             }
316
317             $notes[] = $note;
318         }
319
320         return $notes;
321     }
322 }