Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
30.50% |
43 / 141 |
|
33.33% |
9 / 27 |
CRAP | |
0.00% |
0 / 1 |
AssetParser | |
30.50% |
43 / 141 |
|
33.33% |
9 / 27 |
2688.08 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
extract | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
extractApipAccessibilityAssets | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
extractImg | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
extractObject | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
5.02 | |||
extractXinclude | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
extractStyleSheet | |
9.09% |
1 / 11 |
|
0.00% |
0 / 1 |
33.05 | |||
extractPortableAssetElements | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
extractCustomElement | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getPortableCustomInteraction | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
5.02 | |||
getPortableInfoControl | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
5.02 | |||
loadObjectAssets | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
56 | |||
addAsset | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
loadCustomElementPropertiesAssets | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
loadCustomElementAssets | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
156 | |||
getXmlProperties | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
extractAdvancedCustomInteractionAssets | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
loadStyleSheetAsset | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
90 | |||
setGetSharedLibraries | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGetSharedLibraries | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setGetXinclude | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGetXinclude | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setGetCustomElementDefinition | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGetCustomElementDefinition | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isDeepParsing | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setDeepParsing | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCustomInteractionAssetExtractorAllocator | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /* |
4 | * This program is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU General Public License |
6 | * as published by the Free Software Foundation; under version 2 |
7 | * of the License (non-upgradable). |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License |
15 | * along with this program; if not, write to the Free Software |
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * |
18 | * Copyright (c) 2015 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); |
19 | * |
20 | * |
21 | */ |
22 | |
23 | namespace oat\taoQtiItem\model\qti; |
24 | |
25 | use common_exception_Error; |
26 | use oat\oatbox\filesystem\Directory; |
27 | use oat\oatbox\service\ServiceManager; |
28 | use oat\taoQtiItem\model\qti\container\Container; |
29 | use oat\taoQtiItem\model\qti\CustomInteractionAsset\CustomInteractionAssetExtractorAllocator; |
30 | use oat\taoQtiItem\model\qti\interaction\CustomInteraction; |
31 | use oat\taoQtiItem\model\qti\interaction\PortableCustomInteraction; |
32 | use SimpleXMLElement; |
33 | use tao_helpers_Xml; |
34 | |
35 | /** |
36 | * Parse and Extract all assets of an item. |
37 | * |
38 | * @package taoQtiItem |
39 | * @author Bertrand Chevrier <bertrand@taotesting.com> |
40 | */ |
41 | class AssetParser |
42 | { |
43 | /** |
44 | * The item to parse |
45 | * @var Item |
46 | */ |
47 | private $item; |
48 | |
49 | /** |
50 | * Set mode - if parser have to find shared libraries (PCI and PIC) |
51 | * @var bool |
52 | */ |
53 | private $getSharedLibraries = true; |
54 | |
55 | /** |
56 | * Set mode - if parser have to find shared stimulus |
57 | * @var bool |
58 | */ |
59 | private $getXinclude = true; |
60 | |
61 | /** |
62 | * Set mode - if parser have to find portable element |
63 | * @var bool |
64 | */ |
65 | private $getCustomElement = false; |
66 | |
67 | /** |
68 | * Set mode - if parser have to find all external entries ( like url, require etc ) |
69 | * @var bool |
70 | */ |
71 | private $deepParsing = true; |
72 | |
73 | /** |
74 | * The extracted assets |
75 | * @var array |
76 | */ |
77 | private $assets = []; |
78 | |
79 | /** |
80 | * @var Directory |
81 | */ |
82 | private $directory; |
83 | |
84 | /** |
85 | * Creates a new parser from an item |
86 | * @param Item $item the item to parse |
87 | * @param $directory |
88 | */ |
89 | public function __construct(Item $item, Directory $directory) |
90 | { |
91 | $this->item = $item; |
92 | $this->directory = $directory; |
93 | } |
94 | |
95 | /** |
96 | * Extract all assets from the current item |
97 | * @return array the assets by type |
98 | */ |
99 | public function extract() |
100 | { |
101 | foreach ($this->item->getComposingElements() as $element) { |
102 | $this->extractImg($element); |
103 | $this->extractObject($element); |
104 | $this->extractStyleSheet($element); |
105 | $this->extractCustomElement($element); |
106 | if ($this->getGetXinclude()) { |
107 | $this->extractXinclude($element); |
108 | } |
109 | } |
110 | $this->extractApipAccessibilityAssets(); |
111 | return $this->assets; |
112 | } |
113 | |
114 | private function extractApipAccessibilityAssets() |
115 | { |
116 | if (property_exists($this->item, 'apipAccessibility')) { |
117 | try { |
118 | $assets = tao_helpers_Xml::extractElements( |
119 | 'fileHref', |
120 | $this->item->getApipAccessibility(), |
121 | 'http://www.imsglobal.org/xsd/apip/apipv1p0/imsapip_qtiv1p0' |
122 | ); |
123 | foreach ($assets as $asset) { |
124 | $this->addAsset('apip', $asset); |
125 | } |
126 | } catch (common_exception_Error $e) { |
127 | } |
128 | } |
129 | } |
130 | |
131 | /** |
132 | * Lookup and extract assets from IMG elements |
133 | * @param Element $element container of the target element |
134 | */ |
135 | private function extractImg(Element $element) |
136 | { |
137 | if ($element instanceof Container) { |
138 | foreach ($element->getElements('oat\taoQtiItem\model\qti\Img') as $img) { |
139 | $this->addAsset('img', $img->attr('src')); |
140 | } |
141 | } |
142 | } |
143 | |
144 | /** |
145 | * Lookup and extract assets from a QTI Object |
146 | * @param Element $element the element itself or a container of the target element |
147 | */ |
148 | private function extractObject(Element $element) |
149 | { |
150 | if ($element instanceof Container) { |
151 | foreach ($element->getElements('oat\taoQtiItem\model\qti\QtiObject') as $object) { |
152 | $this->loadObjectAssets($object); |
153 | } |
154 | } |
155 | if ($element instanceof QtiObject) { |
156 | $this->loadObjectAssets($element); |
157 | } |
158 | } |
159 | |
160 | /** |
161 | * Lookup and extract assets from IMG elements |
162 | * @param Element $element container of the target element |
163 | */ |
164 | private function extractXinclude(Element $element) |
165 | { |
166 | if ($element instanceof Container) { |
167 | foreach ($element->getElements('oat\taoQtiItem\model\qti\Xinclude') as $xinclude) { |
168 | $this->addAsset('xinclude', $xinclude->attr('href')); |
169 | } |
170 | } |
171 | } |
172 | |
173 | /** |
174 | * Lookup and extract assets from a stylesheet element |
175 | * @param Element $element the stylesheet element |
176 | */ |
177 | private function extractStyleSheet(Element $element) |
178 | { |
179 | if ($element instanceof StyleSheet) { |
180 | $href = $element->attr('href'); |
181 | $this->addAsset('css', $href); |
182 | |
183 | $parsedUrl = parse_url($href); |
184 | if ( |
185 | $this->isDeepParsing() && array_key_exists('path', $parsedUrl) && !array_key_exists( |
186 | 'host', |
187 | $parsedUrl |
188 | ) |
189 | ) { |
190 | $file = $this->directory->getFile($parsedUrl['path']); |
191 | if ($file->exists()) { |
192 | $this->loadStyleSheetAsset($file->read()); |
193 | } |
194 | } |
195 | } |
196 | } |
197 | |
198 | public function extractPortableAssetElements() |
199 | { |
200 | foreach ($this->item->getComposingElements() as $element) { |
201 | $this->extractCustomElement($element); |
202 | } |
203 | return $this->assets; |
204 | } |
205 | |
206 | /** |
207 | * Lookup and extract assets from a custom element (CustomInteraction, PCI, PIC) |
208 | * @param Element $element the element itself or a container of the target element |
209 | */ |
210 | public function extractCustomElement(Element $element) |
211 | { |
212 | $this->getPortableCustomInteraction($element); |
213 | $this->getPortableInfoControl($element); |
214 | } |
215 | |
216 | public function getPortableCustomInteraction(Element $element) |
217 | { |
218 | if ($element instanceof Container) { |
219 | foreach ($element->getElements('oat\taoQtiItem\model\qti\interaction\CustomInteraction') as $interaction) { |
220 | $this->loadCustomElementAssets($interaction); |
221 | } |
222 | } |
223 | if ($element instanceof CustomInteraction) { |
224 | $this->loadCustomElementAssets($element); |
225 | } |
226 | } |
227 | |
228 | public function getPortableInfoControl(Element $element) |
229 | { |
230 | if ($element instanceof Container) { |
231 | foreach ($element->getElements('oat\taoQtiItem\model\qti\interaction\InfoControl') as $interaction) { |
232 | $this->loadCustomElementAssets($interaction); |
233 | } |
234 | } |
235 | if ($element instanceof InfoControl) { |
236 | $this->loadCustomElementAssets($element); |
237 | } |
238 | } |
239 | |
240 | /** |
241 | * Loads assets from an QTI object element |
242 | * @param QtiObject $object the object |
243 | */ |
244 | private function loadObjectAssets(QtiObject $object) |
245 | { |
246 | |
247 | $type = $object->attr('type'); |
248 | |
249 | if (strpos($type, "image") !== false) { |
250 | $this->addAsset('img', $object->attr('data')); |
251 | } elseif (strpos($type, "video") !== false || strpos($type, "ogg") !== false) { |
252 | $this->addAsset('video', $object->attr('data')); |
253 | } elseif (strpos($type, "audio") !== false) { |
254 | $this->addAsset('audio', $object->attr('data')); |
255 | } elseif (strpos($type, "text/html") !== false) { |
256 | $this->addAsset('html', $object->attr('data')); |
257 | } elseif ($type === 'application/pdf') { |
258 | $this->addAsset('pdf', $object->attr('data')); |
259 | } |
260 | } |
261 | |
262 | /** |
263 | * Add the asset to the current list |
264 | * @param string $type the asset type: img, css, js, audio, video, font, etc. |
265 | * @param string $uri the asset URI |
266 | */ |
267 | private function addAsset($type, $uri) |
268 | { |
269 | if (!array_key_exists($type, $this->assets)) { |
270 | $this->assets[$type] = []; |
271 | } |
272 | if (!empty($uri) && !in_array($uri, $this->assets[$type])) { |
273 | $this->assets[$type][] = $uri; |
274 | } |
275 | } |
276 | |
277 | /** |
278 | * Search assets URI in custom element properties |
279 | * The PCI standard will be extended in the future with typed property value |
280 | * (boolean, integer, float, string, uri, html etc.) |
281 | * Meanwhile, we use the special property name uri for the special type "URI" that represents a file URI. |
282 | * Portable element using this reserved property should be migrated later on when the standard is updated. |
283 | * |
284 | * @param array $properties |
285 | */ |
286 | private function loadCustomElementPropertiesAssets($properties) |
287 | { |
288 | if (is_array($properties)) { |
289 | if (isset($properties['uri'])) { |
290 | $this->addAsset('document', urldecode($properties['uri'])); |
291 | } else { |
292 | foreach ($properties as $property) { |
293 | if (is_array($property)) { |
294 | $this->loadCustomElementPropertiesAssets($property); |
295 | } |
296 | } |
297 | } |
298 | } |
299 | } |
300 | |
301 | /** |
302 | * Load assets from the custom elements (CustomInteraction, PCI, PIC) |
303 | * @param Element $element the custom element |
304 | */ |
305 | private function loadCustomElementAssets(Element $element) |
306 | { |
307 | if ($this->getGetCustomElementDefinition()) { |
308 | $this->assets[$element->getTypeIdentifier()] = $element; |
309 | } |
310 | |
311 | $xmls = []; |
312 | if ($element instanceof PortableCustomInteraction || $element instanceof PortableInfoControl) { |
313 | //some portable elements contains htmlentitied markup in their properties... |
314 | $xmls = $this->getXmlProperties($element->getProperties()); |
315 | } |
316 | |
317 | //parse and extract assets from markup using XPATH |
318 | if ($element instanceof CustomInteraction || $element instanceof InfoControl) { |
319 | // http://php.net/manual/fr/simplexmlelement.xpath.php#116622 |
320 | $sanitizedMarkup = str_replace('xmlns=', 'ns=', $element->getMarkup()); |
321 | |
322 | $xmls[] = new SimpleXMLElement($sanitizedMarkup); |
323 | |
324 | $this->loadCustomElementPropertiesAssets($element->getProperties()); |
325 | |
326 | /** @var SimpleXMLElement $xml */ |
327 | foreach ($xmls as $xml) { |
328 | foreach ($xml->xpath('//img') as $img) { |
329 | $this->addAsset('img', (string)$img['src']); |
330 | } |
331 | foreach ($xml->xpath('//video') as $video) { |
332 | $this->addAsset('video', (string)$video['src']); |
333 | } |
334 | foreach ($xml->xpath('//audio') as $audio) { |
335 | $this->addAsset('audio', (string)$audio['src']); |
336 | } |
337 | foreach ($xml->xpath('//include') as $xinclude) { |
338 | $this->addAsset('xinclude', (string)$xinclude['href']); |
339 | } |
340 | } |
341 | } |
342 | |
343 | if ($element instanceof CustomInteraction) { |
344 | $this->extractAdvancedCustomInteractionAssets($element); |
345 | } |
346 | } |
347 | |
348 | private function getXmlProperties($properties) |
349 | { |
350 | $xmls = []; |
351 | foreach ($properties as $property) { |
352 | if (is_array($property)) { |
353 | $xmls = array_merge($xmls, $this->getXmlProperties($property)); |
354 | } |
355 | if (is_string($property)) { |
356 | $xml = simplexml_load_string('<div>' . $property . '</div>'); |
357 | if ($xml !== false) { |
358 | $xmls[] = $xml; |
359 | } |
360 | } |
361 | } |
362 | return $xmls; |
363 | } |
364 | |
365 | private function extractAdvancedCustomInteractionAssets(CustomInteraction $interaction): void |
366 | { |
367 | $extractorAllocator = $this->getCustomInteractionAssetExtractorAllocator(); |
368 | $extractor = $extractorAllocator->allocateExtractor($interaction->getTypeIdentifier()); |
369 | |
370 | foreach ($extractor->extract($interaction) as $asset) { |
371 | // `apip` type used as something common in reason that it's not possible do define a specific type, |
372 | $this->addAsset('apip', $asset); |
373 | } |
374 | } |
375 | |
376 | /** |
377 | * Parse, extract and load assets from the stylesheet content |
378 | * @param string $css the stylesheet content |
379 | */ |
380 | private function loadStyleSheetAsset($css) |
381 | { |
382 | |
383 | $imageRe = "/url\\s*\\(['|\"]?([^)]*\.(png|jpg|jpeg|gif|svg))['|\"]?\\)/mi"; |
384 | $importRe = "/@import\\s*(url\\s*\\()?['\"]?([^;]*)['\"]/mi"; |
385 | $fontFaceRe = "/@font-face\\s*\\{(.*)?\\}/mi"; |
386 | $fontRe = "/url\\s*\\(['|\"]?([^)'|\"]*)['|\"]?\\)/i"; |
387 | |
388 | //extract images |
389 | preg_match_all($imageRe, $css, $matches); |
390 | if (isset($matches[1])) { |
391 | foreach ($matches[1] as $match) { |
392 | $this->addAsset('img', $match); |
393 | } |
394 | } |
395 | |
396 | //extract @import |
397 | preg_match_all($importRe, $css, $matches); |
398 | if (isset($matches[2])) { |
399 | foreach ($matches[2] as $match) { |
400 | $this->addAsset('css', $match); |
401 | } |
402 | } |
403 | |
404 | //extract fonts |
405 | preg_match_all($fontFaceRe, $css, $matches); |
406 | if (isset($matches[1])) { |
407 | foreach ($matches[1] as $faceMatch) { |
408 | preg_match_all($fontRe, $faceMatch, $fontMatches); |
409 | if (isset($fontMatches[1])) { |
410 | foreach ($fontMatches[1] as $fontMatch) { |
411 | $this->addAsset('font', $fontMatch); |
412 | } |
413 | } |
414 | } |
415 | } |
416 | } |
417 | |
418 | /** |
419 | * @param boolean $getSharedLibraries |
420 | */ |
421 | public function setGetSharedLibraries($getSharedLibraries) |
422 | { |
423 | $this->getSharedLibraries = $getSharedLibraries; |
424 | } |
425 | |
426 | /** |
427 | * @return boolean |
428 | */ |
429 | public function getGetSharedLibraries() |
430 | { |
431 | return $this->getSharedLibraries; |
432 | } |
433 | |
434 | /** |
435 | * @param boolean $getXinclude |
436 | */ |
437 | public function setGetXinclude($getXinclude) |
438 | { |
439 | $this->getXinclude = $getXinclude; |
440 | } |
441 | |
442 | /** |
443 | * @return boolean |
444 | */ |
445 | public function getGetXinclude() |
446 | { |
447 | return $this->getXinclude; |
448 | } |
449 | |
450 | /** |
451 | * @param boolean $getCustomElement |
452 | */ |
453 | public function setGetCustomElementDefinition($getCustomElement) |
454 | { |
455 | $this->getCustomElement = $getCustomElement; |
456 | } |
457 | |
458 | /** |
459 | * @return boolean |
460 | */ |
461 | public function getGetCustomElementDefinition() |
462 | { |
463 | return $this->getCustomElement; |
464 | } |
465 | |
466 | |
467 | /** |
468 | * @return boolean |
469 | */ |
470 | public function isDeepParsing() |
471 | { |
472 | return $this->deepParsing; |
473 | } |
474 | |
475 | /** |
476 | * @param boolean $deepParsing |
477 | */ |
478 | public function setDeepParsing($deepParsing) |
479 | { |
480 | $this->deepParsing = $deepParsing; |
481 | } |
482 | |
483 | private function getCustomInteractionAssetExtractorAllocator(): CustomInteractionAssetExtractorAllocator |
484 | { |
485 | return ServiceManager::getServiceManager() |
486 | ->getContainer() |
487 | ->get(CustomInteractionAssetExtractorAllocator::class); |
488 | } |
489 | } |