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 | } |