Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
QtiTestUtils
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 4
702
0.00% covered (danger)
0.00%
0 / 1
 storeQtiResource
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
110
 emptyImsManifest
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 buildAssessmentItemRefsTestMap
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
156
 getTestDefinition
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
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) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22namespace oat\taoQtiTest\models;
23
24use DOMDocument;
25use InvalidArgumentException;
26use oat\generis\Helper\SystemHelper;
27use oat\taoQtiItem\model\qti\Resource;
28use oat\taoQtiTest\models\export\Formats\Metadata\TestPackageExport as MetadataTestPackageExport;
29use qtism\data\storage\xml\XmlDocument;
30use oat\oatbox\filesystem\FileSystemService;
31use oat\oatbox\filesystem\Directory;
32use qtism\data\AssessmentTest;
33use oat\oatbox\service\ConfigurableService;
34use taoItems_models_classes_TemplateRenderer;
35
36/**
37 * Miscellaneous utility methods for the QtiTest extension.
38 *
39 * Service created from \taoQtiTest_helpers_Utils static helper class
40 *
41 * @author Jérôme Bogaerts <jerome@taotesting.com>
42 */
43class QtiTestUtils extends ConfigurableService
44{
45    public const SERVICE_ID = 'taoQtiTest/QtiTestUtils';
46
47    /**
48     * Store a file referenced by $qtiResource into the final $testContent folder. If the path provided
49     * by $qtiResource contains sub-directories, they will be created before copying the file (even
50     * if $copy = false).
51     *
52     * @param Directory $testContent The pointer to the TAO Test Content folder.
53     * @param \oat\taoQtiItem\model\qti\Resource|string $qtiResource The QTI resource to be copied into $testContent.
54     *                                                               If given as a string, it must be the relative
55     *                                                               (to the IMS QTI Package) path to the resource file.
56     * @param string $origin The path to the directory (root folder of extracted IMS QTI package) containing the
57     *                       QTI resource to be copied.
58     * @param boolean $copy If set to false, the file will not be actually copied.
59     * @param string $rename A new filename  e.g. 'file.css' to be used at storage time.
60     * @return string The path were the file was copied/has to be copied (depending on the $copy argument).
61     * @throws InvalidArgumentException If one of the above arguments is invalid.
62     * @throws \common_Exception
63     */
64    public function storeQtiResource(Directory $testContent, $qtiResource, $origin, $copy = true, $rename = '')
65    {
66        $fss = $this->getServiceLocator()->get(FileSystemService::SERVICE_ID);
67        $fs = $fss->getFileSystem($testContent->getFileSystem()->getId());
68        $contentPath = $testContent->getPrefix();
69
70        $ds = DIRECTORY_SEPARATOR;
71        $contentPath = rtrim($contentPath, $ds);
72
73        if ($qtiResource instanceof Resource) {
74            $filePath = $qtiResource->getFile();
75        } elseif (is_string($qtiResource) === true) {
76            $filePath = $qtiResource;
77        } else {
78            throw new InvalidArgumentException(
79                "The 'qtiResource' argument must be a string or a taoQTI_models_classes_QTI_Resource object."
80            );
81        }
82
83        $resourcePathinfo = pathinfo($filePath);
84
85        if (empty($resourcePathinfo['dirname']) === false && $resourcePathinfo['dirname'] !== '.') {
86            // The resource file is not at the root of the archive but in a sub-folder.
87            // Let's copy it in the same way into the Test Content folder.
88            $breadCrumb = $contentPath . $ds . str_replace('/', $ds, $resourcePathinfo['dirname']);
89            $breadCrumb = rtrim($breadCrumb, $ds);
90            $finalName = (empty($rename) === true)
91                ? ($resourcePathinfo['filename'] . '.' . $resourcePathinfo['extension'])
92                : $rename;
93            $finalPath = $breadCrumb . $ds . $finalName;
94        } else {
95            // The resource file is at the root of the archive.
96            // Overwrite template test.xml (created by self::createContent() method above) file with the new one.
97            $finalName = (empty($rename) === true)
98                ? ($resourcePathinfo['filename'] . '.' . $resourcePathinfo['extension'])
99                : $rename;
100            $finalPath = $contentPath . $ds . $finalName;
101        }
102
103        if ($copy === true) {
104            $origin = str_replace('/', $ds, $origin);
105            $origin = rtrim($origin, $ds);
106            $sourcePath = $origin . $ds . str_replace('/', $ds, $filePath);
107
108            if (is_readable($sourcePath) === false) {
109                throw new \common_Exception(
110                    "An error occured while copying the QTI resource from '${sourcePath}' to '${finalPath}'."
111                );
112            }
113
114            $fh = fopen($sourcePath, 'r');
115            $success = $fs->writeStream($finalPath, $fh);
116            fclose($fh);
117
118            if (!$success) {
119                throw new \common_Exception(
120                    "An error occured while copying the QTI resource from '${sourcePath}' to '${finalPath}'."
121                );
122            }
123        }
124
125        return $finalPath;
126    }
127
128    /**
129     * Returns an empty IMS Manifest file as a DOMDocument, ready to be fill with
130     * new information about IMS QTI Items and Tests.
131     *
132     * @param $version string The requested QTI version. Can be "2.1" or "2.2". Default is "2.1".
133     * @return \DOMDocument
134     */
135    public function emptyImsManifest($version = '2.1'): ?DOMDocument
136    {
137        $manifestFileName = match ($version) {
138            '2.1' => 'imsmanifest',
139            '2.2' => 'imsmanifestQti22',
140            '3.0' => 'imsmanifestQti30',
141            default => false
142        };
143
144        if ($manifestFileName === false) {
145            return null;
146        }
147
148        $templateRenderer = new taoItems_models_classes_TemplateRenderer(
149            ROOT_PATH . 'taoQtiItem/model/qti/templates/' . $manifestFileName . '.tpl.php',
150            [
151                'qtiItems' => [],
152                'manifestIdentifier' => 'QTI-TEST-MANIFEST-'
153                    . \tao_helpers_Display::textCleaner(uniqid('tao', true), '-')
154            ]
155        );
156
157        $manifest = new \DOMDocument('1.0', TAO_DEFAULT_ENCODING);
158        $manifest->loadXML($templateRenderer->render());
159        return $manifest;
160    }
161
162    /**
163     * It is sometimes necessary to identify the link between assessmentItemRefs described in a QTI Test definition and
164     * the resources describing items in IMS Manifest file. This utility method helps you to achieve this.
165     *
166     * The method will return an array describing the IMS Manifest resources that were found in an IMS Manifest file
167     * on basis of the assessmentItemRefs found in an AssessmentTest definition. The keys of the arrays are
168     * assessmentItemRef identifiers and values are IMS Manifest Resources.
169     *
170     * If an IMS Manifest Resource cannot be found for a given assessmentItemRef, the value in the returned array will
171     * be false.
172     *
173     * @param XmlDocument $test A QTI Test Definition.
174     * @param \taoQtiTest_models_classes_ManifestParser $manifestParser A Manifest Parser.
175     * @param string $basePath The base path of the folder the IMS archive is exposed as a file system component.
176     * @return array An array containing two arrays (items and dependencies) where keys are identifiers and values
177     *               are oat\taoQtiItem\model\qti\Resource objects or false.
178     */
179    public function buildAssessmentItemRefsTestMap(
180        XmlDocument $test,
181        \taoQtiTest_models_classes_ManifestParser $manifestParser,
182        $basePath
183    ) {
184        $assessmentItemRefs = $test->getDocumentComponent()->getComponentsByClassName('assessmentItemRef');
185        $map = ['items' => [], 'dependencies' => []];
186        $itemResources = $manifestParser->getResources(
187            Resource::getItemTypes(),
188            \taoQtiTest_models_classes_ManifestParser::FILTER_RESOURCE_TYPE
189        );
190        $allResources = $manifestParser->getResources();
191
192        // cleanup $basePath.
193        $basePath = rtrim($basePath, "/\\");
194        $basePath = \helpers_File::truePath($basePath);
195        $basePath .= DIRECTORY_SEPARATOR;
196
197        $documentURI = preg_replace("/^file:\/{1,3}/", '', $test->getDomDocument()->documentURI);
198        $testPathInfo = pathinfo($documentURI);
199        $testBasePath = \tao_helpers_File::truePath($testPathInfo['dirname']) . DIRECTORY_SEPARATOR;
200
201        foreach ($assessmentItemRefs as $itemRef) {
202            // Find the QTI Resource (in IMS Manifest) related to the item ref.
203            // To achieve this, we compare their path.
204            $itemRefRelativeHref = str_replace('/', DIRECTORY_SEPARATOR, $itemRef->getHref());
205            $itemRefRelativeHref = ltrim($itemRefRelativeHref, "/\\");
206            $itemRefCanonicalHref = \helpers_File::truePath($testBasePath . $itemRefRelativeHref);
207            $map['items'][$itemRef->getIdentifier()] = false;
208
209            // Compare with items referenced in the manifest.
210            foreach ($itemResources as $itemResource) {
211                $itemResourceRelativeHref = str_replace('/', DIRECTORY_SEPARATOR, $itemResource->getFile());
212                $itemResourceRelativeHref = ltrim($itemResourceRelativeHref, "/\\");
213
214                $itemResourceCanonicalHref = \helpers_File::truePath($basePath . $itemResourceRelativeHref);
215
216                // With some Windows flavours (Win7, Win8), the $itemRefCanonicalHref comes out with
217                // a leading 'file:\' component. Let's clean this. (str_replace is binary-safe \0/)
218                $os = SystemHelper::getOperatingSystem();
219                if ($os === 'WINNT' || $os === 'WIN32' || $os === 'Windows') {
220                    $itemRefCanonicalHref = str_replace('file:\\', '', $itemRefCanonicalHref);
221
222                    // And moreover, it sometimes refer the temp directory as Windows\TEMP instead of Windows\Temp.
223                    $itemRefCanonicalHref = str_replace('\\TEMP\\', '\\Temp\\', $itemRefCanonicalHref);
224                    $itemResourceCanonicalHref = str_replace('\\TEMP\\', '\\Temp\\', $itemResourceCanonicalHref);
225                }
226
227                // With some MacOS flavours, the $itemRefCanonicalHref comes out with
228                // a leading '/private' component. Clean it!
229                if ($os === 'Darwin') {
230                    $itemRefCanonicalHref = str_replace('/private', '', $itemRefCanonicalHref);
231                }
232
233                if ($itemResourceCanonicalHref == $itemRefCanonicalHref && is_file($itemResourceCanonicalHref)) {
234                    // assessmentItemRef <-> IMS Manifest resource successful binding!
235                    $map['items'][$itemRef->getIdentifier()] = $itemResource;
236
237                    //get dependencies for each item
238                    foreach ($itemResource->getDependencies() as $dependencyIdentifier) {
239                        /** @var \taoQtiTest_models_classes_QtiResource $resource */
240                        foreach ($allResources as $resource) {
241                            if ($dependencyIdentifier == $resource->getIdentifier()) {
242                                $map['dependencies'][$dependencyIdentifier] = $resource;
243                                break;
244                            }
245                        }
246                    }
247                    break;
248                }
249            }
250        }
251        return $map;
252    }
253
254    /**
255     * Retrieve the Test Definition the test session is built from as an AssessmentTest object.
256     * @param string $qtiTestCompilation (e.g. <i>'http://sample/first.rdf#i14363448108243883-
257     *                                   |http://sample/first.rdf#i14363448109065884+'</i>)
258     * @return AssessmentTest The AssessmentTest object the current test session is built from.
259     * @throws QtiTestExtractionFailedException
260     */
261    public function getTestDefinition($qtiTestCompilation)
262    {
263        try {
264            $directoryIds = explode('|', $qtiTestCompilation);
265            $directory = \tao_models_classes_service_FileStorage::singleton()->getDirectoryById($directoryIds[0]);
266            $compilationDataService = $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID);
267            return $compilationDataService->readCompilationData(
268                $directory,
269                \taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_FILENAME
270            );
271        } catch (\common_exception_FileReadFailedException $e) {
272            throw new QtiTestExtractionFailedException($e->getMessage());
273        }
274    }
275}