Passed
Push — master ( 92a822...888b5e )
by Yannick
19:42 queued 09:01
created

ScormExport   F

Complexity

Total Complexity 135

Size/Duplication

Total Lines 1094
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 690
dl 0
loc 1094
rs 1.91
c 0
b 0
f 0
wmc 135

2 Methods

Rating   Name   Duplication   Size   Complexity  
F export() 0 974 114
D exportToPdf() 0 96 21

How to fix   Complexity   

Complex Class

Complex classes like ScormExport often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ScormExport, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Framework\Container;
6
use Chamilo\CourseBundle\Entity\CDocument;
7
use Chamilo\CourseBundle\Entity\CLp;
8
use Chamilo\CourseBundle\Entity\CLpItem;
9
use Symfony\Component\Filesystem\Filesystem;
10
11
class ScormExport
12
{
13
    /**
14
     * // TODO: The output encoding should be equal to the system encoding.
15
     *
16
     * Exports the learning path as a SCORM package. This is the main function that
17
     * gathers the content, transforms it, writes the imsmanifest.xml file, zips the
18
     * whole thing and returns the zip.
19
     *
20
     * This method needs to be called in PHP5, as it will fail with non-adequate
21
     * XML package (like the ones for PHP4), and it is *not* a static method, so
22
     * you need to call it on a learnpath object.
23
     *
24
     * @TODO The method might be redefined later on in the scorm class itself to avoid
25
     * creating a SCORM structure if there is one already. However, if the initial SCORM
26
     * path has been modified, it should use the generic method here below.
27
     *
28
     * @return string Returns the zip package string, or null if error
29
     */
30
    public static function export(learnpath $lp)
31
    {
32
        // @todo fix export
33
        api_set_more_memory_and_time_limits();
34
35
        $_course = api_get_course_info();
36
        $course_id = $_course['real_id'];
37
        // Create the zip handler (this will remain available throughout the method).
38
        $archivePath = api_get_path(SYS_ARCHIVE_PATH);
39
        $sys_course_path = api_get_path(SYS_COURSE_PATH);
40
        $temp_dir_short = uniqid('scorm_export', true);
41
        $temp_zip_dir = $archivePath.'/'.$temp_dir_short;
42
        $temp_zip_file = $temp_zip_dir.'/'.md5(time()).'.zip';
43
        $zip_folder = new PclZip($temp_zip_file);
44
        $current_course_path = api_get_path(SYS_COURSE_PATH).api_get_course_path();
45
        $root_path = $main_path = api_get_path(SYS_PATH);
46
        $files_cleanup = [];
47
48
        // Place to temporarily stash the zip file.
49
        // create the temp dir if it doesn't exist
50
        // or do a cleanup before creating the zip file.
51
        if (!is_dir($temp_zip_dir)) {
52
            mkdir($temp_zip_dir, api_get_permissions_for_new_directories());
53
        } else {
54
            // Cleanup: Check the temp dir for old files and delete them.
55
            $handle = opendir($temp_zip_dir);
56
            while (false !== ($file = readdir($handle))) {
57
                if ('.' != $file && '..' != $file) {
58
                    unlink("$temp_zip_dir/$file");
59
                }
60
            }
61
            closedir($handle);
62
        }
63
        $zip_files = $zip_files_abs = $zip_files_dist = [];
64
        if (is_dir($current_course_path.'/scorm/'.$lp->path) &&
65
            is_file($current_course_path.'/scorm/'.$lp->path.'/imsmanifest.xml')
66
        ) {
67
            // Remove the possible . at the end of the path.
68
            $dest_path_to_lp = '.' == substr($lp->path, -1) ? substr($lp->path, 0, -1) : $lp->path;
69
            $dest_path_to_scorm_folder = str_replace('//', '/', $temp_zip_dir.'/scorm/'.$dest_path_to_lp);
70
            mkdir(
71
                $dest_path_to_scorm_folder,
72
                api_get_permissions_for_new_directories(),
73
                true
74
            );
75
            copyr(
76
                $current_course_path.'/scorm/'.$lp->path,
77
                $dest_path_to_scorm_folder,
78
                ['imsmanifest'],
79
                $zip_files
80
            );
81
        }
82
83
        // Build a dummy imsmanifest structure.
84
        // Do not add to the zip yet (we still need it).
85
        // This structure is developed following regulations for SCORM 1.2 packaging in the SCORM 1.2 Content
86
        // Aggregation Model official document, section "2.3 Content Packaging".
87
        // We are going to build a UTF-8 encoded manifest.
88
        // Later we will recode it to the desired (and supported) encoding.
89
        $xmldoc = new DOMDocument('1.0');
90
        $root = $xmldoc->createElement('manifest');
91
        $root->setAttribute('identifier', 'SingleCourseManifest');
92
        $root->setAttribute('version', '1.1');
93
        $root->setAttribute('xmlns', 'http://www.imsproject.org/xsd/imscp_rootv1p1p2');
94
        $root->setAttribute('xmlns:adlcp', 'http://www.adlnet.org/xsd/adlcp_rootv1p2');
95
        $root->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
96
        $root->setAttribute(
97
            'xsi:schemaLocation',
98
            'http://www.imsproject.org/xsd/imscp_rootv1p1p2 imscp_rootv1p1p2.xsd http://www.imsglobal.org/xsd/imsmd_rootv1p2p1 imsmd_rootv1p2p1.xsd http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd'
99
        );
100
        // Build mandatory sub-root container elements.
101
        $metadata = $xmldoc->createElement('metadata');
102
        $md_schema = $xmldoc->createElement('schema', 'ADL SCORM');
103
        $metadata->appendChild($md_schema);
104
        $md_schemaversion = $xmldoc->createElement('schemaversion', '1.2');
105
        $metadata->appendChild($md_schemaversion);
106
        $root->appendChild($metadata);
107
108
        $organizations = $xmldoc->createElement('organizations');
109
        $resources = $xmldoc->createElement('resources');
110
111
        // Build the only organization we will use in building our learnpaths.
112
        $organizations->setAttribute('default', 'chamilo_scorm_export');
113
        $organization = $xmldoc->createElement('organization');
114
        $organization->setAttribute('identifier', 'chamilo_scorm_export');
115
        // To set the title of the SCORM entity (=organization), we take the name given
116
        // in Chamilo and convert it to HTML entities using the Chamilo charset (not the
117
        // learning path charset) as it is the encoding that defines how it is stored
118
        // in the database. Then we convert it to HTML entities again as the "&" character
119
        // alone is not authorized in XML (must be &amp;).
120
        // The title is then decoded twice when extracting (see scorm::parse_manifest).
121
        $org_title = $xmldoc->createElement('title', api_utf8_encode($lp->get_name()));
122
        $organization->appendChild($org_title);
123
        $folder_name = 'document';
124
125
        // Removes the learning_path/scorm_folder path when exporting see #4841
126
        $path_to_remove = '';
127
        $path_to_replace = '';
128
        $result = $lp->generate_lp_folder($_course);
129
        if (isset($result['dir']) && strpos($result['dir'], 'learning_path')) {
130
            $path_to_remove = 'document'.$result['dir'];
131
            $path_to_replace = $folder_name.'/';
132
        }
133
134
        // Fixes chamilo scorm exports
135
        if ('chamilo_scorm_export' === $lp->ref) {
136
            $path_to_remove = 'scorm/'.$lp->path.'/document/';
137
        }
138
139
        // For each element, add it to the imsmanifest structure, then add it to the zip.
140
        $link_updates = [];
141
        $links_to_create = [];
142
        foreach ($lp->ordered_items as $index => $itemId) {
143
            /** @var learnpathItem $item */
144
            $item = $lp->items[$itemId];
145
            if (!in_array($item->type, [TOOL_QUIZ, TOOL_FORUM, TOOL_THREAD, TOOL_LINK, TOOL_STUDENTPUBLICATION])) {
146
                // Get included documents from this item.
147
                if ('sco' === $item->type) {
148
                    $inc_docs = $item->get_resources_from_source(
149
                        null,
150
                        $current_course_path.'/scorm/'.$lp->path.'/'.$item->get_path()
151
                    );
152
                } else {
153
                    $inc_docs = $item->get_resources_from_source();
154
                }
155
156
                // Give a child element <item> to the <organization> element.
157
                $my_item_id = $item->get_id();
158
                $my_item = $xmldoc->createElement('item');
159
                $my_item->setAttribute('identifier', 'ITEM_'.$my_item_id);
160
                $my_item->setAttribute('identifierref', 'RESOURCE_'.$my_item_id);
161
                $my_item->setAttribute('isvisible', 'true');
162
                // Give a child element <title> to the <item> element.
163
                $my_title = $xmldoc->createElement(
164
                    'title',
165
                    htmlspecialchars(
166
                        api_utf8_encode($item->get_title()),
167
                        ENT_QUOTES,
168
                        'UTF-8'
169
                    )
170
                );
171
                $my_item->appendChild($my_title);
172
                // Give a child element <adlcp:prerequisites> to the <item> element.
173
                $my_prereqs = $xmldoc->createElement(
174
                    'adlcp:prerequisites',
175
                    $lp->get_scorm_prereq_string($my_item_id)
176
                );
177
                $my_prereqs->setAttribute('type', 'aicc_script');
178
                $my_item->appendChild($my_prereqs);
179
                // Give a child element <adlcp:maxtimeallowed> to the <item> element - not yet supported.
180
                //$xmldoc->createElement('adlcp:maxtimeallowed','');
181
                // Give a child element <adlcp:timelimitaction> to the <item> element - not yet supported.
182
                //$xmldoc->createElement('adlcp:timelimitaction','');
183
                // Give a child element <adlcp:datafromlms> to the <item> element - not yet supported.
184
                //$xmldoc->createElement('adlcp:datafromlms','');
185
                // Give a child element <adlcp:masteryscore> to the <item> element.
186
                $my_masteryscore = $xmldoc->createElement('adlcp:masteryscore', $item->get_mastery_score());
187
                $my_item->appendChild($my_masteryscore);
188
189
                // Attach this item to the organization element or hits parent if there is one.
190
                if (!empty($item->parent) && 0 != $item->parent) {
191
                    $children = $organization->childNodes;
192
                    $possible_parent = $lp->get_scorm_xml_node($children, 'ITEM_'.$item->parent);
193
                    if (is_object($possible_parent)) {
194
                        $possible_parent->appendChild($my_item);
195
                    } else {
196
                        if ($lp->debug > 0) {
197
                            error_log('Parent ITEM_'.$item->parent.' of item ITEM_'.$my_item_id.' not found');
198
                        }
199
                    }
200
                } else {
201
                    if ($lp->debug > 0) {
202
                        error_log('No parent');
203
                    }
204
                    $organization->appendChild($my_item);
205
                }
206
207
                // Get the path of the file(s) from the course directory root.
208
                $my_file_path = $item->get_file_path('scorm/'.$lp->path.'/');
209
                $my_xml_file_path = $my_file_path;
210
                if (!empty($path_to_remove)) {
211
                    // From docs
212
                    $my_xml_file_path = str_replace($path_to_remove, $path_to_replace, $my_file_path);
213
214
                    // From quiz
215
                    if ('chamilo_scorm_export' === $lp->ref) {
216
                        $path_to_remove = 'scorm/'.$lp->path.'/';
217
                        $my_xml_file_path = str_replace($path_to_remove, '', $my_file_path);
218
                    }
219
                }
220
221
                $my_sub_dir = dirname($my_file_path);
222
                $my_sub_dir = str_replace('\\', '/', $my_sub_dir);
223
                $my_xml_sub_dir = $my_sub_dir;
224
                // Give a <resource> child to the <resources> element
225
                $my_resource = $xmldoc->createElement('resource');
226
                $my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
227
                $my_resource->setAttribute('type', 'webcontent');
228
                $my_resource->setAttribute('href', $my_xml_file_path);
229
                // adlcp:scormtype can be either 'sco' or 'asset'.
230
                if ('sco' === $item->type) {
231
                    $my_resource->setAttribute('adlcp:scormtype', 'sco');
232
                } else {
233
                    $my_resource->setAttribute('adlcp:scormtype', 'asset');
234
                }
235
                // xml:base is the base directory to find the files declared in this resource.
236
                $my_resource->setAttribute('xml:base', '');
237
                // Give a <file> child to the <resource> element.
238
                $my_file = $xmldoc->createElement('file');
239
                $my_file->setAttribute('href', $my_xml_file_path);
240
                $my_resource->appendChild($my_file);
241
242
                // Dependency to other files - not yet supported.
243
                $i = 1;
244
                if ($inc_docs) {
245
                    foreach ($inc_docs as $doc_info) {
246
                        if (count($doc_info) < 1 || empty($doc_info[0])) {
247
                            continue;
248
                        }
249
                        $my_dep = $xmldoc->createElement('resource');
250
                        $res_id = 'RESOURCE_'.$item->get_id().'_'.$i;
251
                        $my_dep->setAttribute('identifier', $res_id);
252
                        $my_dep->setAttribute('type', 'webcontent');
253
                        $my_dep->setAttribute('adlcp:scormtype', 'asset');
254
                        $my_dep_file = $xmldoc->createElement('file');
255
                        // Check type of URL.
256
                        if ('remote' == $doc_info[1]) {
257
                            // Remote file. Save url as is.
258
                            $my_dep_file->setAttribute('href', $doc_info[0]);
259
                            $my_dep->setAttribute('xml:base', '');
260
                        } elseif ('local' === $doc_info[1]) {
261
                            switch ($doc_info[2]) {
262
                                case 'url':
263
                                    // Local URL - save path as url for now, don't zip file.
264
                                    $abs_path = api_get_path(SYS_PATH).
265
                                        str_replace(api_get_path(WEB_PATH), '', $doc_info[0]);
266
                                    $current_dir = dirname($abs_path);
267
                                    $current_dir = str_replace('\\', '/', $current_dir);
268
                                    $file_path = realpath($abs_path);
269
                                    $file_path = str_replace('\\', '/', $file_path);
270
                                    $my_dep_file->setAttribute('href', $file_path);
271
                                    $my_dep->setAttribute('xml:base', '');
272
                                    if (false !== strstr($file_path, $main_path)) {
273
                                        // The calculated real path is really inside Chamilo's root path.
274
                                        // Reduce file path to what's under the DocumentRoot.
275
                                        $replace = $file_path;
276
                                        $file_path = substr($file_path, strlen($root_path) - 1);
277
                                        $destinationFile = $file_path;
278
279
                                        if (false !== strstr($file_path, 'upload/users')) {
280
                                            $pos = strpos($file_path, 'my_files/');
281
                                            if (false !== $pos) {
282
                                                $onlyDirectory = str_replace(
283
                                                    'upload/users/',
284
                                                    '',
285
                                                    substr($file_path, $pos, strlen($file_path))
286
                                                );
287
                                            }
288
                                            $replace = $onlyDirectory;
289
                                            $destinationFile = $replace;
290
                                        }
291
                                        $zip_files_abs[] = $file_path;
292
                                        $link_updates[$my_file_path][] = [
293
                                            'orig' => $doc_info[0],
294
                                            'dest' => $destinationFile,
295
                                            'replace' => $replace,
296
                                        ];
297
                                        $my_dep_file->setAttribute('href', $file_path);
298
                                        $my_dep->setAttribute('xml:base', '');
299
                                    } elseif (empty($file_path)) {
300
                                        $file_path = $_SERVER['DOCUMENT_ROOT'].$abs_path;
301
                                        $file_path = str_replace('//', '/', $file_path);
302
                                        if (file_exists($file_path)) {
303
                                            // We get the relative path.
304
                                            $file_path = substr($file_path, strlen($current_dir));
305
                                            $zip_files[] = $my_sub_dir.'/'.$file_path;
306
                                            $link_updates[$my_file_path][] = [
307
                                                'orig' => $doc_info[0],
308
                                                'dest' => $file_path,
309
                                            ];
310
                                            $my_dep_file->setAttribute('href', $file_path);
311
                                            $my_dep->setAttribute('xml:base', '');
312
                                        }
313
                                    }
314
                                    break;
315
                                case 'abs':
316
                                    // Absolute path from DocumentRoot. Save file and leave path as is in the zip.
317
                                    $my_dep_file->setAttribute('href', $doc_info[0]);
318
                                    $my_dep->setAttribute('xml:base', '');
319
320
                                    // The next lines fix a bug when using the "subdir" mode of Chamilo, whereas
321
                                    // an image path would be constructed as /var/www/subdir/subdir/img/foo.bar
322
                                    $abs_img_path_without_subdir = $doc_info[0];
323
                                    $relp = api_get_path(REL_PATH); // The url-append config param.
324
                                    $pos = strpos($abs_img_path_without_subdir, $relp);
325
                                    if (0 === $pos) {
326
                                        $abs_img_path_without_subdir = trim('/'.substr($abs_img_path_without_subdir, strlen($relp)));
327
                                    }
328
329
                                    $file_path = realpath(api_get_path(SYS_APP_PATH).$abs_img_path_without_subdir);
330
                                    $file_path = str_replace(['\\', '//'], '/', $file_path);
331
332
                                    // Prepare the current directory path (until just under 'document') with a trailing slash.
333
                                    $cur_path = '/' == substr($current_course_path, -1) ? $current_course_path : $current_course_path.'/';
334
                                    // Check if the current document is in that path.
335
                                    if (false !== strstr($file_path, $cur_path)) {
336
                                        $destinationFile = substr($file_path, strlen($cur_path));
337
                                        $filePathNoCoursePart = substr($file_path, strlen($cur_path));
338
339
                                        $fileToTest = $cur_path.$my_file_path;
340
                                        if (!empty($path_to_remove)) {
341
                                            $fileToTest = str_replace(
342
                                                $path_to_remove.'/',
343
                                                $path_to_replace,
344
                                                $cur_path.$my_file_path
345
                                            );
346
                                        }
347
348
                                        $relative_path = api_get_relative_path($fileToTest, $file_path);
349
350
                                        // Put the current document in the zip (this array is the array
351
                                        // that will manage documents already in the course folder - relative).
352
                                        $zip_files[] = $filePathNoCoursePart;
353
                                        // Update the links to the current document in the
354
                                        // containing document (make them relative).
355
                                        $link_updates[$my_file_path][] = [
356
                                            'orig' => $doc_info[0],
357
                                            'dest' => $destinationFile,
358
                                            'replace' => $relative_path,
359
                                        ];
360
361
                                        $my_dep_file->setAttribute('href', $file_path);
362
                                        $my_dep->setAttribute('xml:base', '');
363
                                    } elseif (false !== strstr($file_path, $main_path)) {
364
                                        // The calculated real path is really inside Chamilo's root path.
365
                                        // Reduce file path to what's under the DocumentRoot.
366
                                        $file_path = substr($file_path, strlen($root_path));
367
                                        $zip_files_abs[] = $file_path;
368
                                        $link_updates[$my_file_path][] = ['orig' => $doc_info[0], 'dest' => $file_path];
369
                                        $my_dep_file->setAttribute('href', 'document/'.$file_path);
370
                                        $my_dep->setAttribute('xml:base', '');
371
                                    } elseif (empty($file_path)) {
372
                                        // Probably this is an image inside "/main" directory
373
                                        $file_path = api_get_path(SYS_PATH).$abs_img_path_without_subdir;
374
                                        $abs_path = api_get_path(SYS_PATH).str_replace(api_get_path(WEB_PATH), '', $doc_info[0]);
375
376
                                        if (file_exists($file_path)) {
377
                                            if (false !== strstr($file_path, 'main/default_course_document')) {
378
                                                // We get the relative path.
379
                                                $pos = strpos($file_path, 'main/default_course_document/');
380
                                                if (false !== $pos) {
381
                                                    $onlyDirectory = str_replace(
382
                                                        'main/default_course_document/',
383
                                                        '',
384
                                                        substr($file_path, $pos, strlen($file_path))
385
                                                    );
386
                                                }
387
388
                                                $destinationFile = 'default_course_document/'.$onlyDirectory;
389
                                                $fileAbs = substr($file_path, strlen(api_get_path(SYS_PATH)));
390
                                                $zip_files_abs[] = $fileAbs;
391
                                                $link_updates[$my_file_path][] = [
392
                                                    'orig' => $doc_info[0],
393
                                                    'dest' => $destinationFile,
394
                                                ];
395
                                                $my_dep_file->setAttribute('href', 'document/'.$file_path);
396
                                                $my_dep->setAttribute('xml:base', '');
397
                                            }
398
                                        }
399
                                    }
400
                                    break;
401
                                case 'rel':
402
                                    // Path relative to the current document.
403
                                    // Save xml:base as current document's directory and save file in zip as subdir.file_path
404
                                    if ('..' === substr($doc_info[0], 0, 2)) {
405
                                        // Relative path going up.
406
                                        $current_dir = dirname($current_course_path.'/'.$item->get_file_path()).'/';
407
                                        $current_dir = str_replace('\\', '/', $current_dir);
408
                                        $file_path = realpath($current_dir.$doc_info[0]);
409
                                        $file_path = str_replace('\\', '/', $file_path);
410
                                        if (false !== strstr($file_path, $main_path)) {
411
                                            // The calculated real path is really inside Chamilo's root path.
412
                                            // Reduce file path to what's under the DocumentRoot.
413
                                            $file_path = substr($file_path, strlen($root_path));
414
                                            $zip_files_abs[] = $file_path;
415
                                            $link_updates[$my_file_path][] = ['orig' => $doc_info[0], 'dest' => $file_path];
416
                                            $my_dep_file->setAttribute('href', 'document/'.$file_path);
417
                                            $my_dep->setAttribute('xml:base', '');
418
                                        }
419
                                    } else {
420
                                        $zip_files[] = $my_sub_dir.'/'.$doc_info[0];
421
                                        $my_dep_file->setAttribute('href', $doc_info[0]);
422
                                        $my_dep->setAttribute('xml:base', $my_xml_sub_dir);
423
                                    }
424
                                    break;
425
                                default:
426
                                    $my_dep_file->setAttribute('href', $doc_info[0]);
427
                                    $my_dep->setAttribute('xml:base', '');
428
                                    break;
429
                            }
430
                        }
431
                        $my_dep->appendChild($my_dep_file);
432
                        $resources->appendChild($my_dep);
433
                        $dependency = $xmldoc->createElement('dependency');
434
                        $dependency->setAttribute('identifierref', $res_id);
435
                        $my_resource->appendChild($dependency);
436
                        $i++;
437
                    }
438
                }
439
                $resources->appendChild($my_resource);
440
                $zip_files[] = $my_file_path;
441
            } else {
442
                // If the item is a quiz or a link or whatever non-exportable, we include a step indicating it.
443
                switch ($item->type) {
444
                    case TOOL_LINK:
445
                        $my_item = $xmldoc->createElement('item');
446
                        $my_item->setAttribute('identifier', 'ITEM_'.$item->get_id());
447
                        $my_item->setAttribute('identifierref', 'RESOURCE_'.$item->get_id());
448
                        $my_item->setAttribute('isvisible', 'true');
449
                        // Give a child element <title> to the <item> element.
450
                        $my_title = $xmldoc->createElement(
451
                            'title',
452
                            htmlspecialchars(
453
                                api_utf8_encode($item->get_title()),
454
                                ENT_QUOTES,
455
                                'UTF-8'
456
                            )
457
                        );
458
                        $my_item->appendChild($my_title);
459
                        // Give a child element <adlcp:prerequisites> to the <item> element.
460
                        $my_prereqs = $xmldoc->createElement('adlcp:prerequisites', $item->get_prereq_string());
461
                        $my_prereqs->setAttribute('type', 'aicc_script');
462
                        $my_item->appendChild($my_prereqs);
463
                        // Give a child element <adlcp:maxtimeallowed> to the <item> element - not yet supported.
464
                        //$xmldoc->createElement('adlcp:maxtimeallowed', '');
465
                        // Give a child element <adlcp:timelimitaction> to the <item> element - not yet supported.
466
                        //$xmldoc->createElement('adlcp:timelimitaction', '');
467
                        // Give a child element <adlcp:datafromlms> to the <item> element - not yet supported.
468
                        //$xmldoc->createElement('adlcp:datafromlms', '');
469
                        // Give a child element <adlcp:masteryscore> to the <item> element.
470
                        $my_masteryscore = $xmldoc->createElement('adlcp:masteryscore', $item->get_mastery_score());
471
                        $my_item->appendChild($my_masteryscore);
472
473
                        // Attach this item to the organization element or its parent if there is one.
474
                        if (!empty($item->parent) && 0 != $item->parent) {
475
                            $children = $organization->childNodes;
476
                            for ($i = 0; $i < $children->length; $i++) {
477
                                $item_temp = $children->item($i);
478
                                if ('item' == $item_temp->nodeName) {
479
                                    if ($item_temp->getAttribute('identifier') == 'ITEM_'.$item->parent) {
480
                                        $item_temp->appendChild($my_item);
481
                                    }
482
                                }
483
                            }
484
                        } else {
485
                            $organization->appendChild($my_item);
486
                        }
487
488
                        $my_file_path = 'link_'.$item->get_id().'.html';
489
                        $sql = 'SELECT url, title FROM '.Database::get_course_table(TABLE_LINK).'
490
                                WHERE c_id = '.$course_id.' AND id = '.$item->path;
491
                        $rs = Database::query($sql);
492
                        if ($link = Database::fetch_array($rs)) {
493
                            $url = $link['url'];
494
                            $title = stripslashes($link['title']);
495
                            $links_to_create[$my_file_path] = ['title' => $title, 'url' => $url];
496
                            $my_xml_file_path = $my_file_path;
497
                            $my_sub_dir = dirname($my_file_path);
498
                            $my_sub_dir = str_replace('\\', '/', $my_sub_dir);
499
                            $my_xml_sub_dir = $my_sub_dir;
500
                            // Give a <resource> child to the <resources> element.
501
                            $my_resource = $xmldoc->createElement('resource');
502
                            $my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
503
                            $my_resource->setAttribute('type', 'webcontent');
504
                            $my_resource->setAttribute('href', $my_xml_file_path);
505
                            // adlcp:scormtype can be either 'sco' or 'asset'.
506
                            $my_resource->setAttribute('adlcp:scormtype', 'asset');
507
                            // xml:base is the base directory to find the files declared in this resource.
508
                            $my_resource->setAttribute('xml:base', '');
509
                            // give a <file> child to the <resource> element.
510
                            $my_file = $xmldoc->createElement('file');
511
                            $my_file->setAttribute('href', $my_xml_file_path);
512
                            $my_resource->appendChild($my_file);
513
                            $resources->appendChild($my_resource);
514
                        }
515
                        break;
516
                    case TOOL_QUIZ:
517
                        $exe_id = $item->path;
518
                        // Should be using ref when everything will be cleaned up in this regard.
519
                        $exe = new Exercise();
520
                        $exe->read($exe_id);
521
                        $my_item = $xmldoc->createElement('item');
522
                        $my_item->setAttribute('identifier', 'ITEM_'.$item->get_id());
523
                        $my_item->setAttribute('identifierref', 'RESOURCE_'.$item->get_id());
524
                        $my_item->setAttribute('isvisible', 'true');
525
                        // Give a child element <title> to the <item> element.
526
                        $my_title = $xmldoc->createElement(
527
                            'title',
528
                            htmlspecialchars(
529
                                api_utf8_encode($item->get_title()),
530
                                ENT_QUOTES,
531
                                'UTF-8'
532
                            )
533
                        );
534
                        $my_item->appendChild($my_title);
535
                        $my_max_score = $xmldoc->createElement('max_score', $item->get_max());
536
                        $my_item->appendChild($my_max_score);
537
                        // Give a child element <adlcp:prerequisites> to the <item> element.
538
                        $my_prereqs = $xmldoc->createElement('adlcp:prerequisites', $item->get_prereq_string());
539
                        $my_prereqs->setAttribute('type', 'aicc_script');
540
                        $my_item->appendChild($my_prereqs);
541
                        // Give a child element <adlcp:masteryscore> to the <item> element.
542
                        $my_masteryscore = $xmldoc->createElement('adlcp:masteryscore', $item->get_mastery_score());
543
                        $my_item->appendChild($my_masteryscore);
544
545
                        // Attach this item to the organization element or hits parent if there is one.
546
                        if (!empty($item->parent) && 0 != $item->parent) {
547
                            $children = $organization->childNodes;
548
                            $possible_parent = $lp->get_scorm_xml_node($children, 'ITEM_'.$item->parent);
549
                            if ($possible_parent) {
550
                                if ($possible_parent->getAttribute('identifier') === 'ITEM_'.$item->parent) {
551
                                    $possible_parent->appendChild($my_item);
552
                                }
553
                            }
554
                        } else {
555
                            $organization->appendChild($my_item);
556
                        }
557
558
                        // Get the path of the file(s) from the course directory root
559
                        //$my_file_path = $item->get_file_path('scorm/'.$lp->path.'/');
560
                        $my_file_path = 'quiz_'.$item->get_id().'.html';
561
                        // Write the contents of the exported exercise into a (big) html file
562
                        // to later pack it into the exported SCORM. The file will be removed afterwards.
563
                        $scormExercise = new ScormExercise($exe, true);
564
                        $contents = $scormExercise->export();
565
566
                        $tmp_file_path = $archivePath.$temp_dir_short.'/'.$my_file_path;
567
                        $res = file_put_contents($tmp_file_path, $contents);
568
                        if (false === $res) {
569
                            error_log('Could not write into file '.$tmp_file_path.' '.__FILE__.' '.__LINE__, 0);
570
                        }
571
                        $files_cleanup[] = $tmp_file_path;
572
                        $my_xml_file_path = $my_file_path;
573
                        $my_sub_dir = dirname($my_file_path);
574
                        $my_sub_dir = str_replace('\\', '/', $my_sub_dir);
575
                        $my_xml_sub_dir = $my_sub_dir;
576
                        // Give a <resource> child to the <resources> element.
577
                        $my_resource = $xmldoc->createElement('resource');
578
                        $my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
579
                        $my_resource->setAttribute('type', 'webcontent');
580
                        $my_resource->setAttribute('href', $my_xml_file_path);
581
                        // adlcp:scormtype can be either 'sco' or 'asset'.
582
                        $my_resource->setAttribute('adlcp:scormtype', 'sco');
583
                        // xml:base is the base directory to find the files declared in this resource.
584
                        $my_resource->setAttribute('xml:base', '');
585
                        // Give a <file> child to the <resource> element.
586
                        $my_file = $xmldoc->createElement('file');
587
                        $my_file->setAttribute('href', $my_xml_file_path);
588
                        $my_resource->appendChild($my_file);
589
590
                        // Get included docs.
591
                        $inc_docs = $item->get_resources_from_source(null, $tmp_file_path);
592
593
                        // Dependency to other files - not yet supported.
594
                        $i = 1;
595
                        foreach ($inc_docs as $doc_info) {
596
                            if (count($doc_info) < 1 || empty($doc_info[0])) {
597
                                continue;
598
                            }
599
                            $my_dep = $xmldoc->createElement('resource');
600
                            $res_id = 'RESOURCE_'.$item->get_id().'_'.$i;
601
                            $my_dep->setAttribute('identifier', $res_id);
602
                            $my_dep->setAttribute('type', 'webcontent');
603
                            $my_dep->setAttribute('adlcp:scormtype', 'asset');
604
                            $my_dep_file = $xmldoc->createElement('file');
605
                            // Check type of URL.
606
                            if ('remote' == $doc_info[1]) {
607
                                // Remote file. Save url as is.
608
                                $my_dep_file->setAttribute('href', $doc_info[0]);
609
                                $my_dep->setAttribute('xml:base', '');
610
                            } elseif ('local' == $doc_info[1]) {
611
                                switch ($doc_info[2]) {
612
                                    case 'url': // Local URL - save path as url for now, don't zip file.
613
                                        // Save file but as local file (retrieve from URL).
614
                                        $abs_path = api_get_path(SYS_PATH).
615
                                            str_replace(api_get_path(WEB_PATH), '', $doc_info[0]);
616
                                        $current_dir = dirname($abs_path);
617
                                        $current_dir = str_replace('\\', '/', $current_dir);
618
                                        $file_path = realpath($abs_path);
619
                                        $file_path = str_replace('\\', '/', $file_path);
620
                                        $my_dep_file->setAttribute('href', 'document/'.$file_path);
621
                                        $my_dep->setAttribute('xml:base', '');
622
                                        if (false !== strstr($file_path, $main_path)) {
623
                                            // The calculated real path is really inside the chamilo root path.
624
                                            // Reduce file path to what's under the DocumentRoot.
625
                                            $file_path = substr($file_path, strlen($root_path));
626
                                            $zip_files_abs[] = $file_path;
627
                                            $link_updates[$my_file_path][] = [
628
                                                'orig' => $doc_info[0],
629
                                                'dest' => 'document/'.$file_path,
630
                                            ];
631
                                            $my_dep_file->setAttribute('href', 'document/'.$file_path);
632
                                            $my_dep->setAttribute('xml:base', '');
633
                                        } elseif (empty($file_path)) {
634
                                            $file_path = $_SERVER['DOCUMENT_ROOT'].$abs_path;
635
                                            $file_path = str_replace('//', '/', $file_path);
636
                                            if (file_exists($file_path)) {
637
                                                $file_path = substr($file_path, strlen($current_dir));
638
                                                // We get the relative path.
639
                                                $zip_files[] = $my_sub_dir.'/'.$file_path;
640
                                                $link_updates[$my_file_path][] = [
641
                                                    'orig' => $doc_info[0],
642
                                                    'dest' => 'document/'.$file_path,
643
                                                ];
644
                                                $my_dep_file->setAttribute('href', 'document/'.$file_path);
645
                                                $my_dep->setAttribute('xml:base', '');
646
                                            }
647
                                        }
648
                                        break;
649
                                    case 'abs':
650
                                        // Absolute path from DocumentRoot. Save file and leave path as is in the zip.
651
                                        $current_dir = dirname($current_course_path.'/'.$item->get_file_path()).'/';
652
                                        $current_dir = str_replace('\\', '/', $current_dir);
653
                                        $file_path = realpath($doc_info[0]);
654
                                        $file_path = str_replace('\\', '/', $file_path);
655
                                        $my_dep_file->setAttribute('href', $file_path);
656
                                        $my_dep->setAttribute('xml:base', '');
657
658
                                        if (false !== strstr($file_path, $main_path)) {
659
                                            // The calculated real path is really inside the chamilo root path.
660
                                            // Reduce file path to what's under the DocumentRoot.
661
                                            $file_path = substr($file_path, strlen($root_path));
662
                                            $zip_files_abs[] = $file_path;
663
                                            $link_updates[$my_file_path][] = [
664
                                                'orig' => $doc_info[0],
665
                                                'dest' => $file_path,
666
                                            ];
667
                                            $my_dep_file->setAttribute('href', 'document/'.$file_path);
668
                                            $my_dep->setAttribute('xml:base', '');
669
                                        } elseif (empty($file_path)) {
670
                                            $docSysPartPath = str_replace(
671
                                                api_get_path(REL_COURSE_PATH),
672
                                                '',
673
                                                $doc_info[0]
674
                                            );
675
676
                                            $docSysPartPathNoCourseCode = str_replace(
677
                                                $_course['directory'].'/',
678
                                                '',
679
                                                $docSysPartPath
680
                                            );
681
682
                                            $docSysPath = api_get_path(SYS_COURSE_PATH).$docSysPartPath;
683
                                            if (file_exists($docSysPath)) {
684
                                                $file_path = $docSysPartPathNoCourseCode;
685
                                                $zip_files[] = $my_sub_dir.'/'.$file_path;
686
                                                $link_updates[$my_file_path][] = [
687
                                                    'orig' => $doc_info[0],
688
                                                    'dest' => $file_path,
689
                                                ];
690
                                                $my_dep_file->setAttribute('href', 'document/'.$file_path);
691
                                                $my_dep->setAttribute('xml:base', '');
692
                                            }
693
                                        }
694
                                        break;
695
                                    case 'rel':
696
                                        // Path relative to the current document. Save xml:base as current document's
697
                                        // directory and save file in zip as subdir.file_path
698
                                        if ('..' === substr($doc_info[0], 0, 2)) {
699
                                            // Relative path going up.
700
                                            $current_dir = dirname($current_course_path.'/'.$item->get_file_path()).'/';
701
                                            $current_dir = str_replace('\\', '/', $current_dir);
702
                                            $file_path = realpath($current_dir.$doc_info[0]);
703
                                            $file_path = str_replace('\\', '/', $file_path);
704
                                            if (false !== strstr($file_path, $main_path)) {
705
                                                // The calculated real path is really inside Chamilo's root path.
706
                                                // Reduce file path to what's under the DocumentRoot.
707
708
                                                $file_path = substr($file_path, strlen($root_path));
709
                                                $file_path_dest = $file_path;
710
711
                                                // File path is courses/CHAMILO/document/....
712
                                                $info_file_path = explode('/', $file_path);
713
                                                if ('courses' == $info_file_path[0]) {
714
                                                    // Add character "/" in file path.
715
                                                    $file_path_dest = 'document/'.$file_path;
716
                                                }
717
                                                $zip_files_abs[] = $file_path;
718
719
                                                $link_updates[$my_file_path][] = [
720
                                                    'orig' => $doc_info[0],
721
                                                    'dest' => $file_path_dest,
722
                                                ];
723
                                                $my_dep_file->setAttribute('href', 'document/'.$file_path);
724
                                                $my_dep->setAttribute('xml:base', '');
725
                                            }
726
                                        } else {
727
                                            $zip_files[] = $my_sub_dir.'/'.$doc_info[0];
728
                                            $my_dep_file->setAttribute('href', $doc_info[0]);
729
                                            $my_dep->setAttribute('xml:base', $my_xml_sub_dir);
730
                                        }
731
                                        break;
732
                                    default:
733
                                        $my_dep_file->setAttribute('href', $doc_info[0]); // ../../courses/
734
                                        $my_dep->setAttribute('xml:base', '');
735
                                        break;
736
                                }
737
                            }
738
                            $my_dep->appendChild($my_dep_file);
739
                            $resources->appendChild($my_dep);
740
                            $dependency = $xmldoc->createElement('dependency');
741
                            $dependency->setAttribute('identifierref', $res_id);
742
                            $my_resource->appendChild($dependency);
743
                            $i++;
744
                        }
745
                        $resources->appendChild($my_resource);
746
                        $zip_files[] = $my_file_path;
747
                        break;
748
                    default:
749
                        // Get the path of the file(s) from the course directory root
750
                        $my_file_path = 'non_exportable.html';
751
                        //$my_xml_file_path = api_htmlentities(api_utf8_encode($my_file_path), ENT_COMPAT, 'UTF-8');
752
                        $my_xml_file_path = $my_file_path;
753
                        $my_sub_dir = dirname($my_file_path);
754
                        $my_sub_dir = str_replace('\\', '/', $my_sub_dir);
755
                        //$my_xml_sub_dir = api_htmlentities(api_utf8_encode($my_sub_dir), ENT_COMPAT, 'UTF-8');
756
                        $my_xml_sub_dir = $my_sub_dir;
757
                        // Give a <resource> child to the <resources> element.
758
                        $my_resource = $xmldoc->createElement('resource');
759
                        $my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
760
                        $my_resource->setAttribute('type', 'webcontent');
761
                        $my_resource->setAttribute('href', $folder_name.'/'.$my_xml_file_path);
762
                        // adlcp:scormtype can be either 'sco' or 'asset'.
763
                        $my_resource->setAttribute('adlcp:scormtype', 'asset');
764
                        // xml:base is the base directory to find the files declared in this resource.
765
                        $my_resource->setAttribute('xml:base', '');
766
                        // Give a <file> child to the <resource> element.
767
                        $my_file = $xmldoc->createElement('file');
768
                        $my_file->setAttribute('href', 'document/'.$my_xml_file_path);
769
                        $my_resource->appendChild($my_file);
770
                        $resources->appendChild($my_resource);
771
                        break;
772
                }
773
            }
774
        }
775
        $organizations->appendChild($organization);
776
        $root->appendChild($organizations);
777
        $root->appendChild($resources);
778
        $xmldoc->appendChild($root);
779
780
        $copyAll = ('true' === api_get_setting('lp.add_all_files_in_lp_export'));
781
782
        // then add the file to the zip, then destroy the file (this is done automatically).
783
        // http://www.reload.ac.uk/scormplayer.html - once done, don't forget to close FS#138
784
        foreach ($zip_files as $file_path) {
785
            if (empty($file_path)) {
786
                continue;
787
            }
788
789
            $filePath = $sys_course_path.$_course['path'].'/'.$file_path;
790
            $dest_file = $archivePath.$temp_dir_short.'/'.$file_path;
791
792
            if (!empty($path_to_remove) && !empty($path_to_replace)) {
793
                $dest_file = str_replace($path_to_remove, $path_to_replace, $dest_file);
794
            }
795
796
            $lp->create_path($dest_file);
797
            @copy($filePath, $dest_file);
798
799
            // Check if the file needs a link update.
800
            if (in_array($file_path, array_keys($link_updates))) {
801
                $string = file_get_contents($dest_file);
802
                unlink($dest_file);
803
                foreach ($link_updates[$file_path] as $old_new) {
804
                    // This is an ugly hack that allows .flv files to be found by the flv player that
805
                    // will be added in document/main/inc/lib/flv_player/flv_player.swf and that needs
806
                    // to find the flv to play in document/main/, so we replace main/ in the flv path by
807
                    // ../../.. to return from inc/lib/flv_player to the document/main path.
808
                    if ('flv' === substr($old_new['dest'], -3) &&
809
                        'main/' === substr($old_new['dest'], 0, 5)
810
                    ) {
811
                        $old_new['dest'] = str_replace('main/', '../../../', $old_new['dest']);
812
                    } elseif ('flv' === substr($old_new['dest'], -3) &&
813
                        'video/' === substr($old_new['dest'], 0, 6)
814
                    ) {
815
                        $old_new['dest'] = str_replace('video/', '../../../../video/', $old_new['dest']);
816
                    }
817
818
                    // Fix to avoid problems with default_course_document
819
                    if (false === strpos('main/default_course_document', $old_new['dest'])) {
820
                        $newDestination = $old_new['dest'];
821
                        if (isset($old_new['replace']) && !empty($old_new['replace'])) {
822
                            $newDestination = $old_new['replace'];
823
                        }
824
                    } else {
825
                        $newDestination = str_replace('document/', '', $old_new['dest']);
826
                    }
827
                    $string = str_replace($old_new['orig'], $newDestination, $string);
828
829
                    // Add files inside the HTMLs
830
                    $new_path = str_replace(api_get_path(REL_COURSE_PATH), '', $old_new['orig']);
831
                    $destinationFile = $archivePath.$temp_dir_short.'/'.$old_new['dest'];
832
                    if (file_exists($sys_course_path.$new_path) && is_file($sys_course_path.$new_path)) {
833
                        copy(
834
                            $sys_course_path.$new_path,
835
                            $destinationFile
836
                        );
837
                    }
838
                }
839
                file_put_contents($dest_file, $string);
840
            }
841
842
            if (file_exists($filePath) && $copyAll) {
843
                $extension = $lp->get_extension($filePath);
844
                if (in_array($extension, ['html', 'html'])) {
845
                    $containerOrigin = dirname($filePath);
846
                    $containerDestination = dirname($dest_file);
847
848
                    $finder = new Finder();
849
                    $finder->files()->in($containerOrigin)
850
                        ->notName('*_DELETED_*')
851
                        ->exclude('share_folder')
852
                        ->exclude('chat_files')
853
                        ->exclude('certificates')
854
                    ;
855
856
                    if (is_dir($containerOrigin) &&
857
                        is_dir($containerDestination)
858
                    ) {
859
                        $fs = new Filesystem();
860
                        $fs->mirror(
861
                            $containerOrigin,
862
                            $containerDestination,
863
                            $finder
864
                        );
865
                    }
866
                }
867
            }
868
        }
869
870
        foreach ($zip_files_abs as $file_path) {
871
            if (empty($file_path)) {
872
                continue;
873
            }
874
875
            if (!is_file($main_path.$file_path) || !is_readable($main_path.$file_path)) {
876
                continue;
877
            }
878
879
            $dest_file = $archivePath.$temp_dir_short.'/document/'.$file_path;
880
            if (false !== strstr($file_path, 'upload/users')) {
881
                $pos = strpos($file_path, 'my_files/');
882
                if (false !== $pos) {
883
                    $onlyDirectory = str_replace(
884
                        'upload/users/',
885
                        '',
886
                        substr($file_path, $pos, strlen($file_path))
887
                    );
888
                    $dest_file = $archivePath.$temp_dir_short.'/document/'.$onlyDirectory;
889
                }
890
            }
891
892
            if (false !== strstr($file_path, 'default_course_document/')) {
893
                $replace = str_replace('/main', '', $file_path);
894
                $dest_file = $archivePath.$temp_dir_short.'/document/'.$replace;
895
            }
896
897
            if (empty($dest_file)) {
898
                continue;
899
            }
900
901
            $lp->create_path($dest_file);
902
            copy($main_path.$file_path, $dest_file);
903
            // Check if the file needs a link update.
904
            if (in_array($file_path, array_keys($link_updates))) {
905
                $string = file_get_contents($dest_file);
906
                unlink($dest_file);
907
                foreach ($link_updates[$file_path] as $old_new) {
908
                    // This is an ugly hack that allows .flv files to be found by the flv player that
909
                    // will be added in document/main/inc/lib/flv_player/flv_player.swf and that needs
910
                    // to find the flv to play in document/main/, so we replace main/ in the flv path by
911
                    // ../../.. to return from inc/lib/flv_player to the document/main path.
912
                    if ('flv' == substr($old_new['dest'], -3) &&
913
                        'main/' == substr($old_new['dest'], 0, 5)
914
                    ) {
915
                        $old_new['dest'] = str_replace('main/', '../../../', $old_new['dest']);
916
                    }
917
                    $string = str_replace($old_new['orig'], $old_new['dest'], $string);
918
                }
919
                file_put_contents($dest_file, $string);
920
            }
921
        }
922
923
        if (is_array($links_to_create)) {
924
            foreach ($links_to_create as $file => $link) {
925
                $content = '<!DOCTYPE html><head>
926
                            <meta charset="'.api_get_language_isocode().'" />
927
                            <title>'.$link['title'].'</title>
928
                            </head>
929
                            <body dir="'.api_get_text_direction().'">
930
                            <div style="text-align:center">
931
                            <a href="'.$link['url'].'">'.$link['title'].'</a></div>
932
                            </body>
933
                            </html>';
934
                file_put_contents($archivePath.$temp_dir_short.'/'.$file, $content);
935
            }
936
        }
937
938
        // Add non exportable message explanation.
939
        $lang_not_exportable = get_lang('This learning object or activity is not SCORM compliant. That\'s why it is not exportable.');
940
        $file_content = '<!DOCTYPE html><head>
941
                        <meta charset="'.api_get_language_isocode().'" />
942
                        <title>'.$lang_not_exportable.'</title>
943
                        <meta http-equiv="Content-Type" content="text/html; charset='.api_get_system_encoding().'" />
944
                        </head>
945
                        <body dir="'.api_get_text_direction().'">';
946
        $file_content .=
947
            <<<EOD
948
                    <style>
949
            .error-message {
950
                font-family: arial, verdana, helvetica, sans-serif;
951
                border-width: 1px;
952
                border-style: solid;
953
                left: 50%;
954
                margin: 10px auto;
955
                min-height: 30px;
956
                padding: 5px;
957
                right: 50%;
958
                width: 500px;
959
                background-color: #FFD1D1;
960
                border-color: #FF0000;
961
                color: #000;
962
            }
963
        </style>
964
    <body>
965
        <div class="error-message">
966
            $lang_not_exportable
967
        </div>
968
    </body>
969
</html>
970
EOD;
971
        if (!is_dir($archivePath.$temp_dir_short.'/document')) {
972
            @mkdir($archivePath.$temp_dir_short.'/document', api_get_permissions_for_new_directories());
973
        }
974
        file_put_contents($archivePath.$temp_dir_short.'/document/non_exportable.html', $file_content);
975
976
        // Add the extra files that go along with a SCORM package.
977
        $main_code_path = api_get_path(SYS_CODE_PATH).'lp/packaging/';
978
979
        $fs = new Filesystem();
980
        $fs->mirror($main_code_path, $archivePath.$temp_dir_short);
981
982
        // Finalize the imsmanifest structure, add to the zip, then return the zip.
983
        $manifest = @$xmldoc->saveXML();
984
        $manifest = api_utf8_decode_xml($manifest); // The manifest gets the system encoding now.
985
        file_put_contents($archivePath.'/'.$temp_dir_short.'/imsmanifest.xml', $manifest);
986
        $zip_folder->add(
987
            $archivePath.'/'.$temp_dir_short,
988
            PCLZIP_OPT_REMOVE_PATH,
989
            $archivePath.'/'.$temp_dir_short.'/'
990
        );
991
992
        // Clean possible temporary files.
993
        foreach ($files_cleanup as $file) {
994
            $res = unlink($file);
995
            if (false === $res) {
996
                error_log(
997
                    'Could not delete temp file '.$file.' '.__FILE__.' '.__LINE__,
998
                    0
999
                );
1000
            }
1001
        }
1002
        $name = api_replace_dangerous_char($lp->get_name()).'.zip';
1003
        DocumentManager::file_send_for_download($temp_zip_file, true, $name);
1004
    }
1005
1006
    /**
1007
     * Export selected learning path items to a PDF.
1008
     */
1009
    public static function exportToPdf(int $lpId, array $courseInfo, array $selectedItems = []): ?bool
1010
    {
1011
        /** @var CLp $lp */
1012
        $lp = Container::getLpRepository()->find($lpId);
1013
        if (!$lp || empty($courseInfo)) {
1014
            return false;
1015
        }
1016
1017
        $lpItemRepo = Container::getLpItemRepository();
1018
        $documentRepo = Container::getDocumentRepository();
1019
        $filesToExport = [];
1020
        $courseCode = $courseInfo['code'];
1021
        $scormPath = null;
1022
1023
        $entries     = learnpath::get_flat_ordered_items_list($lp, 0, true);
1024
1025
        foreach ($entries as $entry) {
1026
            $itemId = is_array($entry) ? (int) $entry['iid'] : (int) $entry;
1027
1028
            if (!empty($selectedItems) && !in_array($itemId, $selectedItems)) {
1029
                continue;
1030
            }
1031
1032
            if (is_array($entry) && empty($entry['export_allowed'])) {
1033
                continue;
1034
            }
1035
1036
            /** @var CLpItem $item */
1037
            $item = $lpItemRepo->find($itemId);
1038
            if (!$item) {
1039
                continue;
1040
            }
1041
1042
            $type = $item->getItemType();
1043
1044
            switch ($type) {
1045
                case 'document':
1046
                    $document = $documentRepo->find((int) $item->getPath());
1047
                    if (!$document instanceof CDocument) {
1048
                        break;
1049
                    }
1050
1051
                    $fileType = $document->getFiletype();
1052
                    $resourceNode = $document->getResourceNode();
1053
1054
                    if ($fileType !== 'file' || !$resourceNode->hasResourceFile()) {
1055
                        break;
1056
                    }
1057
1058
                    try {
1059
                        $content = $documentRepo->getResourceFileContent($document);
1060
1061
                        if (!is_string($content) || stripos(ltrim($content), '<') !== 0) {
1062
                            break;
1063
                        }
1064
1065
                        $filesToExport[] = [
1066
                            'title' => $item->getTitle(),
1067
                            'content' => $content,
1068
                        ];
1069
                    } catch (\Throwable) {
1070
                        break;
1071
                    }
1072
1073
                    break;
1074
1075
                case 'asset':
1076
                case 'sco':
1077
                    $filePath = $scormPath.'/'.$item->getPath();
1078
                    if (file_exists($filePath)) {
1079
                        $filesToExport[] = [
1080
                            'title' => $item->getTitle(),
1081
                            'path' => $filePath,
1082
                        ];
1083
                    }
1084
                    break;
1085
1086
                case 'dir':
1087
                    $filesToExport[] = [
1088
                        'title' => $item->getTitle(),
1089
                        'path' => null,
1090
                    ];
1091
                    break;
1092
            }
1093
        }
1094
1095
        $pdf = new PDF();
1096
1097
        return $pdf->html_to_pdf(
1098
            $filesToExport,
1099
            $lp->getTitle(),
1100
            $courseCode,
1101
            true,
1102
            true,
1103
            true,
1104
            $lp->getTitle()
1105
        );
1106
    }
1107
}
1108