Passed
Push — master ( 7a7af2...fe97dd )
by Julito
08:17
created

scorm::parse_manifest()   D

Complexity

Conditions 33
Paths 22

Size

Total Lines 232
Code Lines 88

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 33
eloc 88
c 0
b 0
f 0
nc 22
nop 0
dl 0
loc 232
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Asset;
6
use Chamilo\CoreBundle\Framework\Container;
7
use Chamilo\CourseBundle\Entity\CLp;
8
use Chamilo\CourseBundle\Entity\CLpItem;
9
use Symfony\Component\DomCrawler\Crawler;
10
use League\Flysystem\ZipArchive\ZipArchiveAdapter;
11
12
/**
13
 * Defines the scorm class, which is meant to contain the scorm items (nuclear elements).
14
 *
15
 * @author    Yannick Warnier <[email protected]>
16
 */
17
class scorm extends learnpath
18
{
19
    public $manifest = [];
20
    public $resources = [];
21
    public $resources_att = [];
22
    public $organizations = [];
23
    public $organizations_att = [];
24
    public $metadata = [];
25
    // Will hold the references to resources for each item ID found.
26
    public $idrefs = [];
27
    // For each resource found, stores the file url/uri.
28
    public $refurls = [];
29
    /*  Path between the scorm/ directory and the imsmanifest.xml e.g.
30
    maritime_nav/maritime_nav. This is the path that will be used in the
31
    lp_path when importing a package. */
32
    public $subdir;
33
    public $manifestToString;
34
    public $items;
35
    // Keeps the zipfile safe for the object's life so that we can use it if no title avail.
36
    public $zipname = '';
37
    // Keeps an index of the number of uses of the zipname so far.
38
    public $lastzipnameindex = 0;
39
    public $manifest_encoding = 'UTF-8';
40
    public $asset = true;
41
    public $debug = true;
42
43
    /**
44
     * Class constructor. Based on the parent constructor.
45
     *
46
     * @param    string    Course code
47
     * @param    int    Learnpath ID in DB
48
     * @param    int    User ID
49
     */
50
    public function __construct($courseCode = null, $resource_id = null, $user_id = null)
51
    {
52
        $this->items = [];
53
        $this->subdir = '';
54
        $this->manifestToString = '';
55
        parent::__construct($courseCode, $resource_id, $user_id);
56
    }
57
58
    /**
59
     * Opens a resource.
60
     *
61
     * @param int $id Database ID of the resource
62
     */
63
    public function open($id)
64
    {
65
        if ($this->debug > 0) {
66
            error_log('scorm::open() - In scorm::open method', 0);
67
        }
68
    }
69
70
    /**
71
     * Possible SCO status: see CAM doc 2.3.2.5.1: passed, completed, browsed, failed, not attempted, incomplete.
72
     * Prerequisites: see CAM doc 2.3.2.5.1 for pseudo-code.
73
     *
74
     * Parses an imsmanifest.xml file and puts everything into the $manifest array.
75
     *
76
     * @param	string	Path to the imsmanifest.xml file on the system.
77
     * If not defined, uses the base path of the course's scorm dir
78
     *
79
     * @return array Structured array representing the imsmanifest's contents
80
     */
81
    public function parse_manifest()
82
    {
83
        if ($this->manifestToString) {
84
            $xml = $this->manifestToString;
85
            // $this->manifest_encoding = api_detect_encoding_xml($xml);
86
            // This is the usual way for reading the encoding.
87
            // This method reads the encoding, it tries to be correct even in cases
88
            // of wrong or missing encoding declarations.
89
            $this->manifest_encoding = self::detect_manifest_encoding($xml);
0 ignored issues
show
Bug Best Practice introduced by
The method scorm::detect_manifest_encoding() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

89
            /** @scrutinizer ignore-call */ 
90
            $this->manifest_encoding = self::detect_manifest_encoding($xml);
Loading history...
90
91
            // UTF-8 is supported by DOMDocument class, this is for sure.
92
            $xml = api_utf8_encode_xml($xml, $this->manifest_encoding);
93
            $crawler = new Crawler();
94
            $crawler->addXmlContent($xml);
95
            $xmlErrors = libxml_get_errors();
96
97
            if (!empty($xmlErrors)) {
98
                if ($this->debug > 0) {
99
                    error_log('In scorm::parse_manifest() - Exception thrown when loading '.$file.' in DOMDocument');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $file seems to be never defined.
Loading history...
100
                }
101
                // Throw exception?
102
                return null;
103
            }
104
105
            if ($this->debug > 1) {
106
                error_log('Called  (encoding:'.$this->manifest_encoding.' - saved: '.$this->manifest_encoding.')', 0);
107
            }
108
109
            $root = $crawler->getNode(0);
110
            if ($root->hasAttributes()) {
111
                $attributes = $root->attributes;
112
                if (0 !== $attributes->length) {
113
                    foreach ($attributes as $attrib) {
114
                        // <manifest> element attributes
115
                        $this->manifest[$attrib->name] = $attrib->value;
116
                    }
117
                }
118
            }
119
            $this->manifest['name'] = $root->tagName;
120
            if ($root->hasChildNodes()) {
121
                $children = $root->childNodes;
122
                if (0 !== $children->length) {
123
                    foreach ($children as $child) {
124
                        // <manifest> element children (can be <metadata>, <organizations> or <resources> )
125
                        if (XML_ELEMENT_NODE == $child->nodeType) {
126
                            switch ($child->tagName) {
127
                                case 'metadata':
128
                                    // Parse items from inside the <metadata> element.
129
                                    $this->metadata = new scormMetadata('manifest', $child);
130
                                    break;
131
                                case 'organizations':
132
                                    // Contains the course structure - this element appears 1 and only 1 time in a package imsmanifest.
133
                                    // It contains at least one 'organization' sub-element.
134
                                    $orgs_attribs = $child->attributes;
135
                                    foreach ($orgs_attribs as $orgs_attrib) {
136
                                        // Attributes of the <organizations> element.
137
                                        if (XML_ATTRIBUTE_NODE == $orgs_attrib->nodeType) {
138
                                            $this->manifest['organizations'][$orgs_attrib->name] = $orgs_attrib->value;
139
                                        }
140
                                    }
141
                                    $orgs_nodes = $child->childNodes;
142
                                    $i = 0;
143
                                    $found_an_org = false;
144
                                    foreach ($orgs_nodes as $orgnode) {
145
                                        // <organization> elements - can contain <item>, <metadata> and <title>
146
                                        // Here we are at the 'organization' level. There might be several organization tags but
147
                                        // there is generally only one.
148
                                        // There are generally three children nodes we are looking for inside and organization:
149
                                        // -title
150
                                        // -item (may contain other item tags or may appear several times inside organization)
151
                                        // -metadata (relative to the organization)
152
                                        $found_an_org = false;
153
                                        switch ($orgnode->nodeType) {
154
                                            case XML_TEXT_NODE:
155
                                                // Ignore here.
156
                                                break;
157
                                            case XML_ATTRIBUTE_NODE:
158
                                                // Just in case there would be interesting attributes inside the organization tag.
159
                                                // There shouldn't as this is a node-level, not a data level.
160
                                                //$manifest['organizations'][$i][$orgnode->name] = $orgnode->value;
161
                                                //$found_an_org = true;
162
                                                break;
163
                                            case XML_ELEMENT_NODE:
164
                                                // <item>, <metadata> or <title> (or attributes)
165
                                                $organizations_attributes = $orgnode->attributes;
166
                                                foreach ($organizations_attributes as $orgs_attr) {
167
                                                    $this->organizations_att[$orgs_attr->name] = $orgs_attr->value;
168
                                                }
169
                                                $oOrganization = new scormOrganization(
170
                                                    'manifest',
171
                                                    $orgnode,
172
                                                    $this->manifest_encoding
173
                                                );
174
                                                if ('' != $oOrganization->identifier) {
175
                                                    $name = $oOrganization->get_name();
176
                                                    if (empty($name)) {
177
                                                        // If the org title is empty, use zip file name.
178
                                                        $myname = $this->zipname;
179
                                                        if (0 != $this->lastzipnameindex) {
180
                                                            $myname = $myname + $this->lastzipnameindex;
181
                                                            $this->lastzipnameindex++;
182
                                                        }
183
                                                        $oOrganization->set_name($this->zipname);
184
                                                    }
185
                                                    $this->organizations[$oOrganization->identifier] = $oOrganization;
186
                                                }
187
                                                break;
188
                                        }
189
                                    }
190
                                    break;
191
                                case 'resources':
192
                                    if ($child->hasAttributes()) {
193
                                        $resources_attribs = $child->attributes;
194
                                        foreach ($resources_attribs as $res_attr) {
195
                                            if (XML_ATTRIBUTE_NODE == $res_attr->type) {
196
                                                $this->manifest['resources'][$res_attr->name] = $res_attr->value;
197
                                            }
198
                                        }
199
                                    }
200
                                    if ($child->hasChildNodes()) {
201
                                        $resources_nodes = $child->childNodes;
202
                                        $i = 0;
203
                                        foreach ($resources_nodes as $res_node) {
204
                                            $oResource = new scormResource('manifest', $res_node);
205
                                            if ('' != $oResource->identifier) {
206
                                                $this->resources[$oResource->identifier] = $oResource;
207
                                                $i++;
208
                                            }
209
                                        }
210
                                    }
211
                                    // Contains links to physical resources.
212
                                    break;
213
                                case 'manifest':
214
                                    // Only for sub-manifests.
215
                                    break;
216
                            }
217
                        }
218
                    }
219
                }
220
            }
221
            // End parsing using PHP5 DOMXML methods.
222
        } else {
223
            if ($this->debug > 1) {
224
                error_log('Could not open/read file '.$file);
225
            }
226
            $this->set_error_msg("File $file could not be read");
227
228
            return null;
229
        }
230
231
        // @todo implement learnpath_fix_xerte_template
232
233
        $fixTemplate = api_get_configuration_value('learnpath_fix_xerte_template');
234
        $proxyPath = api_get_configuration_value('learnpath_proxy_url');
235
        /*if ($fixTemplate && !empty($proxyPath)) {
236
            // Check organisations:
237
            if (isset($this->manifest['organizations'])) {
238
                foreach ($this->manifest['organizations'] as $data) {
239
                    if (false !== strpos(strtolower($data), 'xerte')) {
240
                        // Check if template.xml exists:
241
                        $templatePath = str_replace('imsmanifest.xml', 'template.xml', $file);
242
                        if (file_exists($templatePath) && is_file($templatePath)) {
243
                            $templateContent = file_get_contents($templatePath);
244
                            $find = [
245
                                'href="www.',
246
                                'href="https://',
247
                                'href="http://',
248
                                'url="www.',
249
                                'pdfs/download.php?',
250
                            ];
251
252
                            $replace = [
253
                                'href="http://www.',
254
                                'target = "_blank" href="'.$proxyPath.'?type=link&src=https://',
255
                                'target = "_blank" href="'.$proxyPath.'?type=link&src=http://',
256
                                'url="http://www.',
257
                                'pdfs/download.php&',
258
                            ];
259
                            $templateContent = str_replace($find, $replace, $templateContent);
260
                            file_put_contents($templatePath, $templateContent);
261
                        }
262
263
                        // Fix link generation:
264
                        $linkPath = str_replace('imsmanifest.xml', 'models_html5/links.html', $file);
265
                        if (file_exists($linkPath) && is_file($linkPath)) {
266
                            $linkContent = file_get_contents($linkPath);
267
                            $find = [
268
                                ':this.getAttribute("url")',
269
                            ];
270
                            $replace = [
271
                                ':"'.$proxyPath.'?type=link&src=" + this.getAttribute("url")',
272
                            ];
273
                            $linkContent = str_replace($find, $replace, $linkContent);
274
                            file_put_contents($linkPath, $linkContent);
275
                        }
276
277
                        // Fix iframe generation
278
                        $framePath = str_replace('imsmanifest.xml', 'models_html5/embedDiv.html', $file);
279
280
                        if (file_exists($framePath) && is_file($framePath)) {
281
                            $content = file_get_contents($framePath);
282
                            $find = [
283
                                '$iFrameHolder.html(iFrameTag);',
284
                            ];
285
                            $replace = [
286
                                'iFrameTag = \'<a target ="_blank" href="'.$proxyPath.'?type=link&src=\'+ pageSrc + \'">Open website. <img width="16px" src="'.Display::returnIconPath('link-external.png').'"></a>\'; $iFrameHolder.html(iFrameTag); ',
287
                            ];
288
                            $content = str_replace($find, $replace, $content);
289
                            file_put_contents($framePath, $content);
290
                        }
291
292
                        // Fix new window generation
293
                        $newWindowPath = str_replace('imsmanifest.xml', 'models_html5/newWindow.html', $file);
294
295
                        if (file_exists($newWindowPath) && is_file($newWindowPath)) {
296
                            $content = file_get_contents($newWindowPath);
297
                            $find = [
298
                                'var src = x_currentPageXML',
299
                            ];
300
                            $replace = [
301
                                'var src = "'.$proxyPath.'?type=link&src=" + x_currentPageXML',
302
                            ];
303
                            $content = str_replace($find, $replace, $content);
304
                            file_put_contents($newWindowPath, $content);
305
                        }
306
                    }
307
                }
308
            }
309
        }*/
310
311
        // TODO: Close the DOM handler.
312
        return $this->manifest;
313
    }
314
315
    /**
316
     * Import the scorm object (as a result from the parse_manifest function) into the database structure.
317
     *
318
     * @param string $courseCode
319
     * @param int    $userMaxScore
320
     * @param int    $sessionId
321
     *
322
     * @return bool Returns -1 on error
323
     */
324
    public function import_manifest($courseCode, $userMaxScore = 1, $sessionId = 0)
325
    {
326
        if ($this->debug > 0) {
327
            error_log('Entered import_manifest('.$courseCode.')', 0);
328
        }
329
        $courseInfo = api_get_course_info($courseCode);
330
        $courseId = $courseInfo['real_id'];
331
332
        // Get table names.
333
        $new_lp = Database::get_course_table(TABLE_LP_MAIN);
334
        $new_lp_item = Database::get_course_table(TABLE_LP_ITEM);
335
        $userMaxScore = (int) $userMaxScore;
336
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
337
338
        $repo = Container::getLpRepository();
339
        $em = Database::getManager();
340
        $lp = null;
341
        foreach ($this->organizations as $id => $dummy) {
342
            $oOrganization = &$this->organizations[$id];
343
            // Prepare and execute insert queries:
344
            // -for learnpath
345
            // -for items
346
            // -for views?
347
            $get_max = "SELECT MAX(display_order) FROM $new_lp WHERE c_id = $courseId ";
348
            $res_max = Database::query($get_max);
349
            $dsp = 1;
350
            if (Database::num_rows($res_max) > 0) {
351
                $row = Database::fetch_array($res_max);
352
                $dsp = $row[0] + 1;
353
            }
354
355
            $courseEntity = api_get_course_entity($courseInfo['real_id']);
356
            $myname = api_utf8_decode($oOrganization->get_name());
357
            $lp = new CLp();
358
            $lp
359
                ->setCId($courseId)
360
                ->setLpType(CLp::SCORM_TYPE)
361
                ->setName($myname)
362
                ->setRef($oOrganization->get_ref())
363
                ->setPath($this->subdir)
364
                ->setDefaultEncoding($this->manifest_encoding)
365
                ->setJsLib('scorm_api.php')
366
                ->setDisplayOrder($dsp)
367
                ->setUseMaxScore($userMaxScore)
368
                ->setAsset($this->asset)
369
                ->setSessionId($sessionId)
370
                ->setParent($courseEntity)
371
                ->addCourseLink($courseEntity, api_get_session_entity())
372
            ;
373
374
            $repo->create($lp);
375
376
            $lp_id = $lp->getIid();
377
378
            // Now insert all elements from inside that learning path.
379
            // Make sure we also get the href and sco/asset from the resources.
380
            $list = $oOrganization->get_flat_items_list();
381
            $parents_stack = [0];
382
            $parent = 0;
383
            $previous = 0;
384
            $level = 0;
385
            foreach ($list as $item) {
386
                if ($item['level'] > $level) {
387
                    // Push something into the parents array.
388
                    array_push($parents_stack, $previous);
389
                    $parent = $previous;
390
                } elseif ($item['level'] < $level) {
391
                    $diff = $level - $item['level'];
392
                    // Pop something out of the parents array.
393
                    for ($j = 1; $j <= $diff; $j++) {
394
                        $outdated_parent = array_pop($parents_stack);
395
                    }
396
                    $parent = array_pop($parents_stack); // Just save that value, then add it back.
397
                    array_push($parents_stack, $parent);
398
                }
399
                $path = '';
400
                $type = 'dir';
401
                if (isset($this->resources[$item['identifierref']])) {
402
                    $oRes = &$this->resources[$item['identifierref']];
403
                    $path = @$oRes->get_path();
404
                    if (!empty($path)) {
405
                        $temptype = $oRes->get_scorm_type();
406
                        if (!empty($temptype)) {
407
                            $type = $temptype;
408
                        }
409
                    }
410
                }
411
                $level = $item['level'];
412
                $title = $item['title'];
413
                $title = api_utf8_decode($title);
414
                $max_score = (int) $item['max_score'];
415
                if (0 === $max_score) {
416
                    // If max score is not set The use_max_score parameter
417
                    // is check in order to use 100 (chamilo style) or '' (strict scorm)
418
                    $max_score = 'NULL';
419
                    if ($userMaxScore) {
420
                        $max_score = 100;
421
                    }
422
                } else {
423
                    // Otherwise save the max score.
424
                    $max_score = "'$max_score'";
425
                }
426
427
                if (empty($title)) {
428
                    $title = get_lang('Untitled');
429
                }
430
431
                $lpItem = new CLpItem();
432
                $lpItem
433
                    ->setTitle($title)
434
                    ->setCId($courseId)
435
                    ->setItemType($type)
436
                    ->setRef($item['identifier'])
437
                    ->setPath($path)
438
                    ->setMinScore(0)
439
                    ->setMaxScore($max_score)
440
                    ->setParentItemId($parent)
441
                    ->setPreviousItemId($previous)
442
                    ->setNextItemId(0)
443
                    ->setPrerequisite($item['prerequisites'])
444
                    ->setDisplayOrder($item['rel_order'])
445
                    ->setLaunchData($item['datafromlms'])
446
                    ->setParameters($item['parameters'])
447
                    ->setLp($lp)
448
                ;
449
450
                if (!empty($item['masteryscore'])) {
451
                    $lpItem->setMasteryScore($item['masteryscore']);
452
                }
453
454
                if (!empty($item['maxtimeallowed'])) {
455
                    $lpItem->setMaxTimeAllowed($item['maxtimeallowed']);
456
                }
457
                $em->persist($lpItem);
458
                $em->flush();
459
460
                $item_id = $lpItem->getIid();
461
                if ($item_id) {
462
                    // Now update previous item to change next_item_id.
463
                    $upd = "UPDATE $new_lp_item SET next_item_id = $item_id
464
                            WHERE iid = $previous";
465
                    Database::query($upd);
466
                    // Update previous item id.
467
                    $previous = $item_id;
468
                }
469
470
                // Code for indexing, now only index specific fields like terms and the title.
471
                if (!empty($_POST['index_document'])) {
472
                    require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
473
                    $di = new ChamiloIndexer();
474
                    isset($_POST['language']) ? $lang = Database::escape_string($_POST['language']) : $lang = 'english';
475
                    $di->connectDb(null, null, $lang);
476
                    $ic_slide = new IndexableChunk();
477
                    $ic_slide->addValue('title', $title);
478
                    $specific_fields = get_specific_field_list();
479
                    $all_specific_terms = '';
480
                    foreach ($specific_fields as $specific_field) {
481
                        if (isset($_REQUEST[$specific_field['code']])) {
482
                            $sterms = trim($_REQUEST[$specific_field['code']]);
483
                            $all_specific_terms .= ' '.$sterms;
484
                            if (!empty($sterms)) {
485
                                $sterms = explode(',', $sterms);
486
                                foreach ($sterms as $sterm) {
487
                                    $ic_slide->addTerm(trim($sterm), $specific_field['code']);
488
                                }
489
                            }
490
                        }
491
                    }
492
                    $body_to_index = $all_specific_terms.' '.$title;
493
                    $ic_slide->addValue("content", $body_to_index);
494
                    // TODO: Add a comment to say terms separated by commas.
495
                    $courseid = api_get_course_id();
496
                    $ic_slide->addCourseId($courseid);
497
                    $ic_slide->addToolId(TOOL_LEARNPATH);
498
                    // TODO: Unify with other lp types.
499
                    $xapian_data = [
500
                        SE_COURSE_ID => $courseid,
501
                        SE_TOOL_ID => TOOL_LEARNPATH,
502
                        SE_DATA => ['lp_id' => $lp_id, 'lp_item' => $previous, 'document_id' => ''],
503
                        SE_USER => api_get_user_id(),
504
                    ];
505
                    $ic_slide->xapian_data = serialize($xapian_data);
506
                    $di->addChunk($ic_slide);
507
                    // Index and return search engine document id.
508
                    $did = $di->index();
509
                    if ($did) {
510
                        // Save it to db.
511
                        $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
512
                        $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, ref_id_second_level, search_did)
513
                                VALUES (NULL , \'%s\', \'%s\', %s, %s, %s)';
514
                        $sql = sprintf($sql, $tbl_se_ref, $courseCode, TOOL_LEARNPATH, $lp_id, $previous, $did);
515
                        Database::query($sql);
516
                    }
517
                }
518
            }
519
        }
520
521
        return $lp;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $lp also could return the type Chamilo\CourseBundle\Entity\CLp which is incompatible with the documented return type boolean.
Loading history...
522
    }
523
524
    /**
525
     * Intermediate to import_package only to allow import from local zip files.
526
     *
527
     * @param  string    Path to the zip file, from the sys root
528
     * @param  string    Current path (optional)
529
     *
530
     * @return string Absolute path to the imsmanifest.xml file or empty string on error
531
     */
532
    public function import_local_package($file_path, $currentDir = '')
533
    {
534
        // TODO: Prepare info as given by the $_FILES[''] vector.
535
        $fileInfo = [];
536
        $fileInfo['tmp_name'] = $file_path;
537
        $fileInfo['name'] = basename($file_path);
538
        // Call the normal import_package function.
539
        return $this->import_package($fileInfo, $currentDir);
540
    }
541
542
    /**
543
     * Imports a zip file into the Chamilo structure.
544
     *
545
     * @param string    $zipFileInfo       Zip file info as given by $_FILES['userFile']
546
     * @param string    $currentDir
547
     * @param array     $courseInfo
548
     * @param bool      $updateDirContents
549
     * @param learnpath $lpToCheck
550
     * @param bool      $allowHtaccess
551
     *
552
     * @return string $current_dir Absolute path to the imsmanifest.xml file or empty string on error
553
     */
554
    public function import_package(
555
        $zipFileInfo,
556
        $currentDir = '',
557
        $courseInfo = [],
558
        $updateDirContents = false,
559
        $lpToCheck = null,
560
        $allowHtaccess = false
561
    ) {
562
        if ($this->debug) {
563
            error_log(
564
                'In scorm::import_package('.print_r($zipFileInfo, true).',"'.$currentDir.'") method'
565
            );
566
        }
567
568
        $courseInfo = empty($courseInfo) ? api_get_course_info() : $courseInfo;
569
        $maxFilledSpace = DocumentManager::get_course_quota($courseInfo['code']);
570
        $zipFilePath = $zipFileInfo['tmp_name'];
571
        $zipFileName = $zipFileInfo['name'];
572
573
        if ($this->debug > 1) {
574
            error_log(
575
                'import_package() - zip file path = '.$zipFilePath.', zip file name = '.$zipFileName,
576
                0
577
            );
578
        }
579
580
        //$courseRelDir = api_get_course_path($courseInfo['code']).'/scorm'; // scorm dir web path starting from /courses
581
        //$courseSysDir = api_get_path(SYS_COURSE_PATH).$courseRelDir; // Absolute system path for this course.
582
        $currentDir = api_replace_dangerous_char(trim($currentDir)); // Current dir we are in, inside scorm/
583
584
        if ($this->debug > 1) {
585
            error_log('import_package() - current_dir = '.$currentDir, 0);
586
        }
587
588
        // Get name of the zip file without the extension.
589
        $fileInfo = pathinfo($zipFileName);
590
        $filename = $fileInfo['basename'];
591
        $extension = $fileInfo['extension'];
592
        $fileBaseName = str_replace('.'.$extension, '', $filename); // Filename without its extension.
593
        $this->zipname = $fileBaseName; // Save for later in case we don't have a title.
594
        $newDir = api_replace_dangerous_char(trim($fileBaseName));
595
        $this->subdir = $newDir;
596
        if ($this->debug > 1) {
597
            error_log('Received zip file name: '.$zipFilePath);
598
            error_log("subdir is first set to : ".$this->subdir);
599
            error_log("base file name is : ".$fileBaseName);
600
        }
601
602
        $zipAdapter = new ZipArchiveAdapter($zipFilePath);
603
        $filesystem = new \League\Flysystem\Filesystem($zipAdapter);
604
        $zipContentArray = $filesystem->listContents();
605
606
        $packageType = '';
607
        $manifestList = [];
608
        // The following loop should be stopped as soon as we found the right imsmanifest.xml (how to recognize it?).
609
        $realFileSize = 0;
610
        foreach ($zipContentArray as $thisContent) {
611
            $fileName = $thisContent['path'];
612
            if (preg_match('~.(php.*|phtml)$~i', $fileName)) {
613
                $file = $fileName;
614
                $this->set_error_msg("File $file contains a PHP script");
615
            } elseif (stristr($fileName, 'imsmanifest.xml')) {
616
                if ($fileName == basename($fileName)) {
617
                } else {
618
                    if ($this->debug > 2) {
619
                        error_log("subdir is now ".$this->subdir);
620
                    }
621
                }
622
                $packageType = 'scorm';
623
                $manifestList[] = $fileName;
624
            }
625
626
            if (isset($thisContent['size'])) {
627
                $realFileSize += $thisContent['size'];
628
            }
629
        }
630
631
        // Now get the shortest path (basically, the imsmanifest that is the closest to the root).
632
        $shortestPath = $manifestList[0];
633
        $slashCount = substr_count($shortestPath, '/');
634
        foreach ($manifestList as $manifestPath) {
635
            $tmpSlashCount = substr_count($manifestPath, '/');
636
            if ($tmpSlashCount < $slashCount) {
637
                $shortestPath = $manifestPath;
638
                $slashCount = $tmpSlashCount;
639
            }
640
        }
641
642
        $this->subdir .= '/'.dirname($shortestPath); // Do not concatenate because already done above.
643
        $this->manifestToString = $filesystem->read($shortestPath);
644
645
        if ($this->debug) {
646
            error_log("Package type is now: '$packageType'");
647
        }
648
        if ('' === $packageType) {
649
            Display::addFlash(
650
                Display::return_message(get_lang('This is not a valid SCORM ZIP file !'))
651
            );
652
653
            return false;
654
        }
655
656
        // Todo check filesize
657
        /*if (!enough_size($realFileSize, $courseSysDir, $maxFilledSpace)) {
658
            if ($this->debug > 1) {
659
                error_log('Not enough space to store package');
660
            }
661
            Display::addFlash(
662
                Display::return_message(
663
                    get_lang(
664
                        'The upload has failed. Either you have exceeded your maximum quota, or there is not enough disk space.'
665
                    )
666
                )
667
            );
668
669
            return false;
670
        }*/
671
672
        /*if ($updateDirContents && $lpToCheck) {
673
            $originalPath = str_replace('/.', '', $lpToCheck->path);
674
            if ($originalPath != $newDir) {
675
                Display::addFlash(Display::return_message(get_lang('The file to upload is not valid.')));
676
677
                return false;
678
            }
679
        }
680
681
        // It happens on Linux that $newDir sometimes doesn't start with '/'
682
        if ('/' !== $newDir[0]) {
683
            $newDir = '/'.$newDir;
684
        }
685
686
        if ('/' === $newDir[strlen($newDir) - 1]) {
687
            $newDir = substr($newDir, 0, -1);
688
        }*/
689
690
691
        /* Uncompressing phase */
692
        /*
693
            We need to process each individual file in the zip archive to
694
            - add it to the database
695
            - parse & change relative html links
696
            - make sure the filenames are secure (filter funny characters or php extensions)
697
        */
698
699
700
        // 1. Upload zip file
701
        $request = Container::getRequest();
702
        $uploadFile = null;
703
        if ($request->files->has('user_file')) {
704
            $uploadFile = $request->files->get('user_file');
705
        }
706
        $em = Database::getManager();
707
        $asset = new Asset();
708
        $asset
709
            ->setCategory(Asset::SCORM)
710
            ->setTitle($zipFileName)
711
            ->setFile($uploadFile)
712
            ->setCompressed(true)
713
        ;
714
        $em->persist($asset);
715
        $em->flush();
716
717
        // 2. Unzip file
718
        $repo = Container::getAssetRepository();
719
        $repo->unZipFile($asset, $zipAdapter);
720
        $this->asset = $asset;
721
722
        /*if (is_dir($courseSysDir.$newDir) ||
723
            @mkdir(
724
                $courseSysDir.$newDir,
725
                api_get_permissions_for_new_directories()
726
            )
727
        ) {
728
            // PHP method - slower...
729
            if ($this->debug >= 1) {
730
                error_log('Changing dir to '.$courseSysDir.$newDir);
731
            }
732
733
            chdir($courseSysDir.$newDir);
734
735
            $callBack = 'clean_up_files_in_zip';
736
            if ($allowHtaccess) {
737
                $callBack = 'cleanZipFilesAllowHtaccess';
738
            }
739
740
            if (api_get_configuration_value('skip_scorm_package_clean_up')) {
741
                $callBack = 'cleanZipFilesNoRename';
742
            }
743
744
            $zipFile->extract(
745
                PCLZIP_CB_PRE_EXTRACT,
746
                $callBack
747
            );
748
749
            if (!empty($newDir)) {
750
                $newDir = $newDir.'/';
751
            }
752
            api_chmod_R($courseSysDir.$newDir, api_get_permissions_for_new_directories());
753
        } else {
754
            return false;
755
        }*/
756
757
        //return $courseSysDir.$newDir.$manifest;
758
759
        return $asset;
760
    }
761
762
    /**
763
     * Exports the current SCORM object's files as a zip.
764
     * Excerpts taken from learnpath_functions.inc.php::exportpath().
765
     *
766
     * @param int    Learnpath ID (optional, taken from object context if not defined)
767
     *
768
     * @return bool
769
     */
770
    public function export_zip($lp_id = null)
771
    {
772
        if ($this->debug > 0) {
773
            error_log('In scorm::export_zip method('.$lp_id.')');
774
        }
775
        if (empty($lp_id)) {
776
            if (!is_object($this)) {
777
                return false;
778
            } else {
779
                $id = $this->get_id();
780
                if (empty($id)) {
781
                    return false;
782
                } else {
783
                    $lp_id = $this->get_id();
784
                }
785
            }
786
        }
787
        //zip everything that is in the corresponding scorm dir
788
        //write the zip file somewhere (might be too big to return)
789
790
        $_course = api_get_course_info();
791
        $tbl_lp = Database::get_course_table(TABLE_LP_MAIN);
792
        $sql = "SELECT * FROM $tbl_lp WHERE iid = $lp_id";
793
        $result = Database::query($sql);
794
        $row = Database::fetch_array($result);
795
        $LPname = $row['path'];
796
        $list = explode('/', $LPname);
797
        $LPnamesafe = $list[0];
798
        $zipfoldername = api_get_path(SYS_COURSE_PATH).$_course['directory'].'/temp/'.$LPnamesafe;
0 ignored issues
show
Bug introduced by
The constant SYS_COURSE_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
799
        $scormfoldername = api_get_path(SYS_COURSE_PATH).$_course['directory'].'/scorm/'.$LPnamesafe;
800
        $zipfilename = $zipfoldername.'/'.$LPnamesafe.'.zip';
801
802
        // Get a temporary dir for creating the zip file.
803
        //error_log('cleaning dir '.$zipfoldername, 0);
804
        my_delete($zipfoldername); // Make sure the temp dir is cleared.
805
        mkdir($zipfoldername, api_get_permissions_for_new_directories());
806
807
        // Create zipfile of given directory.
808
        $zip_folder = new PclZip($zipfilename);
809
        $zip_folder->create($scormfoldername.'/', PCLZIP_OPT_REMOVE_PATH, $scormfoldername.'/');
0 ignored issues
show
Bug introduced by
The constant PCLZIP_OPT_REMOVE_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
810
811
        //This file sending implies removing the default mime-type from php.ini
812
        //DocumentManager::file_send_for_download($zipfilename, true, $LPnamesafe.'.zip');
813
        DocumentManager::file_send_for_download($zipfilename, true);
814
815
        // Delete the temporary zip file and directory in fileManage.lib.php
816
        my_delete($zipfilename);
817
        my_delete($zipfoldername);
818
819
        return true;
820
    }
821
822
    /**
823
     * Gets a resource's path if available, otherwise return empty string.
824
     *
825
     * @param	string	Resource ID as used in resource array
826
     *
827
     * @return string The resource's path as declared in imsmanifest.xml
828
     */
829
    public function get_res_path($id)
830
    {
831
        if ($this->debug > 0) {
832
            error_log('In scorm::get_res_path('.$id.') method');
833
        }
834
        $path = '';
835
        if (isset($this->resources[$id])) {
836
            $oRes = &$this->resources[$id];
837
            $path = @$oRes->get_path();
838
        }
839
840
        return $path;
841
    }
842
843
    /**
844
     * Gets a resource's type if available, otherwise return empty string.
845
     *
846
     * @param  string    Resource ID as used in resource array
847
     *
848
     * @return string The resource's type as declared in imsmanifest.xml
849
     */
850
    public function get_res_type($id)
851
    {
852
        if ($this->debug > 0) {
853
            error_log('In scorm::get_res_type('.$id.') method');
854
        }
855
        $type = '';
856
        if (isset($this->resources[$id])) {
857
            $oRes = &$this->resources[$id];
858
            $temptype = $oRes->get_scorm_type();
859
            if (!empty($temptype)) {
860
                $type = $temptype;
861
            }
862
        }
863
864
        return $type;
865
    }
866
867
    /**
868
     * Gets the default organisation's title.
869
     *
870
     * @return string The organization's title
871
     */
872
    public function get_title()
873
    {
874
        if ($this->debug > 0) {
875
            error_log('In scorm::get_title() method');
876
        }
877
        $title = '';
878
        if (isset($this->manifest['organizations']['default'])) {
879
            $title = $this->organizations[$this->manifest['organizations']['default']]->get_name();
880
        } elseif (1 == count($this->organizations)) {
881
            // This will only get one title but so we don't need to know the index.
882
            foreach ($this->organizations as $id => $value) {
883
                $title = $this->organizations[$id]->get_name();
884
                break;
885
            }
886
        }
887
888
        return $title;
889
    }
890
891
    /**
892
     * // TODO @TODO Implement this function to restore items data from an imsmanifest,
893
     * updating the existing table... This will prove very useful in case initial data
894
     * from imsmanifest were not imported well enough.
895
     *
896
     * @param string $courseCode
897
     * @param int	LP ID (in database)
898
     * @param string	Manifest file path (optional if lp_id defined)
899
     *
900
     * @return int New LP ID or false on failure
901
     *             TODO @TODO Implement imsmanifest_path parameter
902
     */
903
    public function reimport_manifest($courseCode, $lp_id = null, $imsmanifest_path = '')
904
    {
905
        if ($this->debug > 0) {
906
            error_log('In scorm::reimport_manifest() method', 0);
907
        }
908
909
        $courseInfo = api_get_course_info($courseCode);
910
        if (empty($courseInfo)) {
911
            $this->error = 'Course code does not exist in database';
912
913
            return false;
914
        }
915
916
        $this->cc = $courseInfo['code'];
917
918
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
919
        $lp_id = intval($lp_id);
920
        $sql = "SELECT * FROM $lp_table WHERE iid = $lp_id";
921
        if ($this->debug > 2) {
922
            error_log('scorm::reimport_manifest() '.__LINE__.' - Querying lp: '.$sql);
923
        }
924
        $res = Database::query($sql);
925
        if (Database::num_rows($res) > 0) {
926
            $this->lp_id = $lp_id;
927
            $row = Database::fetch_array($res);
928
            $this->type = $row['lp_type'];
929
            $this->name = stripslashes($row['name']);
930
            $this->encoding = $row['default_encoding'];
931
            $this->proximity = $row['content_local'];
932
            $this->maker = $row['content_maker'];
933
            $this->prevent_reinit = $row['prevent_reinit'];
934
            $this->license = $row['content_license'];
935
            $this->scorm_debug = $row['debug'];
936
            $this->js_lib = $row['js_lib'];
937
            $this->path = $row['path'];
938
            if (2 == $this->type) {
939
                if (1 == $row['force_commit']) {
940
                    $this->force_commit = true;
941
                }
942
            }
943
            $this->mode = $row['default_view_mod'];
944
            $this->subdir = $row['path'];
945
        }
946
        // Parse the manifest (it is already in this lp's details).
947
        $manifest_file = api_get_path(SYS_COURSE_PATH).$courseInfo['directory'].'/scorm/'.$this->subdir.'/imsmanifest.xml';
0 ignored issues
show
Bug introduced by
The constant SYS_COURSE_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
948
        if ('' == $this->subdir) {
949
            $manifest_file = api_get_path(SYS_COURSE_PATH).$courseInfo['directory'].'/scorm/imsmanifest.xml';
950
        }
951
        echo $manifest_file;
952
        if (is_file($manifest_file) && is_readable($manifest_file)) {
953
            // Re-parse the manifest file.
954
            if ($this->debug > 1) {
955
                error_log('In scorm::reimport_manifest() - Parsing manifest '.$manifest_file);
956
            }
957
            $manifest = $this->parse_manifest($manifest_file);
958
            // Import new LP in DB (ignore the current one).
959
            if ($this->debug > 1) {
960
                error_log('In scorm::reimport_manifest() - Importing manifest '.$manifest_file);
961
            }
962
            $this->import_manifest($this->cc);
963
        } else {
964
            if ($this->debug > 0) {
965
                error_log('In scorm::reimport_manifest() - Could not find manifest file at '.$manifest_file);
966
            }
967
        }
968
969
        return false;
970
    }
971
972
    /**
973
     * Detects the encoding of a given manifest (a xml-text).
974
     * It is possible the encoding of the manifest to be wrongly declared or
975
     * not to be declared at all. The proposed method tries to resolve these problems.
976
     *
977
     * @param string $xml the input xml-text
978
     *
979
     * @return string the detected value of the input xml
980
     */
981
    private function detect_manifest_encoding(&$xml)
982
    {
983
        if (api_is_valid_utf8($xml)) {
984
            return 'UTF-8';
985
        }
986
987
        if (preg_match(_PCRE_XML_ENCODING, $xml, $matches)) {
988
            $declared_encoding = api_refine_encoding_id($matches[1]);
989
        } else {
990
            $declared_encoding = '';
991
        }
992
993
        if (!empty($declared_encoding) && !api_is_utf8($declared_encoding)) {
994
            return $declared_encoding;
995
        }
996
997
        $test_string = '';
998
        if (preg_match_all('/<langstring[^>]*>(.*)<\/langstring>/m', $xml, $matches)) {
999
            $test_string = implode("\n", $matches[1]);
1000
            unset($matches);
1001
        }
1002
        if (preg_match_all('/<title[^>]*>(.*)<\/title>/m', $xml, $matches)) {
1003
            $test_string .= "\n".implode("\n", $matches[1]);
1004
            unset($matches);
1005
        }
1006
        if (empty($test_string)) {
1007
            $test_string = $xml;
1008
        }
1009
1010
        return api_detect_encoding($test_string);
1011
    }
1012
}
1013