Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 160 |
|
0.00% |
0 / 17 |
CRAP | |
0.00% |
0 / 1 |
PortableElementItemParser | |
0.00% |
0 / 160 |
|
0.00% |
0 / 17 |
1892 | |
0.00% |
0 / 1 |
getService | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getPortableFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
importPortableElementFile | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
hasPortableElement | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isPortableElementAsset | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFileInfo | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getQtiModel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setQtiModel | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
feedRequiredFiles | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getSourceAdjustedNodulePath | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
parsePortableElement | |
0.00% |
0 / 88 |
|
0.00% |
0 / 1 |
342 | |||
setSource | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setItemDir | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getPortableObjects | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
importPortableElements | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 | |||
replaceLibAliases | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
isRelativePath | |
0.00% |
0 / 1 |
|
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) 2016 (original work) Open Assessment Technologies SA; |
19 | * |
20 | */ |
21 | |
22 | namespace oat\taoQtiItem\model\portableElement\parser\itemParser; |
23 | |
24 | use oat\taoQtiItem\model\portableElement\exception\PortableElementInconsistencyModelException; |
25 | use oat\taoQtiItem\model\portableElement\element\PortableElementObject; |
26 | use oat\taoQtiItem\model\portableElement\model\PortableModelRegistry; |
27 | use oat\taoQtiItem\model\portableElement\model\PortableElementModel; |
28 | use oat\taoQtiItem\model\portableElement\PortableElementService; |
29 | use oat\taoQtiItem\model\qti\Item; |
30 | use oat\taoQtiItem\model\qti\Element; |
31 | use Zend\ServiceManager\ServiceLocatorAwareInterface; |
32 | use Zend\ServiceManager\ServiceLocatorAwareTrait; |
33 | |
34 | class PortableElementItemParser implements ServiceLocatorAwareInterface |
35 | { |
36 | use ServiceLocatorAwareTrait; |
37 | |
38 | /** |
39 | * @var Item |
40 | */ |
41 | protected $qtiModel; |
42 | |
43 | protected $importingFiles = []; |
44 | protected $requiredFiles = []; |
45 | protected $portableObjects = []; |
46 | protected $picModels = []; |
47 | |
48 | protected $source; |
49 | protected $itemDir; |
50 | |
51 | /** |
52 | * @var PortableElementService |
53 | */ |
54 | protected $service; |
55 | |
56 | /** |
57 | * @return PortableElementService |
58 | */ |
59 | public function getService() |
60 | { |
61 | if (!$this->service) { |
62 | $this->service = new PortableElementService(); |
63 | $this->service->setServiceLocator($this->getServiceLocator()); |
64 | } |
65 | return $this->service; |
66 | } |
67 | |
68 | /** |
69 | * @return PortableModelRegistry |
70 | */ |
71 | protected function getPortableFactory() |
72 | { |
73 | return PortableModelRegistry::getRegistry(); |
74 | } |
75 | |
76 | /** |
77 | * Handle pci import process for a file |
78 | * |
79 | * @param $absolutePath |
80 | * @param $relativePath |
81 | * @return array |
82 | * @throws \common_Exception |
83 | * @throws \tao_models_classes_FileNotFoundException |
84 | */ |
85 | public function importPortableElementFile($absolutePath, $relativePath) |
86 | { |
87 | if ($this->isPortableElementAsset($relativePath)) { |
88 | //marked the file as being ok to be imported in the end |
89 | $this->importingFiles[] = $relativePath; |
90 | |
91 | //@todo remove qti file used by PCI |
92 | |
93 | return $this->getFileInfo($absolutePath, $relativePath); |
94 | } else { |
95 | throw new \common_Exception( |
96 | 'trying to import an asset that is not part of the portable element asset list' |
97 | ); |
98 | } |
99 | } |
100 | |
101 | /** |
102 | * Check if Item contains portable element |
103 | * |
104 | * @return bool |
105 | */ |
106 | public function hasPortableElement() |
107 | { |
108 | return (count($this->requiredFiles) > 0); |
109 | } |
110 | |
111 | /** |
112 | * Check if file is required by a portable element |
113 | * |
114 | * @param $fileRelativePath |
115 | * @return bool |
116 | */ |
117 | public function isPortableElementAsset($fileRelativePath) |
118 | { |
119 | return isset($this->requiredFiles[$fileRelativePath]); |
120 | } |
121 | |
122 | /** |
123 | * Get details about file |
124 | * |
125 | * @param $path |
126 | * @param $relPath |
127 | * @return array |
128 | * @throws \tao_models_classes_FileNotFoundException |
129 | */ |
130 | public function getFileInfo($path, $relPath) |
131 | { |
132 | |
133 | if (file_exists($path)) { |
134 | return [ |
135 | 'name' => basename($path), |
136 | 'uri' => $relPath, |
137 | 'mime' => \tao_helpers_File::getMimeType($path), |
138 | 'filePath' => $path, |
139 | 'size' => filesize($path), |
140 | ]; |
141 | } |
142 | |
143 | throw new \tao_models_classes_FileNotFoundException($path); |
144 | } |
145 | |
146 | /** |
147 | * @return Item |
148 | */ |
149 | public function getQtiModel() |
150 | { |
151 | return $this->qtiModel; |
152 | } |
153 | |
154 | /** |
155 | * |
156 | * @param Item $item |
157 | * @return $this |
158 | */ |
159 | public function setQtiModel(Item $item) |
160 | { |
161 | $this->qtiModel = $item; |
162 | $this->feedRequiredFiles($item); |
163 | return $this; |
164 | } |
165 | |
166 | /** |
167 | * Feed the instance with portable related data extracted from the item |
168 | * |
169 | * @param Item $item |
170 | * @throws \common_Exception |
171 | */ |
172 | protected function feedRequiredFiles(Item $item) |
173 | { |
174 | $this->requiredFiles = []; |
175 | $this->portableObjects = []; |
176 | $this->picModels = []; |
177 | |
178 | $models = $this->getPortableFactory()->getModels(); |
179 | |
180 | foreach ($models as $model) { |
181 | $className = $model->getQtiElementClassName(); |
182 | $portableElementsXml = $item->getComposingElements($className); |
183 | foreach ($portableElementsXml as $portableElementXml) { |
184 | $this->parsePortableElement($model, $portableElementXml); |
185 | } |
186 | } |
187 | } |
188 | |
189 | protected function getSourceAdjustedNodulePath($path) |
190 | { |
191 | $realpath = realpath($this->itemDir . DIRECTORY_SEPARATOR . $path); |
192 | $sourcePath = realpath($this->source); |
193 | return str_replace($sourcePath . DIRECTORY_SEPARATOR, '', $realpath); |
194 | } |
195 | |
196 | /** |
197 | * Parse individual portable element into the given portable model |
198 | * @param PortableElementModel $model |
199 | * @param Element $portableElement |
200 | * @throws \common_Exception |
201 | * @throws PortableElementInconsistencyModelException |
202 | */ |
203 | protected function parsePortableElement(PortableElementModel $model, Element $portableElement) |
204 | { |
205 | $typeId = $portableElement->getTypeIdentifier(); |
206 | $libs = []; |
207 | $librariesFiles = []; |
208 | $entryPoint = []; |
209 | |
210 | //Adjust file resource entries where {QTI_NS}/xxx/yyy is equivalent to {QTI_NS}/xxx/yyy.js |
211 | foreach ($portableElement->getLibraries() as $lib) { |
212 | if (preg_match('/^' . $typeId . '/', $lib) && substr($lib, -3) != '.js') {//filter shared stimulus |
213 | $librariesFiles[] = $lib . '.js';//amd modules |
214 | $libs[] = $lib . '.js'; |
215 | } else { |
216 | $libs[] = $lib;//shared libs |
217 | } |
218 | } |
219 | |
220 | $moduleFiles = []; |
221 | $emptyModules = [];//list of modules that are referenced directly in the module node |
222 | $adjustedModules = []; |
223 | foreach ($portableElement->getModules() as $id => $paths) { |
224 | $adjustedPaths = []; |
225 | if (empty($paths)) { |
226 | $emptyModules[] = $id; |
227 | continue; |
228 | } |
229 | foreach ($paths as $path) { |
230 | if ($this->isRelativePath($path)) { |
231 | //only copy into data the relative files |
232 | $moduleFiles[] = $path; |
233 | $adjustedPaths[] = $this->getSourceAdjustedNodulePath($path); |
234 | } else { |
235 | $adjustedPaths[] = $path; |
236 | } |
237 | } |
238 | $adjustedModules[$id] = $adjustedPaths; |
239 | } |
240 | |
241 | /** |
242 | * Parse the standard portable configuration if applicable. |
243 | * Local config files will be preloaded into the registry itself and the registered modules will be included |
244 | * as required dependency files. |
245 | * Per standard, every config file have the following structure: |
246 | * { |
247 | * "waitSeconds": 15, |
248 | * "paths": { |
249 | * "graph": "https://example.com/js/modules/graph1.01/graph.js", |
250 | * "foo": "foo/bar1.2/foo.js" |
251 | * } |
252 | * } |
253 | */ |
254 | $configDataArray = []; |
255 | $configFiles = []; |
256 | foreach ($portableElement->getConfig() as $configFile) { |
257 | //only read local config file |
258 | if ($this->isRelativePath($configFile)) { |
259 | //save the content and file config data in registry, to allow later retrieval |
260 | $configFiles[] = $configFile; |
261 | |
262 | |
263 | //read the config file content |
264 | $configData = json_decode(file_get_contents($this->itemDir . DIRECTORY_SEPARATOR . $configFile), true); |
265 | if (!empty($configData)) { |
266 | if (isset($configData['paths'])) { |
267 | foreach ($configData['paths'] as $id => $path) { |
268 | // only copy the relative files to local portable element filesystem, absolute ones are |
269 | // loaded dynamically |
270 | if ($this->isRelativePath($path)) { |
271 | //resolution of path, relative to the current config file it has been defined in |
272 | $path = dirname($configFile) . DIRECTORY_SEPARATOR . $path; |
273 | if (file_exists($this->itemDir . DIRECTORY_SEPARATOR . $path)) { |
274 | $moduleFiles[] = $path; |
275 | $configData['paths'][$id] = $this->getSourceAdjustedNodulePath($path); |
276 | ; |
277 | } else { |
278 | throw new \tao_models_classes_FileNotFoundException( |
279 | "The portable config {$configFile} references a missing module file " |
280 | . "{$id} => {$path}" |
281 | ); |
282 | } |
283 | } |
284 | } |
285 | } |
286 | $configDataArray[] = [ |
287 | 'file' => $this->getSourceAdjustedNodulePath($configFile), |
288 | 'data' => $configData |
289 | ]; |
290 | } |
291 | } else { |
292 | $configDataArray[] = ['file' => $configFile]; |
293 | } |
294 | } |
295 | |
296 | /** |
297 | * In the standard IMS PCI, entry points become optionnal |
298 | */ |
299 | if (!empty($portableElement->getEntryPoint())) { |
300 | $entryPoint[] = $portableElement->getEntryPoint(); |
301 | } |
302 | |
303 | //register the files here |
304 | $data = [ |
305 | 'typeIdentifier' => $typeId, |
306 | 'version' => $portableElement->getVersion(), |
307 | 'label' => $typeId, |
308 | 'short' => $typeId, |
309 | 'runtime' => [ |
310 | 'hook' => $portableElement->getEntryPoint(), |
311 | 'libraries' => $libs, |
312 | 'stylesheets' => $portableElement->getStylesheets(), |
313 | 'mediaFiles' => $portableElement->getMediaFiles(), |
314 | 'config' => $configDataArray, |
315 | 'modules' => $adjustedModules |
316 | ] |
317 | ]; |
318 | |
319 | $portableObject = $model->createDataObject($data); |
320 | |
321 | $compatibleRegisteredObject = $this->getService()->getLatestCompatibleVersionElementById( |
322 | $portableObject->getModel()->getId(), |
323 | $portableObject->getTypeIdentifier(), |
324 | $portableObject->getVersion() |
325 | ); |
326 | |
327 | $latestVersionRegisteredObject = $this->getService()->getPortableElementByIdentifier( |
328 | $portableObject->getModel()->getId(), |
329 | $portableObject->getTypeIdentifier() |
330 | ); |
331 | |
332 | if (is_null($compatibleRegisteredObject) && !is_null($latestVersionRegisteredObject)) { |
333 | // @todo return a user exception to inform user of incompatible pci version found and that an item update |
334 | // is required |
335 | throw new \common_Exception( |
336 | 'Unable to import pci asset because compatible version is not found. ' |
337 | . 'Current version is ' . $latestVersionRegisteredObject->getVersion() . ' and imported is ' |
338 | . $portableObject->getVersion() |
339 | ); |
340 | } |
341 | |
342 | $this->portableObjects[$typeId] = $portableObject; |
343 | |
344 | $files = array_merge( |
345 | $entryPoint, |
346 | $librariesFiles, |
347 | $configFiles, |
348 | $moduleFiles, |
349 | $portableObject->getRuntimeKey('stylesheets'), |
350 | $portableObject->getRuntimeKey('mediaFiles') |
351 | ); |
352 | $this->requiredFiles = array_merge($this->requiredFiles, array_fill_keys($files, $typeId)); |
353 | } |
354 | |
355 | /** |
356 | * Set the root directory of the QTI package, where the qti manifest.xml is located |
357 | * |
358 | * @param $source |
359 | * @return $this |
360 | */ |
361 | public function setSource($source) |
362 | { |
363 | $this->source = $source; |
364 | return $this; |
365 | } |
366 | |
367 | /** |
368 | * Set the directory where the qti item qti.xml file is locate |
369 | * |
370 | * @param $itemDir |
371 | * @return $this |
372 | */ |
373 | public function setItemDir($itemDir) |
374 | { |
375 | $this->itemDir = $itemDir; |
376 | return $this; |
377 | } |
378 | |
379 | /** |
380 | * Get the parsed portable objects |
381 | * |
382 | * @return array |
383 | */ |
384 | public function getPortableObjects() |
385 | { |
386 | return $this->portableObjects; |
387 | } |
388 | |
389 | /** |
390 | * Do the import of portable elements |
391 | */ |
392 | public function importPortableElements() |
393 | { |
394 | if (count($this->importingFiles) != count($this->requiredFiles)) { |
395 | throw new \common_Exception( |
396 | 'Needed files are missing during Portable Element asset files ' |
397 | . print_r($this->requiredFiles, true) . ' ' . print_r($this->importingFiles, true) |
398 | ); |
399 | } |
400 | |
401 | /** @var PortableElementObject $object */ |
402 | foreach ($this->portableObjects as $object) { |
403 | $lastVersionModel = $this->getService()->getPortableElementByIdentifier( |
404 | $object->getModel()->getId(), |
405 | $object->getTypeIdentifier() |
406 | ); |
407 | // only register a pci that has not been register yet, subsequent update must be done through pci package |
408 | // import |
409 | if (is_null($lastVersionModel)) { |
410 | $this->getService()->registerModel( |
411 | $object, |
412 | $object->getRegistrationSourcePath($this->source, $this->itemDir) |
413 | ); |
414 | } else { |
415 | \common_Logger::i( |
416 | 'The imported item contains the portable element ' . $object->getTypeIdentifier() |
417 | . ' in a version ' . $object->getVersion() . ' compatible with the current ' |
418 | . $lastVersionModel->getVersion() |
419 | ); |
420 | } |
421 | } |
422 | return true; |
423 | } |
424 | |
425 | /** |
426 | * Replace the libs aliases with their relative url before saving into the registry |
427 | * This format is consistent with the format of TAO portable package manifest |
428 | * |
429 | * @param PortableElementObject $object |
430 | * @return PortableElementObject |
431 | */ |
432 | private function replaceLibAliases(PortableElementObject $object) |
433 | { |
434 | |
435 | $id = $object->getTypeIdentifier(); |
436 | $object->setRuntimeKey('libraries', array_map(function ($lib) use ($id) { |
437 | if (preg_match('/^' . $id . '/', $lib)) { |
438 | return $lib . '.js'; |
439 | } |
440 | return $lib; |
441 | }, $object->getRuntimeKey('libraries'))); |
442 | |
443 | return $object; |
444 | } |
445 | |
446 | private function isRelativePath($path) |
447 | { |
448 | return (strpos($path, 'http') !== 0); |
449 | } |
450 | } |