DocumentManager   F
last analyzed

Complexity

Total Complexity 446

Size/Duplication

Total Lines 3425
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1830
dl 0
loc 3425
rs 0.8
c 1
b 0
f 0
wmc 446

50 Methods

Rating   Name   Duplication   Size   Complexity  
A attach_gradebook_certificate() 0 19 4
A generateAudioJavascript() 0 12 1
A create_directory_certificate_in_course() 0 19 3
A get_default_certificate_id() 0 28 5
A generateAudioPreview() 0 7 1
A replaceUrlWithNewCourseCode() 0 36 6
B replace_user_info_into_html() 0 61 7
A get_document_id_of_directory_certificate() 0 10 1
D parse_HTML_attributes() 0 82 21
A generateNewUrlForCourseResource() 0 10 1
A enough_space() 0 14 3
A get_course_quota() 0 20 5
A generateMediaPreview() 0 18 4
A get_document_id() 0 26 5
A getSessionFolderFilters() 0 13 3
D get_text_content() 0 72 20
B is_visible() 0 78 7
A remove_attach_certificate() 0 23 6
A displaySimpleQuota() 0 10 1
F file_send_for_download() 0 139 34
B get_all_document_folders() 0 56 6
A setDocumentAsTemplate() 0 21 1
C get_document_preview() 0 127 11
F get_resources_from_source_html() 0 364 66
B smartReadFile() 0 54 11
F get_document_data_by_id() 0 101 19
B file_get_mime_type() 0 214 4
A is_certificate_mode() 0 11 5
A delete_document_from_search_engine() 0 20 3
F get_all_info_to_certificate() 0 161 14
B delete_document() 0 61 10
A getDocumentDefaultVisibility() 0 22 6
A createDefaultAudioFolder() 0 7 2
A get_system_folders() 0 12 1
F index_document() 0 220 31
A getProtectedFolderFromStudent() 0 8 1
B is_folder_to_avoid() 0 49 10
D build_document_icon_tag() 0 104 24
A isBasicCourseFolder() 0 6 1
D addDocument() 0 117 17
B generateDefaultCertificate() 0 83 9
A getUniqueFileName() 0 22 2
A documentExists() 0 43 3
A folderExists() 0 40 3
A addSuffixToFileName() 0 17 4
C syncResourceLinkParentForContext() 0 53 16
A getDocumentSuffix() 0 8 3
A findResourceLinkForContext() 0 45 5
C build_directory_selector() 0 107 17
A fixDocumentName() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like DocumentManager 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 DocumentManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Course;
6
use Chamilo\CoreBundle\Entity\ResourceLink;
7
use Chamilo\CoreBundle\Entity\ResourceNode;
8
use Chamilo\CoreBundle\Entity\Session;
9
use Chamilo\CoreBundle\Enums\ObjectIcon;
10
use Chamilo\CoreBundle\Framework\Container;
11
use Chamilo\CourseBundle\Entity\CDocument;
12
use Chamilo\CourseBundle\Entity\CGroup;
13
use Chamilo\CourseBundle\Repository\CDocumentRepository;
14
use Doctrine\ORM\EntityManagerInterface;
15
16
/**
17
 *  Class DocumentManager
18
 *  This is the document library for Chamilo.
19
 *  It is / will be used to provide a service layer to all document-using tools.
20
 *  and eliminate code duplication fro group documents, scorm documents, main documents.
21
 *  Include/require it in your code to use its functionality.
22
 */
23
class DocumentManager
24
{
25
    /**
26
     * @param string $course_code
27
     *
28
     * @return int the document folder quota for the current course in megabytes
29
     *             or the default quota
30
     */
31
    public static function get_course_quota($course_code = null)
32
    {
33
        if (empty($course_code)) {
34
            $course_info = api_get_course_info();
35
        } else {
36
            $course_info = api_get_course_info($course_code);
37
        }
38
39
        $course_quota = null;
40
        if (empty($course_info)) {
41
            return DEFAULT_DOCUMENT_QUOTA;
42
        } else {
43
            $course_quota = $course_info['disk_quota'];
44
        }
45
        if (is_null($course_quota) || empty($course_quota)) {
46
            // Course table entry for quota was null, then use default value
47
            $course_quota = DEFAULT_DOCUMENT_QUOTA;
48
        }
49
50
        return $course_quota;
51
    }
52
53
    /**
54
     * Get the content type of a file by checking the extension
55
     * We could use mime_content_type() with php-versions > 4.3,
56
     * but this doesn't work as it should on Windows installations.
57
     *
58
     * @param string $filename or boolean TRUE to return complete array
59
     *
60
     * @author ? first version
61
     * @author Bert Vanderkimpen
62
     *
63
     * @return string
64
     */
65
    public static function file_get_mime_type($filename)
66
    {
67
        // All MIME types in an array (from 1.6, this is the authorative source)
68
        // Please, keep this alphabetical if you add something to this list!
69
        $mimeTypes = [
70
            'ai' => 'application/postscript',
71
            'aif' => 'audio/x-aiff',
72
            'aifc' => 'audio/x-aiff',
73
            'aiff' => 'audio/x-aiff',
74
            'asf' => 'video/x-ms-asf',
75
            'asc' => 'text/plain',
76
            'au' => 'audio/basic',
77
            'avi' => 'video/x-msvideo',
78
            'bcpio' => 'application/x-bcpio',
79
            'bin' => 'application/octet-stream',
80
            'bmp' => 'image/bmp',
81
            'cdf' => 'application/x-netcdf',
82
            'class' => 'application/octet-stream',
83
            'cpio' => 'application/x-cpio',
84
            'cpt' => 'application/mac-compactpro',
85
            'csh' => 'application/x-csh',
86
            'css' => 'text/css',
87
            'dcr' => 'application/x-director',
88
            'dir' => 'application/x-director',
89
            'djv' => 'image/vnd.djvu',
90
            'djvu' => 'image/vnd.djvu',
91
            'dll' => 'application/octet-stream',
92
            'dmg' => 'application/x-diskcopy',
93
            'dms' => 'application/octet-stream',
94
            'doc' => 'application/msword',
95
            'docm' => 'application/vnd.ms-word.document.macroEnabled.12',
96
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
97
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
98
            'dvi' => 'application/x-dvi',
99
            'dwg' => 'application/vnd.dwg',
100
            'dwf' => 'application/vnd.dwf',
101
            'dxf' => 'application/vnd.dxf',
102
            'dxr' => 'application/x-director',
103
            'eps' => 'application/postscript',
104
            'epub' => 'application/epub+zip',
105
            'etx' => 'text/x-setext',
106
            'exe' => 'application/octet-stream',
107
            'ez' => 'application/andrew-inset',
108
            'flv' => 'video/flv',
109
            'gif' => 'image/gif',
110
            'gtar' => 'application/x-gtar',
111
            'gz' => 'application/x-gzip',
112
            'hdf' => 'application/x-hdf',
113
            'hqx' => 'application/mac-binhex40',
114
            'htm' => 'text/html',
115
            'html' => 'text/html',
116
            'ice' => 'x-conference-xcooltalk',
117
            'ief' => 'image/ief',
118
            'iges' => 'model/iges',
119
            'igs' => 'model/iges',
120
            'jar' => 'application/java-archiver',
121
            'jpe' => 'image/jpeg',
122
            'jpeg' => 'image/jpeg',
123
            'jpg' => 'image/jpeg',
124
            'js' => 'application/x-javascript',
125
            'kar' => 'audio/midi',
126
            'lam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
127
            'latex' => 'application/x-latex',
128
            'lha' => 'application/octet-stream',
129
            'log' => 'text/plain',
130
            'lzh' => 'application/octet-stream',
131
            'm1a' => 'audio/mpeg',
132
            'm2a' => 'audio/mpeg',
133
            'm3u' => 'audio/x-mpegurl',
134
            'man' => 'application/x-troff-man',
135
            'me' => 'application/x-troff-me',
136
            'mesh' => 'model/mesh',
137
            'mid' => 'audio/midi',
138
            'midi' => 'audio/midi',
139
            'mov' => 'video/quicktime',
140
            'movie' => 'video/x-sgi-movie',
141
            'mp2' => 'audio/mpeg',
142
            'mp3' => 'audio/mpeg',
143
            'mp4' => 'video/mp4',
144
            'mpa' => 'audio/mpeg',
145
            'mpe' => 'video/mpeg',
146
            'mpeg' => 'video/mpeg',
147
            'mpg' => 'video/mpeg',
148
            'mpga' => 'audio/mpeg',
149
            'ms' => 'application/x-troff-ms',
150
            'msh' => 'model/mesh',
151
            'mxu' => 'video/vnd.mpegurl',
152
            'nc' => 'application/x-netcdf',
153
            'oda' => 'application/oda',
154
            'oga' => 'audio/ogg',
155
            'ogg' => 'application/ogg',
156
            'ogx' => 'application/ogg',
157
            'ogv' => 'video/ogg',
158
            'pbm' => 'image/x-portable-bitmap',
159
            'pct' => 'image/pict',
160
            'pdb' => 'chemical/x-pdb',
161
            'pdf' => 'application/pdf',
162
            'pgm' => 'image/x-portable-graymap',
163
            'pgn' => 'application/x-chess-pgn',
164
            'pict' => 'image/pict',
165
            'png' => 'image/png',
166
            'pnm' => 'image/x-portable-anymap',
167
            'potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
168
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
169
            'pps' => 'application/vnd.ms-powerpoint',
170
            'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
171
            'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
172
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
173
            'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
174
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
175
            'ppm' => 'image/x-portable-pixmap',
176
            'ppt' => 'application/vnd.ms-powerpoint',
177
            'ps' => 'application/postscript',
178
            'qt' => 'video/quicktime',
179
            'ra' => 'audio/x-realaudio',
180
            'ram' => 'audio/x-pn-realaudio',
181
            'rar' => 'image/x-rar-compressed',
182
            'ras' => 'image/x-cmu-raster',
183
            'rgb' => 'image/x-rgb',
184
            'rm' => 'audio/x-pn-realaudio',
185
            'roff' => 'application/x-troff',
186
            'rpm' => 'audio/x-pn-realaudio-plugin',
187
            'rtf' => 'text/rtf',
188
            'rtx' => 'text/richtext',
189
            'sgm' => 'text/sgml',
190
            'sgml' => 'text/sgml',
191
            'sh' => 'application/x-sh',
192
            'shar' => 'application/x-shar',
193
            'silo' => 'model/mesh',
194
            'sib' => 'application/X-Sibelius-Score',
195
            'sit' => 'application/x-stuffit',
196
            'skd' => 'application/x-koan',
197
            'skm' => 'application/x-koan',
198
            'skp' => 'application/x-koan',
199
            'skt' => 'application/x-koan',
200
            'smi' => 'application/smil',
201
            'smil' => 'application/smil',
202
            'snd' => 'audio/basic',
203
            'so' => 'application/octet-stream',
204
            'spl' => 'application/x-futuresplash',
205
            'src' => 'application/x-wais-source',
206
            'sv4cpio' => 'application/x-sv4cpio',
207
            'sv4crc' => 'application/x-sv4crc',
208
            'svf' => 'application/vnd.svf',
209
            'svg' => 'image/svg+xml',
210
            //'svgz' => 'image/svg+xml',
211
            'swf' => 'application/x-shockwave-flash',
212
            'sxc' => 'application/vnd.sun.xml.calc',
213
            'sxi' => 'application/vnd.sun.xml.impress',
214
            'sxw' => 'application/vnd.sun.xml.writer',
215
            't' => 'application/x-troff',
216
            'tar' => 'application/x-tar',
217
            'tcl' => 'application/x-tcl',
218
            'tex' => 'application/x-tex',
219
            'texi' => 'application/x-texinfo',
220
            'texinfo' => 'application/x-texinfo',
221
            'tga' => 'image/x-targa',
222
            'tif' => 'image/tif',
223
            'tiff' => 'image/tiff',
224
            'tr' => 'application/x-troff',
225
            'tsv' => 'text/tab-seperated-values',
226
            'txt' => 'text/plain',
227
            'ustar' => 'application/x-ustar',
228
            'vcd' => 'application/x-cdlink',
229
            'vrml' => 'model/vrml',
230
            'wav' => 'audio/x-wav',
231
            'wbmp' => 'image/vnd.wap.wbmp',
232
            'wbxml' => 'application/vnd.wap.wbxml',
233
            'webp' => 'image/webp',
234
            'wml' => 'text/vnd.wap.wml',
235
            'wmlc' => 'application/vnd.wap.wmlc',
236
            'wmls' => 'text/vnd.wap.wmlscript',
237
            'wmlsc' => 'application/vnd.wap.wmlscriptc',
238
            'wma' => 'audio/x-ms-wma',
239
            'wmv' => 'video/x-ms-wmv',
240
            'wrl' => 'model/vrml',
241
            'xbm' => 'image/x-xbitmap',
242
            'xht' => 'application/xhtml+xml',
243
            'xhtml' => 'application/xhtml+xml',
244
            'xls' => 'application/vnd.ms-excel',
245
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
246
            'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
247
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
248
            'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
249
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
250
            'xml' => 'text/xml',
251
            'xpm' => 'image/x-xpixmap',
252
            'xsl' => 'text/xml',
253
            'xwd' => 'image/x-windowdump',
254
            'xyz' => 'chemical/x-xyz',
255
            'zip' => 'application/zip',
256
        ];
257
258
        if (true === $filename) {
259
            return $mimeTypes;
260
        }
261
262
        // Get the extension of the file
263
        $extension = explode('.', $filename);
264
265
        // $filename will be an array if a . was found
266
        if (is_array($extension)) {
267
            $extension = strtolower($extension[count($extension) - 1]);
268
        } else {
269
            //file without extension
270
            $extension = 'empty';
271
        }
272
273
        //if the extension is found, return the content type
274
        if (isset($mimeTypes[$extension])) {
275
            return $mimeTypes[$extension];
276
        }
277
278
        return 'application/octet-stream';
279
    }
280
281
    /**
282
     * This function smart streams a file to the client using HTTP headers.
283
     *
284
     * @param string $fullFilename The full path of the file to be sent
285
     * @param string $filename     The name of the file as shown to the client
286
     * @param string $contentType  The MIME type of the file
287
     *
288
     * @return bool false if file doesn't exist, true if stream succeeded
289
     */
290
    public static function smartReadFile($fullFilename, $filename, $contentType = 'application/octet-stream')
291
    {
292
        if (!file_exists($fullFilename)) {
293
            header("HTTP/1.1 404 Not Found");
294
295
            return false;
296
        }
297
298
        $size = filesize($fullFilename);
299
        $time = date('r', filemtime($fullFilename));
300
301
        $fm = @fopen($fullFilename, 'rb');
302
        if (!$fm) {
0 ignored issues
show
introduced by
$fm is of type false|resource, thus it always evaluated to false.
Loading history...
303
            header("HTTP/1.1 505 Internal server error");
304
305
            return false;
306
        }
307
308
        $begin = 0;
309
        $end = $size - 1;
310
311
        if (isset($_SERVER['HTTP_RANGE'])) {
312
            if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
313
                $begin = intval($matches[1]);
314
                if (!empty($matches[2])) {
315
                    $end = intval($matches[2]);
316
                }
317
            }
318
        }
319
320
        if (isset($_SERVER['HTTP_RANGE'])) {
321
            header('HTTP/1.1 206 Partial Content');
322
        } else {
323
            header('HTTP/1.1 200 OK');
324
        }
325
326
        header("Content-Type: $contentType");
327
        header('Cache-Control: public, must-revalidate, max-age=0');
328
        header('Pragma: no-cache');
329
        header('Accept-Ranges: bytes');
330
        header('Content-Length:'.(($end - $begin) + 1));
331
        if (isset($_SERVER['HTTP_RANGE'])) {
332
            header("Content-Range: bytes $begin-$end/$size");
333
        }
334
        header("Content-Disposition: inline; filename=$filename");
335
        header("Content-Transfer-Encoding: binary");
336
        header("Last-Modified: $time");
337
338
        $cur = $begin;
339
        fseek($fm, $begin, 0);
340
341
        while (!feof($fm) && $cur <= $end && (0 == connection_status())) {
342
            echo fread($fm, min(1024 * 16, ($end - $cur) + 1));
343
            $cur += 1024 * 16;
344
        }
345
    }
346
347
    /**
348
     * This function streams a file to the client.
349
     *
350
     * @param string $full_file_name
351
     * @param bool   $forced              Whether to force the browser to download the file
352
     * @param string $name
353
     * @param bool   $fixLinksHttpToHttps change file content from http to https
354
     * @param array  $extraHeaders        Additional headers to be sent
355
     *
356
     * @return false if file doesn't exist, true if stream succeeded
357
     */
358
    public static function file_send_for_download(
359
        $full_file_name,
360
        $forced = false,
361
        $name = '',
362
        $fixLinksHttpToHttps = false,
363
        $extraHeaders = []
364
    ) {
365
        session_write_close(); //we do not need write access to session anymore
366
        if (!is_file($full_file_name)) {
367
            return false;
368
        }
369
        $filename = '' == $name ? basename($full_file_name) : api_replace_dangerous_char($name);
370
        $len = filesize($full_file_name);
371
        // Fixing error when file name contains a ","
372
        $filename = str_replace(',', '', $filename);
373
        $sendFileHeaders = ('true' === api_get_setting('document.enable_x_sendfile_headers'));
374
375
        // Allows chrome to make videos and audios seekable
376
        header('Accept-Ranges: bytes');
377
        if (!empty($extraHeaders)) {
378
            foreach ($extraHeaders as $name => $value) {
379
                //TODO: add restrictions to allowed headers?
380
                header($name.': '.$value);
381
            }
382
        }
383
384
        if ($forced) {
385
            // Force the browser to save the file instead of opening it
386
            if (isset($sendFileHeaders) &&
387
                !empty($sendFileHeaders)) {
388
                header("X-Sendfile: $filename");
389
            }
390
391
            header('Content-type: application/octet-stream');
392
            header('Content-length: '.$len);
393
            if (preg_match("/MSIE 5.5/", $_SERVER['HTTP_USER_AGENT'])) {
394
                header('Content-Disposition: filename= '.$filename);
395
            } else {
396
                header('Content-Disposition: attachment; filename= '.$filename);
397
            }
398
            if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
399
                header('Pragma: ');
400
                header('Cache-Control: ');
401
                header('Cache-Control: public'); // IE cannot download from sessions without a cache
402
            }
403
            header('Content-Description: '.$filename);
404
            header('Content-Transfer-Encoding: binary');
405
406
            if (function_exists('ob_end_clean') && ob_get_length()) {
407
                // Use ob_end_clean() to avoid weird buffering situations
408
                // where file is sent broken/incomplete for download
409
                ob_end_clean();
410
            }
411
412
            $res = fopen($full_file_name, 'r');
413
            fpassthru($res);
414
415
            return true;
416
        } else {
417
            // no forced download, just let the browser decide what to do according to the mimetype
418
            $lpFixedEncoding = 'true' === api_get_setting('lp.lp_fixed_encoding');
419
420
            // Commented to let courses content to be cached in order to improve performance:
421
            //header('Expires: Wed, 01 Jan 1990 00:00:00 GMT');
422
            //header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
423
424
            // Commented to avoid double caching declaration when playing with IE and HTTPS
425
            //header('Cache-Control: no-cache, must-revalidate');
426
            //header('Pragma: no-cache');
427
428
            $contentType = self::file_get_mime_type($filename);
429
430
            switch ($contentType) {
431
                case 'text/html':
432
                    if (isset($lpFixedEncoding) && 'true' === $lpFixedEncoding) {
433
                        $contentType .= '; charset=UTF-8';
434
                    } else {
435
                        $encoding = @api_detect_encoding_html(file_get_contents($full_file_name));
436
                        if (!empty($encoding)) {
437
                            $contentType .= '; charset='.$encoding;
438
                        }
439
                    }
440
                    break;
441
                case 'text/plain':
442
                    if (isset($lpFixedEncoding) && 'true' === $lpFixedEncoding) {
443
                        $contentType .= '; charset=UTF-8';
444
                    } else {
445
                        $encoding = @api_detect_encoding(strip_tags(file_get_contents($full_file_name)));
446
                        if (!empty($encoding)) {
447
                            $contentType .= '; charset='.$encoding;
448
                        }
449
                    }
450
                    break;
451
                case 'video/mp4':
452
                case 'audio/mpeg':
453
                case 'audio/mp4':
454
                case 'audio/ogg':
455
                case 'audio/webm':
456
                case 'audio/wav':
457
                case 'video/ogg':
458
                case 'video/webm':
459
                    self::smartReadFile($full_file_name, $filename, $contentType);
460
                    exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
461
                case 'application/vnd.dwg':
462
                case 'application/vnd.dwf':
463
                    header('Content-type: application/octet-stream');
464
                    break;
465
            }
466
467
            header('Content-type: '.$contentType);
468
            header('Content-Length: '.$len);
469
            $userAgent = strtolower($_SERVER['HTTP_USER_AGENT']);
470
471
            if (strpos($userAgent, 'msie')) {
472
                header('Content-Disposition: ; filename= '.$filename);
473
            } else {
474
                //header('Content-Disposition: inline');
475
                header('Content-Disposition: inline;');
476
            }
477
478
            if ($fixLinksHttpToHttps) {
479
                $content = file_get_contents($full_file_name);
480
                $content = str_replace(
481
                    ['http%3A%2F%2F', 'http://'],
482
                    ['https%3A%2F%2F', 'https://'],
483
                    $content
484
                );
485
                echo $content;
486
            } else {
487
                if (function_exists('ob_end_clean') && ob_get_length()) {
488
                    // Use ob_end_clean() to avoid weird buffering situations
489
                    // where file is sent broken/incomplete for download
490
                    ob_end_clean();
491
                }
492
493
                readfile($full_file_name);
494
            }
495
496
            return true;
497
        }
498
    }
499
500
    /**
501
     * Session folder filters.
502
     *
503
     * @param string $path
504
     * @param int    $sessionId
505
     *
506
     * @return string|null
507
     */
508
    public static function getSessionFolderFilters($path, $sessionId)
509
    {
510
        $sessionId = (int) $sessionId;
511
        $condition = null;
512
513
        if (!empty($sessionId)) {
514
            // Chat folder filter
515
            if ('/chat_files' == $path) {
516
                $condition .= " AND (docs.session_id = '$sessionId') ";
517
            }
518
        }
519
520
        return $condition;
521
    }
522
523
    /**
524
     * Gets the paths of all folders in a course
525
     * can show all folders (except for the deleted ones) or only visible ones.
526
     *
527
     * @param array  $courseInfo
528
     * @param int    $groupIid          iid
529
     * @param bool   $can_see_invisible
530
     * @param bool   $getInvisibleList
531
     * @param string $path              current path
532
     *
533
     * @return array with paths
534
     */
535
    public static function get_all_document_folders(
536
        $courseInfo,
537
        $groupIid = 0,
538
        $can_see_invisible = false,
539
        $getInvisibleList = false,
540
        $path = ''
541
    ) {
542
        if (empty($courseInfo)) {
543
            return [];
544
        }
545
546
        $sessionId = api_get_session_id();
547
548
        /** @var Course|null $course */
549
        $course = api_get_course_entity();
550
        if (!$course instanceof Course) {
551
            return [];
552
        }
553
554
        /** @var Session|null $session */
555
        $session = null;
556
        if (!empty($sessionId)) {
557
            $session = Container::$container
558
                ->get('doctrine')
559
                ->getRepository(Session::class)
560
                ->find($sessionId);
561
        }
562
563
        /** @var CGroup|null $group */
564
        $group = null;
565
        if (!empty($groupIid)) {
566
            $group = Container::$container
567
                ->get('doctrine')
568
                ->getRepository(CGroup::class)
569
                ->find($groupIid);
570
        }
571
572
        /** @var CDocumentRepository $docRepo */
573
        $docRepo = Container::$container->get('doctrine')
574
            ->getRepository(CDocument::class);
575
576
        $folders = $docRepo->getAllFoldersForContext(
577
            $course,
578
            $session,
579
            $group,
580
            (bool) $can_see_invisible,
581
            (bool) $getInvisibleList
582
        );
583
584
        if (empty($folders)) {
585
            // Keep backward compatibility: some legacy callers expect "false"
586
            // when there are no visible folders.
587
            return false;
588
        }
589
590
        return $folders;
591
    }
592
593
    /**
594
     * This deletes a document by changing visibility to 2, renaming it to filename_DELETED_#id
595
     * Files/folders that are inside a deleted folder get visibility 2.
596
     *
597
     * @param array  $_course
598
     * @param string $path          Path stored in the database
599
     * @param string $base_work_dir Path to the documents folder (if not defined, $documentId must be used)
600
     * @param int    $sessionId     The ID of the session, if any
601
     * @param int    $documentId    The document id, if available
602
     * @param int    $groupId       iid
603
     *
604
     * @return bool true/false
605
     *
606
     * @todo now only files/folders in a folder get visibility 2, we should rename them too.
607
     * @todo We should be able to get rid of this later when using only documentId (check further usage)
608
     */
609
    public static function delete_document(
610
        $_course,
611
        $path = null,
612
        $base_work_dir = null,
613
        $sessionId = null,
614
        $documentId = null,
615
        $groupId = 0
616
    ) {
617
        $groupId = (int) $groupId;
618
        if (empty($groupId)) {
619
            $groupId = api_get_group_id();
620
        }
621
622
        $sessionId = (int) $sessionId;
623
        if (empty($sessionId)) {
624
            $sessionId = api_get_session_id();
625
        }
626
627
        $course_id = $_course['real_id'];
628
629
        if (empty($course_id)) {
630
            return false;
631
        }
632
633
        if (empty($documentId)) {
634
            $documentId = self::get_document_id($_course, $path, $sessionId);
635
            $docInfo = self::get_document_data_by_id(
636
                $documentId,
637
                $_course['code'],
638
                false,
639
                $sessionId
640
            );
641
            $path = $docInfo['path'];
642
        } else {
643
            $docInfo = self::get_document_data_by_id(
644
                $documentId,
645
                $_course['code'],
646
                false,
647
                $sessionId
648
            );
649
            if (empty($docInfo)) {
650
                return false;
651
            }
652
            $path = $docInfo['path'];
653
        }
654
655
        $documentId = (int) $documentId;
656
657
        if (empty($path) || empty($docInfo) || empty($documentId)) {
658
            return false;
659
        }
660
661
        $repo = Container::getDocumentRepository();
662
        $document = $repo->find($docInfo['iid']);
663
        if ($document) {
664
            $repo->hardDelete($document);
665
666
            return true;
667
        }
668
669
        return false;
670
    }
671
672
    /**
673
     * Removes documents from search engine database.
674
     *
675
     * @param string $course_id   Course code
676
     * @param int    $document_id Document id to delete
677
     */
678
    public static function delete_document_from_search_engine($course_id, $document_id)
679
    {
680
        // remove from search engine if enabled
681
        if ('true' === api_get_setting('search_enabled')) {
682
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
683
            $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
684
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_DOCUMENT, $document_id);
685
            $res = Database::query($sql);
686
            if (Database::num_rows($res) > 0) {
687
                $row2 = Database::fetch_array($res);
688
                $di = new ChamiloIndexer();
689
                $di->remove_document($row2['search_did']);
690
            }
691
            $sql = 'DELETE FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
692
            $sql = sprintf($sql, $tbl_se_ref, $course_id, TOOL_DOCUMENT, $document_id);
693
            Database::query($sql);
694
695
            // remove terms from db
696
            require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
697
            delete_all_values_for_item($course_id, TOOL_DOCUMENT, $document_id);
698
        }
699
    }
700
701
    /**
702
     * Gets the id of a document with a given path.
703
     *
704
     * @param array  $courseInfo
705
     * @param string $path
706
     * @param int    $sessionId
707
     *
708
     * @return int id of document / false if no doc found
709
     */
710
    public static function get_document_id($courseInfo, $path, $sessionId = 0)
711
    {
712
        $table = Database::get_course_table(TABLE_DOCUMENT);
713
        $courseId = $courseInfo['real_id'];
714
715
        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
716
        $sessionCondition = api_get_session_condition($sessionId, true);
717
718
        $path = Database::escape_string($path);
719
        if (!empty($courseId) && !empty($path)) {
720
            $sql = "SELECT iid FROM $table
721
                    WHERE
722
                        c_id = $courseId AND
723
                        path LIKE BINARY '$path'
724
                        $sessionCondition
725
                    LIMIT 1";
726
727
            $result = Database::query($sql);
728
            if (Database::num_rows($result)) {
729
                $row = Database::fetch_array($result);
730
731
                return (int) $row['iid'];
732
            }
733
        }
734
735
        return false;
736
    }
737
738
    /**
739
     * Gets the document data with a given id.
740
     *
741
     * @param int    $id            Document Id (id field in c_document table)
742
     * @param string $course_code   Course code
743
     * @param bool   $load_parents  load folder parents
744
     * @param int    $session_id    The session ID,
745
     *                              0 if requires context *out of* session, and null to use global context
746
     * @param bool   $ignoreDeleted
747
     *
748
     * @deprecated  use $repo->find()
749
     *
750
     * @return array document content
751
     */
752
    public static function get_document_data_by_id(
753
        int $id,
754
        string $course_code,
755
        bool $load_parents = false,
756
        int $session_id = null,
757
        bool $ignoreDeleted = false
758
    ): bool|array {
759
        $course_info = api_get_course_info($course_code);
760
        if (empty($course_info)) {
761
            return false;
762
        }
763
764
        $course_id  = (int) $course_info['real_id'];
765
        $session_id = empty($session_id) ? api_get_session_id() : $session_id;
766
767
        $TABLE_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
768
        $id = (int) $id;
769
770
        $sql = "SELECT * FROM $TABLE_DOCUMENT WHERE iid = $id";
771
        $result = Database::query($sql);
772
        if (!$result || Database::num_rows($result) != 1) {
773
            return false;
774
        }
775
776
        $row = Database::fetch_assoc($result);
777
778
        try {
779
            /** @var EntityManagerInterface $em */
780
            $em = \Database::getManager();
781
782
            /** @var Course|null $courseEntity */
783
            $courseEntity = $em->getRepository(Course::class)
784
                ->findOneBy(['code' => $course_code]);
785
786
            if ($courseEntity) {
787
                /** @var CDocumentRepository $docRepo */
788
                $docRepo = Container::getDocumentRepository();
789
790
                $qb = $docRepo->getResourcesByCourse($courseEntity,);
791
                $qb->andWhere('resource.iid = :iid')->setParameter('iid', $id);
792
                /** @var CDocument[] $docs */
793
                $docs = $qb->getQuery()->getResult();
794
                $doc  = $docs[0] ?? null;
795
796
                if ($doc) {
797
                    $fullPath  = (string) $doc->getFullPath();
798
                    $filetype  = (string) $doc->getFiletype();
799
                    $title     = (string) $doc->getTitle();
800
                    $size      = 0;
801
802
                    $node = $doc->getResourceNode();
803
                    if ($node && $filetype === 'file') {
804
                        $file = $node->getFirstResourceFile();
805
                        if ($file) {
806
                            $size = (int) $file->getSize();
807
                        }
808
                    } elseif ($node && $filetype === 'folder') {
809
                        $size = (int) $docRepo->getFolderSize($node, $courseEntity, null);
810
                    }
811
812
                    $row['path']       = $row['path']       ?? ('document/' . ltrim($fullPath, '/'));
813
                    $row['full_path']  = $row['full_path']  ?? $row['path'];
814
                    $row['filetype']   = $row['filetype']   ?? $filetype;
815
                    $row['file_type']  = $row['file_type']  ?? $filetype;
816
                    $row['title']      = $row['title']      ?? $title;
817
                    $row['size']       = $row['size']       ?? $size;
818
                    if (empty($row['parent_id'])) {
819
                        $parentNode = $node?->getParent();
820
                        if ($parentNode) {
821
                            $parentResource = $docRepo->getResourceByResourceNode($parentNode);
822
                            $row['parent_id'] = $parentResource?->getIid() ?? '0';
823
                        } else {
824
                            $row['parent_id'] = '0';
825
                        }
826
                    }
827
                }
828
            }
829
        } catch (\Throwable $e) {
830
            error_log('DOC_COMPAT: Fail CDocument iid='.$id.' -> '.$e->getMessage());
831
        }
832
833
        $row['url']          = api_get_path(WEB_CODE_PATH).'document/showinframes.php?id='.$id;
834
        $row['document_url'] = api_get_path(WEB_CODE_PATH).'document/document.php?id='.$id;
835
        $row['basename']     = $row['basename'] ?? $id;
836
837
        $parents = [];
838
        if ($load_parents) {
839
            $current_id = $row['parent_id'] ?? '0';
840
            $guard = 0;
841
            while ($current_id != '0' && $guard++ < 64) {
842
                $parent_data = self::get_document_data_by_id((int)$current_id, $course_code, false, $session_id);
843
                if (!$parent_data) {
844
                    break;
845
                }
846
                $parents[]  = $parent_data;
847
                $current_id = $parent_data['parent_id'] ?? '0';
848
            }
849
        }
850
        $row['parents'] = array_reverse($parents);
851
852
        return $row;
853
    }
854
855
    /**
856
     * Allow to set a specific document as a new template for CKeditor
857
     * for a particular user in a particular course.
858
     *
859
     * @param string $title
860
     * @param string $description
861
     * @param int    $document_id_for_template the document id
862
     * @param int    $courseId
863
     * @param int    $user_id
864
     * @param string $image
865
     *
866
     * @return bool
867
     */
868
    public static function setDocumentAsTemplate(
869
        $title,
870
        $description,
871
        $document_id_for_template,
872
        $courseId,
873
        $user_id,
874
        $image
875
    ) {
876
        // Database table definition
877
        $table_template = Database::get_main_table(TABLE_MAIN_TEMPLATES);
878
        $params = [
879
            'title' => $title,
880
            'description' => $description,
881
            'c_id' => $courseId,
882
            'user_id' => $user_id,
883
            'ref_doc' => $document_id_for_template,
884
            'image' => $image,
885
        ];
886
        Database::insert($table_template, $params);
887
888
        return true;
889
    }
890
891
    /**
892
     * Check document visibility.
893
     *
894
     * @param string $doc_path the relative complete path of the document
895
     * @param array  $course   the _course array info of the document's course
896
     * @param int
897
     * @param string
898
     *
899
     * @return bool
900
     */
901
    public static function is_visible(
902
        $doc_path,
903
        $course,
904
        $session_id = 0,
905
        $file_type = 'file'
906
    ) {
907
        $docTable = Database::get_course_table(TABLE_DOCUMENT);
908
909
        $course_id = $course['real_id'];
910
        // note the extra / at the end of doc_path to match every path in
911
        // the document table that is part of the document path
912
        $session_id = (int) $session_id;
913
        $condition = " AND d.session_id IN  ('$session_id', '0') ";
914
        // The " d.filetype='file' " let the user see a file even if the folder is hidden see #2198
915
916
        /*
917
          When using hotpotatoes files, a new html files are generated
918
          in the hotpotatoes folder to display the test.
919
          The genuine html file is copied to math4.htm(user_id).t.html
920
          Images files are not copied, and keep same name.
921
          To check the html file visibility, we don't have to check file math4.htm(user_id).t.html but file math4.htm
922
          In this case, we have to remove (user_id).t.html to check the visibility of the file
923
          For images, we just check the path of the image file.
924
925
          Exemple of hotpotatoes folder :
926
          A.jpg
927
          maths4-consigne.jpg
928
          maths4.htm
929
          maths4.htm1.t.html
930
          maths4.htm52.t.html
931
          maths4.htm654.t.html
932
          omega.jpg
933
          theta.jpg
934
         */
935
936
        if (strpos($doc_path, 'HotPotatoes_files') && preg_match("/\.t\.html$/", $doc_path)) {
937
            $doc_path = substr($doc_path, 0, strlen($doc_path) - 7 - strlen(api_get_user_id()));
938
        }
939
940
        if (!in_array($file_type, ['file', 'folder'])) {
941
            $file_type = 'file';
942
        }
943
        $doc_path = Database::escape_string($doc_path).'/';
944
945
        $sql = "SELECT iid
946
                FROM $docTable d
947
        		WHERE
948
        		    d.c_id  = $course_id AND
949
        		    $condition AND
950
        			filetype = '$file_type' AND
951
        			locate(concat(path,'/'), '$doc_path')=1
952
                ";
953
954
        $result = Database::query($sql);
955
        $is_visible = false;
956
        if (Database::num_rows($result) > 0) {
957
            $row = Database::fetch_assoc($result);
958
959
            $em = Database::getManager();
960
961
            $repo = $em->getRepository(CDocument::class);
962
            /** @var CDocument $document */
963
            $document = $repo->find($row['iid']);
964
            if (ResourceLink::VISIBILITY_PUBLISHED === $document->getVisibility()) {
965
                $is_visible = api_is_allowed_in_course() || api_is_platform_admin();
966
            }
967
        }
968
969
        /* improved protection of documents viewable directly through the url:
970
            incorporates the same protections of the course at the url of
971
            documents:
972
            access allowed for the whole world Open, access allowed for
973
            users registered on the platform Private access, document accessible
974
            only to course members (see the Users list), Completely closed;
975
            the document is only accessible to the course admin and
976
            teaching assistants.*/
977
        //return $_SESSION ['is_allowed_in_course'] || api_is_platform_admin();
978
        return $is_visible;
979
    }
980
981
    /**
982
     * Allow attach a certificate to a course.
983
     *
984
     * @todo move to certificate.lib.php
985
     *
986
     * @param int $courseId
987
     * @param int $document_id
988
     * @param int $sessionId
989
     */
990
    public static function attach_gradebook_certificate($courseId, $document_id, $sessionId = 0)
991
    {
992
        $tbl_category = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
993
        $sessionId = intval($sessionId);
994
        $courseId = (int) $courseId;
995
        if (empty($sessionId)) {
996
            $sessionId = api_get_session_id();
997
        }
998
999
        if (empty($sessionId)) {
1000
            $sql_session = 'AND (session_id = 0 OR isnull(session_id)) ';
1001
        } elseif ($sessionId > 0) {
1002
            $sql_session = 'AND session_id='.$sessionId;
1003
        } else {
1004
            $sql_session = '';
1005
        }
1006
        $sql = 'UPDATE '.$tbl_category.' SET document_id="'.intval($document_id).'"
1007
                WHERE c_id ="'.$courseId.'" '.$sql_session;
1008
        Database::query($sql);
1009
    }
1010
1011
    /**
1012
     * get the document id of default certificate.
1013
     *
1014
     * @todo move to certificate.lib.php
1015
     *
1016
     * @param int $courseId
1017
     * @param int $session_id
1018
     *
1019
     * @return int The default certificate id
1020
     */
1021
    public static function get_default_certificate_id($courseId, $session_id = 0)
1022
    {
1023
        $tbl_category = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
1024
        $session_id = (int) $session_id;
1025
        $courseId = (int) $courseId;
1026
        if (empty($session_id)) {
1027
            $session_id = api_get_session_id();
1028
        }
1029
1030
        if (empty($session_id)) {
1031
            $sql_session = 'AND (session_id = 0 OR isnull(session_id)) ';
1032
        } elseif ($session_id > 0) {
1033
            $sql_session = 'AND session_id='.$session_id;
1034
        } else {
1035
            $sql_session = '';
1036
        }
1037
1038
        $sql = 'SELECT document_id FROM '.$tbl_category.'
1039
                WHERE c_id ="'.$courseId.'" '.$sql_session;
1040
1041
        $rs = Database::query($sql);
1042
        $num = Database::num_rows($rs);
1043
        if (0 == $num) {
1044
            return null;
1045
        }
1046
        $row = Database::fetch_array($rs);
1047
1048
        return $row['document_id'];
1049
    }
1050
1051
    /**
1052
     * Allow replace user info in file html.
1053
     *
1054
     * @param int   $user_id
1055
     * @param array $courseInfo
1056
     * @param int   $sessionId
1057
     * @param bool  $is_preview
1058
     *
1059
     * @return array
1060
     */
1061
    public static function replace_user_info_into_html(
1062
        $user_id,
1063
        $courseInfo,
1064
        $sessionId,
1065
        $is_preview = false
1066
    ) {
1067
        $user_id = (int) $user_id;
1068
        $course_id = $courseInfo['real_id'];
1069
        $document_id = self::get_default_certificate_id($course_id, $sessionId);
1070
1071
        $my_content_html = null;
1072
        if ($document_id) {
1073
            $repo = Container::getDocumentRepository();
1074
            $doc = $repo->find($document_id);
1075
            $new_content = '';
1076
            $all_user_info = [];
1077
            if ($doc) {
1078
                try {
1079
                    // Validate if the document content is not empty
1080
                    $my_content_html = $repo->getResourceFileContent($doc);
1081
                    if (empty($my_content_html)) {
1082
                        throw new Exception("The document content is empty.");
1083
                    }
1084
1085
                    // Retrieve user information for the certificate
1086
                    $all_user_info = self::get_all_info_to_certificate(
1087
                        $user_id,
1088
                        $course_id,
1089
                        $is_preview
1090
                    );
1091
1092
                    // Ensure user info array is properly structured
1093
                    if (!isset($all_user_info[0]) || !isset($all_user_info[1])) {
1094
                        throw new Exception("Error retrieving user information for the certificate.");
1095
                    }
1096
1097
                    $info_to_be_replaced_in_content_html = $all_user_info[0];
1098
                    $info_to_replace_in_content_html = $all_user_info[1];
1099
1100
                    // Replace placeholders in the certificate template with user info
1101
                    $new_content = str_replace(
1102
                        $info_to_be_replaced_in_content_html,
1103
                        $info_to_replace_in_content_html,
1104
                        $my_content_html
1105
                    );
1106
                } catch (Exception $e) {
1107
                    error_log("Error in replace_user_info_into_html: " . $e->getMessage());
1108
                    return [
1109
                        'content' => '',
1110
                        'variables' => [],
1111
                    ];
1112
                }
1113
            }
1114
1115
            return [
1116
                'content' => $new_content,
1117
                'variables' => $all_user_info,
1118
            ];
1119
        }
1120
1121
        return [];
1122
    }
1123
1124
    /**
1125
     * Return all content to replace and all content to be replace.
1126
     *
1127
     * @param int  $user_id
1128
     * @param bool $is_preview
1129
     *
1130
     * @return array
1131
     */
1132
    public static function get_all_info_to_certificate($user_id, $courseId, $sessionId, $is_preview = false)
1133
    {
1134
        $info_list = [];
1135
        $user_id = (int) $user_id;
1136
        $sessionId = (int) $sessionId;
1137
        $course_info = api_get_course_info_by_id($courseId);
1138
        $courseCode = $course_info['code'];
1139
1140
        // Portal info
1141
        $organization_name = api_get_setting('Institution');
1142
        $portal_name = api_get_setting('siteName');
1143
1144
        // Extra user data information
1145
        $extra_user_info_data = UserManager::get_extra_user_data(
1146
            $user_id,
1147
            false,
1148
            false,
1149
            false,
1150
            true
1151
        );
1152
1153
        // get extra fields
1154
        $extraField = new ExtraField('user');
1155
        $extraFields = $extraField->get_all(['filter = ? AND visible_to_self = ?' => [1, 1]]);
1156
1157
        // Student information
1158
        $user_info = api_get_user_info($user_id);
1159
        $first_name = $user_info['firstname'];
1160
        $last_name = $user_info['lastname'];
1161
        $username = $user_info['username'];
1162
        $official_code = $user_info['official_code'];
1163
1164
        // Teacher information
1165
        $info_teacher_id = UserManager::get_user_id_of_course_admin_or_session_admin($course_info);
1166
        $teacher_info = api_get_user_info($info_teacher_id);
1167
        $teacher_first_name = $teacher_info['firstname'];
1168
        $teacher_last_name = $teacher_info['lastname'];
1169
1170
        // info gradebook certificate
1171
        $info_grade_certificate = UserManager::get_info_gradebook_certificate($course_info, $sessionId, $user_id);
1172
        $date_long_certificate = '';
1173
        $date_certificate = '';
1174
        $url = '';
1175
        if ($info_grade_certificate) {
1176
            $date_certificate = $info_grade_certificate['created_at'];
1177
            if (!empty($info_grade_certificate['path_certificate'])) {
1178
                $hash = pathinfo($info_grade_certificate['path_certificate'], PATHINFO_FILENAME);
1179
                $url = api_get_path(WEB_PATH) . 'certificates/' . $hash . '.html';
1180
            }
1181
        }
1182
        $date_no_time = api_convert_and_format_date(api_get_utc_datetime(), DATE_FORMAT_LONG_NO_DAY);
1183
        if (!empty($date_certificate)) {
1184
            $date_long_certificate = api_convert_and_format_date($date_certificate);
1185
            $date_no_time = api_convert_and_format_date($date_certificate, DATE_FORMAT_LONG_NO_DAY);
1186
        }
1187
1188
        if ($is_preview) {
1189
            $date_long_certificate = api_convert_and_format_date(api_get_utc_datetime());
1190
            $date_no_time = api_convert_and_format_date(api_get_utc_datetime(), DATE_FORMAT_LONG_NO_DAY);
1191
        }
1192
1193
        $externalStyleFile = api_get_path(SYS_CSS_PATH).'themes/'.api_get_visual_theme().'/certificate.css';
1194
        $externalStyle = '';
1195
        if (is_file($externalStyleFile)) {
1196
            $externalStyle = file_get_contents($externalStyleFile);
1197
        }
1198
        $timeInCourse = Tracking::get_time_spent_on_the_course($user_id, $course_info['real_id'], $sessionId);
1199
        $timeInCourse = api_time_to_hms($timeInCourse, ':', false, true);
1200
1201
        $timeInCourseInAllSessions = 0;
1202
        $sessions = SessionManager::get_session_by_course($course_info['real_id']);
1203
1204
        if (!empty($sessions)) {
1205
            foreach ($sessions as $session) {
1206
                $timeInCourseInAllSessions += Tracking::get_time_spent_on_the_course($user_id, $course_info['real_id'], $session['id']);
1207
            }
1208
        }
1209
        $timeInCourseInAllSessions = api_time_to_hms($timeInCourseInAllSessions, ':', false, true);
1210
1211
        $first = Tracking::get_first_connection_date_on_the_course($user_id, $course_info['real_id'], $sessionId, false);
1212
        $first = substr($first, 0, 10);
1213
        $last = Tracking::get_last_connection_date_on_the_course($user_id, $course_info, $sessionId, false);
1214
        $last = substr($last, 0, 10);
1215
1216
        if ($first === $last) {
1217
            $startDateAndEndDate = get_lang('From').' '.$first;
1218
        } else {
1219
            $startDateAndEndDate = sprintf(
1220
                get_lang('From %s to %s'),
1221
                $first,
1222
                $last
1223
            );
1224
        }
1225
        $courseDescription = new CourseDescription();
1226
        $description = $courseDescription->get_data_by_description_type(2, $course_info['real_id'], $sessionId);
1227
        $courseObjectives = '';
1228
        if ($description) {
1229
            $courseObjectives = $description['description_content'];
1230
        }
1231
1232
        // Replace content
1233
        $info_to_replace_in_content_html = [
1234
            $first_name,
1235
            $last_name,
1236
            $username,
1237
            $organization_name,
1238
            $portal_name,
1239
            $teacher_first_name,
1240
            $teacher_last_name,
1241
            $official_code,
1242
            $date_long_certificate,
1243
            $date_no_time,
1244
            $course_info['code'],
1245
            $course_info['name'],
1246
            isset($info_grade_certificate['grade']) ? $info_grade_certificate['grade'] : '',
1247
            $url,
1248
            '<a href="'.$url.'" target="_blank">'.get_lang('Online link to certificate').'</a>',
1249
            '((certificate_barcode))',
1250
            $externalStyle,
1251
            $timeInCourse,
1252
            $timeInCourseInAllSessions,
1253
            $startDateAndEndDate,
1254
            $courseObjectives,
1255
        ];
1256
1257
        $tags = [
1258
            '((user_firstname))',
1259
            '((user_lastname))',
1260
            '((user_username))',
1261
            '((gradebook_institution))',
1262
            '((gradebook_sitename))',
1263
            '((teacher_firstname))',
1264
            '((teacher_lastname))',
1265
            '((official_code))',
1266
            '((date_certificate))',
1267
            '((date_certificate_no_time))',
1268
            '((course_code))',
1269
            '((course_title))',
1270
            '((gradebook_grade))',
1271
            '((certificate_link))',
1272
            '((certificate_link_html))',
1273
            '((certificate_barcode))',
1274
            '((external_style))',
1275
            '((time_in_course))',
1276
            '((time_in_course_in_all_sessions))',
1277
            '((start_date_and_end_date))',
1278
            '((course_objectives))',
1279
        ];
1280
1281
        if (!empty($extraFields)) {
1282
            foreach ($extraFields as $extraField) {
1283
                $valueExtra = isset($extra_user_info_data[$extraField['variable']]) ? $extra_user_info_data[$extraField['variable']] : '';
1284
                $tags[] = '(('.strtolower($extraField['variable']).'))';
1285
                $info_to_replace_in_content_html[] = $valueExtra;
1286
            }
1287
        }
1288
1289
        $info_list[] = $tags;
1290
        $info_list[] = $info_to_replace_in_content_html;
1291
1292
        return $info_list;
1293
    }
1294
1295
    /**
1296
     * Remove default certificate.
1297
     *
1298
     * @param int $course_id              The course code
1299
     * @param int $default_certificate_id The document id of the default certificate
1300
     */
1301
    public static function remove_attach_certificate($course_id, $default_certificate_id)
1302
    {
1303
        if (empty($default_certificate_id)) {
1304
            return false;
1305
        }
1306
1307
        $default_certificate = self::get_default_certificate_id($course_id);
1308
        if ((int) $default_certificate == (int) $default_certificate_id) {
1309
            $tbl_category = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
1310
            $session_id = api_get_session_id();
1311
            if (0 == $session_id || is_null($session_id)) {
1312
                $sql_session = 'AND (session_id='.intval($session_id).' OR isnull(session_id)) ';
1313
            } elseif ($session_id > 0) {
1314
                $sql_session = 'AND session_id='.intval($session_id);
1315
            } else {
1316
                $sql_session = '';
1317
            }
1318
1319
            $sql = 'UPDATE '.$tbl_category.' SET document_id = null
1320
                    WHERE
1321
                        c_id = "'.Database::escape_string($course_id).'" AND
1322
                        document_id="'.$default_certificate_id.'" '.$sql_session;
1323
            Database::query($sql);
1324
        }
1325
    }
1326
1327
    /**
1328
     * Create directory certificate.
1329
     *
1330
     * @param array $courseInfo
1331
     */
1332
    public static function create_directory_certificate_in_course($courseInfo)
1333
    {
1334
        if (!empty($courseInfo)) {
1335
            $dir_name = '/certificates';
1336
            $post_dir_name = get_lang('Certificates');
1337
            $id = self::get_document_id_of_directory_certificate();
1338
            if (empty($id)) {
1339
                create_unexisting_directory(
1340
                    $courseInfo,
1341
                    api_get_user_id(),
1342
                    api_get_session_id(),
1343
                    0,
1344
                    0,
1345
                    '',
1346
                    $dir_name,
1347
                    $post_dir_name,
1348
                    null,
1349
                    false,
1350
                    false
1351
                );
1352
            }
1353
        }
1354
    }
1355
1356
    /**
1357
     * Get the document id of the directory certificate.
1358
     *
1359
     * @return int The document id of the directory certificate
1360
     *
1361
     * @todo move to certificate.lib.php
1362
     */
1363
    public static function get_document_id_of_directory_certificate()
1364
    {
1365
        $tbl_document = Database::get_course_table(TABLE_DOCUMENT);
1366
        $course_id = api_get_course_int_id();
1367
        $sql = "SELECT id FROM $tbl_document
1368
                WHERE c_id = $course_id AND path='/certificates' ";
1369
        $rs = Database::query($sql);
1370
        $row = Database::fetch_array($rs);
1371
1372
        return $row['id'];
1373
    }
1374
1375
    /**
1376
     * Check if a directory given is for certificate.
1377
     *
1378
     * @todo move to certificate.lib.php
1379
     *
1380
     * @param string $dir path of directory
1381
     *
1382
     * @return bool true if is a certificate or false otherwise
1383
     */
1384
    public static function is_certificate_mode($dir)
1385
    {
1386
        // I'm in the certification module?
1387
        $is_certificate_mode = false;
1388
        $is_certificate_array = explode('/', $dir);
1389
        array_shift($is_certificate_array);
1390
        if (isset($is_certificate_array[0]) && 'certificates' == $is_certificate_array[0]) {
1391
            $is_certificate_mode = true;
1392
        }
1393
1394
        return $is_certificate_mode || (isset($_GET['certificate']) && 'true' === $_GET['certificate']);
1395
    }
1396
1397
    /**
1398
     * Gets the list of included resources as a list of absolute or relative paths from a html file or string html
1399
     * This allows for a better SCORM export or replace urls inside content html from copy course
1400
     * The list will generally include pictures, flash objects, java applets, or any other
1401
     * stuff included in the source of the current item. The current item is expected
1402
     * to be an HTML file or string html. If it is not, then the function will return and empty list.
1403
     *
1404
     * @param    string  source html (content or path)
1405
     * @param    bool    is file or string html
1406
     * @param    string    type (one of the app tools) - optional (otherwise takes the current item's type)
1407
     * @param    int        level of recursivity we're in
1408
     *
1409
     * @return array List of file paths. An additional field containing 'local' or 'remote' helps determine
1410
     *               if the file should be copied into the zip or just linked
1411
     */
1412
    public static function get_resources_from_source_html(
1413
        $source_html,
1414
        $is_file = false,
1415
        $type = null,
1416
        $recursivity = 1
1417
    ) {
1418
        $max = 5;
1419
        $attributes = [];
1420
        $wanted_attributes = [
1421
            'src',
1422
            'url',
1423
            '@import',
1424
            'href',
1425
            'value',
1426
            'flashvars',
1427
            'poster',
1428
        ];
1429
        $explode_attributes = ['flashvars' => 'file'];
1430
        $abs_path = '';
1431
1432
        if ($recursivity > $max) {
1433
            return [];
1434
        }
1435
1436
        if (!isset($type)) {
1437
            $type = TOOL_DOCUMENT;
1438
        }
1439
1440
        if (!$is_file) {
1441
            $attributes = self::parse_HTML_attributes(
1442
                $source_html,
1443
                $wanted_attributes,
1444
                $explode_attributes
1445
            );
1446
        } else {
1447
            if (is_file($source_html)) {
1448
                $abs_path = $source_html;
1449
                //for now, read the whole file in one go (that's gonna be a problem when the file is too big)
1450
                $info = pathinfo($abs_path);
1451
                $ext = $info['extension'];
1452
                switch (strtolower($ext)) {
1453
                    case 'html':
1454
                    case 'htm':
1455
                    case 'shtml':
1456
                    case 'css':
1457
                        $file_content = file_get_contents($abs_path);
1458
                        // get an array of attributes from the HTML source
1459
                        $attributes = self::parse_HTML_attributes(
1460
                            $file_content,
1461
                            $wanted_attributes,
1462
                            $explode_attributes
1463
                        );
1464
                        break;
1465
                    default:
1466
                        break;
1467
                }
1468
            } else {
1469
                return [];
1470
            }
1471
        }
1472
1473
        $files_list = [];
1474
        switch ($type) {
1475
            case TOOL_DOCUMENT:
1476
            case TOOL_QUIZ:
1477
            case 'sco':
1478
                foreach ($wanted_attributes as $attr) {
1479
                    if (isset($attributes[$attr])) {
1480
                        //find which kind of path these are (local or remote)
1481
                        $sources = $attributes[$attr];
1482
                        foreach ($sources as $source) {
1483
                            //skip what is obviously not a resource
1484
                            if (strpos($source, '+this.')) {
1485
                                continue; //javascript code - will still work unaltered
1486
                            }
1487
                            if (false === strpos($source, '.')) {
1488
                                continue; //no dot, should not be an external file anyway
1489
                            }
1490
                            if (strpos($source, 'mailto:')) {
1491
                                continue; //mailto link
1492
                            }
1493
                            if (strpos($source, ';') && !strpos($source, '&amp;')) {
1494
                                continue; //avoid code - that should help
1495
                            }
1496
1497
                            if ('value' == $attr) {
1498
                                if (strpos($source, 'mp3file')) {
1499
                                    $files_list[] = [
1500
                                        substr($source, 0, strpos($source, '.swf') + 4),
1501
                                        'local',
1502
                                        'abs',
1503
                                    ];
1504
                                    $mp3file = substr($source, strpos($source, 'mp3file=') + 8);
1505
                                    if ('/' == substr($mp3file, 0, 1)) {
1506
                                        $files_list[] = [$mp3file, 'local', 'abs'];
1507
                                    } else {
1508
                                        $files_list[] = [$mp3file, 'local', 'rel'];
1509
                                    }
1510
                                } elseif (0 === strpos($source, 'flv=')) {
1511
                                    $source = substr($source, 4);
1512
                                    if (strpos($source, '&') > 0) {
1513
                                        $source = substr($source, 0, strpos($source, '&'));
1514
                                    }
1515
                                    if (strpos($source, '://') > 0) {
1516
                                        if (false !== strpos($source, api_get_path(WEB_PATH))) {
1517
                                            //we found the current portal url
1518
                                            $files_list[] = [$source, 'local', 'url'];
1519
                                        } else {
1520
                                            //we didn't find any trace of current portal
1521
                                            $files_list[] = [$source, 'remote', 'url'];
1522
                                        }
1523
                                    } else {
1524
                                        $files_list[] = [$source, 'local', 'abs'];
1525
                                    }
1526
                                    /* skipping anything else to avoid two entries
1527
                                    (while the others can have sub-files in their url, flv's can't)*/
1528
                                    continue;
1529
                                }
1530
                            }
1531
                            if (strpos($source, '://') > 0) {
1532
                                //cut at '?' in a URL with params
1533
                                if (strpos($source, '?') > 0) {
1534
                                    $second_part = substr($source, strpos($source, '?'));
1535
                                    if (strpos($second_part, '://') > 0) {
1536
                                        //if the second part of the url contains a url too, treat the second one before cutting
1537
                                        $pos1 = strpos($second_part, '=');
1538
                                        $pos2 = strpos($second_part, '&');
1539
                                        $second_part = substr($second_part, $pos1 + 1, $pos2 - ($pos1 + 1));
1540
                                        if (false !== strpos($second_part, api_get_path(WEB_PATH))) {
1541
                                            //we found the current portal url
1542
                                            $files_list[] = [$second_part, 'local', 'url'];
1543
                                            $in_files_list[] = self::get_resources_from_source_html(
1544
                                                $second_part,
1545
                                                true,
1546
                                                TOOL_DOCUMENT,
1547
                                                $recursivity + 1
1548
                                            );
1549
                                            if (count($in_files_list) > 0) {
1550
                                                $files_list = array_merge($files_list, $in_files_list);
1551
                                            }
1552
                                        } else {
1553
                                            //we didn't find any trace of current portal
1554
                                            $files_list[] = [$second_part, 'remote', 'url'];
1555
                                        }
1556
                                    } elseif (strpos($second_part, '=') > 0) {
1557
                                        if ('/' === substr($second_part, 0, 1)) {
1558
                                            //link starts with a /, making it absolute (relative to DocumentRoot)
1559
                                            $files_list[] = [$second_part, 'local', 'abs'];
1560
                                            $in_files_list[] = self::get_resources_from_source_html(
1561
                                                $second_part,
1562
                                                true,
1563
                                                TOOL_DOCUMENT,
1564
                                                $recursivity + 1
1565
                                            );
1566
                                            if (count($in_files_list) > 0) {
1567
                                                $files_list = array_merge($files_list, $in_files_list);
1568
                                            }
1569
                                        } elseif (0 === strstr($second_part, '..')) {
1570
                                            //link is relative but going back in the hierarchy
1571
                                            $files_list[] = [$second_part, 'local', 'rel'];
1572
                                            //$dir = api_get_path(SYS_CODE_PATH);//dirname($abs_path);
1573
                                            //$new_abs_path = realpath($dir.'/'.$second_part);
1574
                                            $dir = '';
1575
                                            if (!empty($abs_path)) {
1576
                                                $dir = dirname($abs_path).'/';
1577
                                            }
1578
                                            $new_abs_path = realpath($dir.$second_part);
1579
                                            $in_files_list[] = self::get_resources_from_source_html(
1580
                                                $new_abs_path,
1581
                                                true,
1582
                                                TOOL_DOCUMENT,
1583
                                                $recursivity + 1
1584
                                            );
1585
                                            if (count($in_files_list) > 0) {
1586
                                                $files_list = array_merge($files_list, $in_files_list);
1587
                                            }
1588
                                        } else {
1589
                                            //no starting '/', making it relative to current document's path
1590
                                            if ('./' == substr($second_part, 0, 2)) {
1591
                                                $second_part = substr($second_part, 2);
1592
                                            }
1593
                                            $files_list[] = [$second_part, 'local', 'rel'];
1594
                                            $dir = '';
1595
                                            if (!empty($abs_path)) {
1596
                                                $dir = dirname($abs_path).'/';
1597
                                            }
1598
                                            $new_abs_path = realpath($dir.$second_part);
1599
                                            $in_files_list[] = self::get_resources_from_source_html(
1600
                                                $new_abs_path,
1601
                                                true,
1602
                                                TOOL_DOCUMENT,
1603
                                                $recursivity + 1
1604
                                            );
1605
                                            if (count($in_files_list) > 0) {
1606
                                                $files_list = array_merge($files_list, $in_files_list);
1607
                                            }
1608
                                        }
1609
                                    }
1610
                                    //leave that second part behind now
1611
                                    $source = substr($source, 0, strpos($source, '?'));
1612
                                    if (strpos($source, '://') > 0) {
1613
                                        if (false !== strpos($source, api_get_path(WEB_PATH))) {
1614
                                            //we found the current portal url
1615
                                            $files_list[] = [$source, 'local', 'url'];
1616
                                            $in_files_list[] = self::get_resources_from_source_html(
1617
                                                $source,
1618
                                                true,
1619
                                                TOOL_DOCUMENT,
1620
                                                $recursivity + 1
1621
                                            );
1622
                                            if (count($in_files_list) > 0) {
1623
                                                $files_list = array_merge($files_list, $in_files_list);
1624
                                            }
1625
                                        } else {
1626
                                            //we didn't find any trace of current portal
1627
                                            $files_list[] = [$source, 'remote', 'url'];
1628
                                        }
1629
                                    } else {
1630
                                        //no protocol found, make link local
1631
                                        if ('/' === substr($source, 0, 1)) {
1632
                                            //link starts with a /, making it absolute (relative to DocumentRoot)
1633
                                            $files_list[] = [$source, 'local', 'abs'];
1634
                                            $in_files_list[] = self::get_resources_from_source_html(
1635
                                                $source,
1636
                                                true,
1637
                                                TOOL_DOCUMENT,
1638
                                                $recursivity + 1
1639
                                            );
1640
                                            if (count($in_files_list) > 0) {
1641
                                                $files_list = array_merge($files_list, $in_files_list);
1642
                                            }
1643
                                        } elseif (0 === strstr($source, '..')) {
1644
                                            //link is relative but going back in the hierarchy
1645
                                            $files_list[] = [$source, 'local', 'rel'];
1646
                                            $dir = '';
1647
                                            if (!empty($abs_path)) {
1648
                                                $dir = dirname($abs_path).'/';
1649
                                            }
1650
                                            $new_abs_path = realpath($dir.$source);
1651
                                            $in_files_list[] = self::get_resources_from_source_html(
1652
                                                $new_abs_path,
1653
                                                true,
1654
                                                TOOL_DOCUMENT,
1655
                                                $recursivity + 1
1656
                                            );
1657
                                            if (count($in_files_list) > 0) {
1658
                                                $files_list = array_merge($files_list, $in_files_list);
1659
                                            }
1660
                                        } else {
1661
                                            //no starting '/', making it relative to current document's path
1662
                                            if ('./' == substr($source, 0, 2)) {
1663
                                                $source = substr($source, 2);
1664
                                            }
1665
                                            $files_list[] = [$source, 'local', 'rel'];
1666
                                            $dir = '';
1667
                                            if (!empty($abs_path)) {
1668
                                                $dir = dirname($abs_path).'/';
1669
                                            }
1670
                                            $new_abs_path = realpath($dir.$source);
1671
                                            $in_files_list[] = self::get_resources_from_source_html(
1672
                                                $new_abs_path,
1673
                                                true,
1674
                                                TOOL_DOCUMENT,
1675
                                                $recursivity + 1
1676
                                            );
1677
                                            if (count($in_files_list) > 0) {
1678
                                                $files_list = array_merge($files_list, $in_files_list);
1679
                                            }
1680
                                        }
1681
                                    }
1682
                                }
1683
                                //found some protocol there
1684
                                if (false !== strpos($source, api_get_path(WEB_PATH))) {
1685
                                    //we found the current portal url
1686
                                    $files_list[] = [$source, 'local', 'url'];
1687
                                    $in_files_list[] = self::get_resources_from_source_html(
1688
                                        $source,
1689
                                        true,
1690
                                        TOOL_DOCUMENT,
1691
                                        $recursivity + 1
1692
                                    );
1693
                                    if (count($in_files_list) > 0) {
1694
                                        $files_list = array_merge($files_list, $in_files_list);
1695
                                    }
1696
                                } else {
1697
                                    //we didn't find any trace of current portal
1698
                                    $files_list[] = [$source, 'remote', 'url'];
1699
                                }
1700
                            } else {
1701
                                //no protocol found, make link local
1702
                                if ('/' === substr($source, 0, 1)) {
1703
                                    //link starts with a /, making it absolute (relative to DocumentRoot)
1704
                                    $files_list[] = [$source, 'local', 'abs'];
1705
                                    $in_files_list[] = self::get_resources_from_source_html(
1706
                                        $source,
1707
                                        true,
1708
                                        TOOL_DOCUMENT,
1709
                                        $recursivity + 1
1710
                                    );
1711
                                    if (count($in_files_list) > 0) {
1712
                                        $files_list = array_merge($files_list, $in_files_list);
1713
                                    }
1714
                                } elseif (0 === strpos($source, '..')) {
1715
                                    //link is relative but going back in the hierarchy
1716
                                    $files_list[] = [$source, 'local', 'rel'];
1717
                                    $dir = '';
1718
                                    if (!empty($abs_path)) {
1719
                                        $dir = dirname($abs_path).'/';
1720
                                    }
1721
                                    $new_abs_path = realpath($dir.$source);
1722
                                    $in_files_list[] = self::get_resources_from_source_html(
1723
                                        $new_abs_path,
1724
                                        true,
1725
                                        TOOL_DOCUMENT,
1726
                                        $recursivity + 1
1727
                                    );
1728
                                    if (count($in_files_list) > 0) {
1729
                                        $files_list = array_merge($files_list, $in_files_list);
1730
                                    }
1731
                                } else {
1732
                                    //no starting '/', making it relative to current document's path
1733
                                    if ('./' == substr($source, 0, 2)) {
1734
                                        $source = substr($source, 2);
1735
                                    }
1736
                                    $files_list[] = [$source, 'local', 'rel'];
1737
                                    $dir = '';
1738
                                    if (!empty($abs_path)) {
1739
                                        $dir = dirname($abs_path).'/';
1740
                                    }
1741
                                    $new_abs_path = realpath($dir.$source);
1742
                                    $in_files_list[] = self::get_resources_from_source_html(
1743
                                        $new_abs_path,
1744
                                        true,
1745
                                        TOOL_DOCUMENT,
1746
                                        $recursivity + 1
1747
                                    );
1748
                                    if (count($in_files_list) > 0) {
1749
                                        $files_list = array_merge($files_list, $in_files_list);
1750
                                    }
1751
                                }
1752
                            }
1753
                        }
1754
                    }
1755
                }
1756
                break;
1757
            default: //ignore
1758
                break;
1759
        }
1760
1761
        $checked_files_list = [];
1762
        $checked_array_list = [];
1763
1764
        if (count($files_list) > 0) {
1765
            foreach ($files_list as $idx => $file) {
1766
                if (!empty($file[0])) {
1767
                    if (!in_array($file[0], $checked_files_list)) {
1768
                        $checked_files_list[] = $files_list[$idx][0];
1769
                        $checked_array_list[] = $files_list[$idx];
1770
                    }
1771
                }
1772
            }
1773
        }
1774
1775
        return $checked_array_list;
1776
    }
1777
1778
    /**
1779
     * Parses the HTML attributes given as string.
1780
     *
1781
     * @param string HTML attribute string
1782
     * @param array List of attributes that we want to get back
1783
     * @param array
1784
     *
1785
     * @return array An associative array of attributes
1786
     *
1787
     * @author Based on a function from the HTML_Common2 PEAR module     *
1788
     */
1789
    public static function parse_HTML_attributes($attrString, $wanted = [], $explode_variables = [])
1790
    {
1791
        $attributes = [];
1792
        $regs = [];
1793
        $reduced = false;
1794
        if (count($wanted) > 0) {
1795
            $reduced = true;
1796
        }
1797
        try {
1798
            //Find all occurences of something that looks like a URL
1799
            // The structure of this regexp is:
1800
            // (find protocol) then
1801
            // (optionally find some kind of space 1 or more times) then
1802
            // find (either an equal sign or a bracket) followed by an optional space
1803
            // followed by some text without quotes (between quotes itself or not)
1804
            // then possible closing brackets if we were in the opening bracket case
1805
            // OR something like @import()
1806
            $res = preg_match_all(
1807
                '/(((([A-Za-z_:])([A-Za-z0-9_:\.-]*))'.
1808
                // '/(((([A-Za-z_:])([A-Za-z0-9_:\.-]|[^\x00-\x7F])*)' . -> seems to be taking too much
1809
                // '/(((([A-Za-z_:])([^\x00-\x7F])*)' . -> takes only last letter of parameter name
1810
                '([ \n\t\r]+)?('.
1811
                // '(=([ \n\t\r]+)?("[^"]+"|\'[^\']+\'|[^ \n\t\r]+))' . -> doesn't restrict close enough to the url itself
1812
                '(=([ \n\t\r]+)?("[^"\)]+"|\'[^\'\)]+\'|[^ \n\t\r\)]+))'.
1813
                '|'.
1814
                // '(\(([ \n\t\r]+)?("[^"]+"|\'[^\']+\'|[^ \n\t\r]+)\))' . -> doesn't restrict close enough to the url itself
1815
                '(\(([ \n\t\r]+)?("[^"\)]+"|\'[^\'\)]+\'|[^ \n\t\r\)]+)\))'.
1816
                '))'.
1817
                '|'.
1818
                // '(@import([ \n\t\r]+)?("[^"]+"|\'[^\']+\'|[^ \n\t\r]+)))?/', -> takes a lot (like 100's of thousands of empty possibilities)
1819
                '(@import([ \n\t\r]+)?("[^"]+"|\'[^\']+\'|[^ \n\t\r]+)))/',
1820
                $attrString,
1821
                $regs
1822
            );
1823
        } catch (Exception $e) {
1824
            error_log('Caught exception: '.$e->getMessage(), 0);
1825
        }
1826
        if ($res) {
1827
            for ($i = 0; $i < count($regs[1]); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1828
                $name = trim($regs[3][$i]);
1829
                $check = trim($regs[0][$i]);
1830
                $value = trim($regs[10][$i]);
1831
                if (empty($value) and !empty($regs[13][$i])) {
1832
                    $value = $regs[13][$i];
1833
                }
1834
                if (empty($name) && !empty($regs[16][$i])) {
1835
                    $name = '@import';
1836
                    $value = trim($regs[16][$i]);
1837
                }
1838
                if (!empty($name)) {
1839
                    if (!$reduced || in_array(strtolower($name), $wanted)) {
1840
                        if ($name == $check) {
1841
                            $attributes[strtolower($name)][] = strtolower($name);
1842
                        } else {
1843
                            if (!empty($value) && ('\'' == $value[0] || '"' == $value[0])) {
1844
                                $value = substr($value, 1, -1);
1845
                            }
1846
1847
                            if ('API.LMSGetValue(name' == $value) {
1848
                                $value = 'API.LMSGetValue(name)';
1849
                            }
1850
                            //Gets the xx.flv value from the string flashvars="width=320&height=240&autostart=false&file=xxx.flv&repeat=false"
1851
                            if (isset($explode_variables[$name])) {
1852
                                $value_modified = str_replace('&amp;', '&', $value);
1853
                                $value_array = explode('&', $value_modified);
1854
                                foreach ($value_array as $item) {
1855
                                    $itemParts = explode('=', $item);
1856
                                    $key = $itemParts[0];
1857
                                    $item_value = !empty($itemParts[1]) ? $itemParts[1] : '';
1858
                                    if ($key == $explode_variables[$name]) {
1859
                                        $attributes[strtolower($name)][] = $item_value;
1860
                                    }
1861
                                }
1862
                            }
1863
                            $attributes[strtolower($name)][] = $value;
1864
                        }
1865
                    }
1866
                }
1867
            }
1868
        }
1869
1870
        return $attributes;
1871
    }
1872
1873
    /**
1874
     * Replace urls inside content html from a copy course.
1875
     *
1876
     * @param string $content_html
1877
     * @param string $origin_course_code
1878
     * @param string $destination_course_directory
1879
     * @param string $origin_course_path_from_zip
1880
     * @param string $origin_course_info_path
1881
     *
1882
     * @return string new content html with replaced urls or return false if content is not a string
1883
     */
1884
    public static function replaceUrlWithNewCourseCode(
1885
        $content_html,
1886
        $origin_course_code,
1887
        $destination_course_directory,
1888
        $origin_course_path_from_zip = null,
1889
        $origin_course_info_path = null
1890
    ) {
1891
        if (empty($content_html)) {
1892
            return false;
1893
        }
1894
1895
        $orig_source_html = self::get_resources_from_source_html($content_html);
1896
        $orig_course_info = api_get_course_info($origin_course_code);
1897
1898
        $destination_course_code = CourseManager::getCourseCodeFromDirectory($destination_course_directory);
1899
        $destination_course_info = api_get_course_info($destination_course_code);
1900
1901
        if (!empty($orig_source_html)) {
1902
            foreach ($orig_source_html as $source) {
1903
1904
                $real_orig_url = $source[0];
1905
                $scope_url = $source[1];
1906
                $type_url = $source[2];
1907
1908
                if ('local' === $scope_url) {
1909
                    $document_file = strstr($real_orig_url, 'document');
1910
1911
                    if (false !== $document_file) {
1912
                        $new_url = self::generateNewUrlForCourseResource($destination_course_info, $document_file);
1913
                        $content_html = str_replace($real_orig_url, $new_url, $content_html);
1914
                    }
1915
                }
1916
            }
1917
        }
1918
1919
        return $content_html;
1920
    }
1921
1922
    /**
1923
     * Generates a new URL for a resource within the context of the target course.
1924
     *
1925
     * This function constructs a URL to access a given resource, such as a document
1926
     * or image, which has been copied into the target course. It's essential for
1927
     * updating resource links in course content to point to the correct location
1928
     * after resources have been duplicated or moved between courses.
1929
     */
1930
    public static function generateNewUrlForCourseResource(array $destination_course_info, string $document_file): string
1931
    {
1932
        $courseCode = $destination_course_info['code'];
1933
        $courseWebPath = api_get_path(WEB_COURSE_PATH) . $courseCode . "/document/";
1934
1935
        $document_file = ltrim($document_file, '/');
1936
1937
        $newUrl = $courseWebPath . $document_file;
1938
1939
        return $newUrl;
1940
    }
1941
1942
1943
    /**
1944
     * Obtains the text inside the file with the right parser.
1945
     */
1946
    public static function get_text_content($doc_path, $doc_mime)
1947
    {
1948
        // TODO: review w$ compatibility
1949
        // Use usual exec output lines array to store stdout instead of a temp file
1950
        // because we need to store it at RAM anyway before index on ChamiloIndexer object
1951
        $ret_val = null;
1952
        switch ($doc_mime) {
1953
            case 'text/plain':
1954
                $handle = fopen($doc_path, 'r');
1955
                $output = [fread($handle, filesize($doc_path))];
1956
                fclose($handle);
1957
                break;
1958
            case 'application/pdf':
1959
                exec("pdftotext $doc_path -", $output, $ret_val);
1960
                break;
1961
            case 'application/postscript':
1962
                $temp_file = tempnam(sys_get_temp_dir(), 'chamilo');
1963
                exec("ps2pdf $doc_path $temp_file", $output, $ret_val);
1964
                if (0 !== $ret_val) { // shell fail, probably 127 (command not found)
1965
                    return false;
1966
                }
1967
                exec("pdftotext $temp_file -", $output, $ret_val);
1968
                unlink($temp_file);
1969
                break;
1970
            case 'application/msword':
1971
                exec("catdoc $doc_path", $output, $ret_val);
1972
                break;
1973
            case 'text/html':
1974
                exec("html2text $doc_path", $output, $ret_val);
1975
                break;
1976
            case 'text/rtf':
1977
                // Note: correct handling of code pages in unrtf
1978
                // on debian lenny unrtf v0.19.2 can not, but unrtf v0.20.5 can
1979
                exec("unrtf --text $doc_path", $output, $ret_val);
1980
                if (127 == $ret_val) { // command not found
1981
                    return false;
1982
                }
1983
                // Avoid index unrtf comments
1984
                if (is_array($output) && count($output) > 1) {
1985
                    $parsed_output = [];
1986
                    foreach ($output as &$line) {
1987
                        if (!preg_match('/^###/', $line, $matches)) {
1988
                            if (!empty($line)) {
1989
                                $parsed_output[] = $line;
1990
                            }
1991
                        }
1992
                    }
1993
                    $output = $parsed_output;
1994
                }
1995
                break;
1996
            case 'application/vnd.ms-powerpoint':
1997
                exec("catppt $doc_path", $output, $ret_val);
1998
                break;
1999
            case 'application/vnd.ms-excel':
2000
                exec("xls2csv -c\" \" $doc_path", $output, $ret_val);
2001
                break;
2002
        }
2003
2004
        $content = '';
2005
        if (!is_null($ret_val)) {
2006
            if (0 !== $ret_val) { // shell fail, probably 127 (command not found)
2007
                return false;
2008
            }
2009
        }
2010
        if (isset($output)) {
2011
            foreach ($output as &$line) {
2012
                $content .= $line."\n";
2013
            }
2014
2015
            return $content;
2016
        } else {
2017
            return false;
2018
        }
2019
    }
2020
2021
    /**
2022
     * Display the document quota in a simple way.
2023
     *
2024
     *  Here we count 1 Kilobyte = 1024 Bytes, 1 Megabyte = 1048576 Bytes
2025
     */
2026
    public static function displaySimpleQuota($course_quota, $already_consumed_space)
2027
    {
2028
        $course_quota_m = round($course_quota / 1048576);
2029
        $already_consumed_space_m = round($already_consumed_space / 1048576, 2);
2030
        $percentage = $already_consumed_space / $course_quota * 100;
2031
        $percentage = round($percentage, 1);
2032
        $message = get_lang('You are currently using %s MB (%s) of your %s MB.');
2033
        $message = sprintf($message, $already_consumed_space_m, $percentage.'%', $course_quota_m.' ');
2034
2035
        return Display::div($message, ['id' => 'document_quota', 'class' => 'card-quota']);
2036
    }
2037
2038
    /**
2039
     * Checks if there is enough place to add a file on a directory
2040
     * on the base of a maximum directory size allowed.
2041
     *
2042
     * @author Bert Vanderkimpen
2043
     *
2044
     * @param int $file_size     size of the file in byte
2045
     * @param int $max_dir_space maximum size
2046
     *
2047
     * @return bool true if there is enough space, false otherwise
2048
     */
2049
    public static function enough_space($file_size, $max_dir_space)
2050
    {
2051
        if ($max_dir_space) {
2052
            $max_dir_space = $max_dir_space * 1024 * 1024;
2053
            $courseEntity = api_get_course_entity();
2054
            $repo = Container::getDocumentRepository();
2055
            $total = $repo->getFolderSize($courseEntity->getResourceNode(), $courseEntity);
2056
2057
            if (($file_size + $total) > $max_dir_space) {
2058
                return false;
2059
            }
2060
        }
2061
2062
        return true;
2063
    }
2064
2065
    /**
2066
     * @param array $params count, url, extension
2067
     *
2068
     * @return string
2069
     */
2070
    public static function generateAudioJavascript($params = [])
2071
    {
2072
        $js = '
2073
            $(\'audio.audio_preview\').mediaelementplayer({
2074
                features: [\'playpause\'],
2075
                audioWidth: 30,
2076
                audioHeight: 30,
2077
                success: function(mediaElement, originalNode, instance) {
2078
                }
2079
            });';
2080
2081
        return $js;
2082
    }
2083
2084
    /**
2085
     * Shows a play icon next to the document title in the document list.
2086
     *
2087
     * @param string $documentWebPath
2088
     * @param array  $documentInfo
2089
     *
2090
     * @return string
2091
     */
2092
    public static function generateAudioPreview($documentWebPath, $documentInfo)
2093
    {
2094
        $filePath = $documentWebPath.$documentInfo['path'];
2095
        $extension = $documentInfo['file_extension'];
2096
2097
        return '<span class="preview">
2098
                    <audio class="audio_preview skip" src="'.$filePath.'" type="audio/'.$extension.'"></audio>
2099
                </span>';
2100
    }
2101
2102
    /**
2103
     * @param string $file
2104
     * @param string $extension
2105
     *
2106
     * @return string
2107
     */
2108
    public static function generateMediaPreview($file, $extension)
2109
    {
2110
        $id = api_get_unique_id();
2111
        switch ($extension) {
2112
            case 'wav':
2113
            case 'ogg':
2114
            case 'mp3':
2115
                $html = '<div style="margin: 0; position: absolute; top: 50%; left: 35%;">';
2116
                $html .= '<audio id="'.$id.'" controls="controls" src="'.$file.'" type="audio/mp3" ></audio></div>';
2117
                break;
2118
            default:
2119
                $html = '<video id="'.$id.'" controls>';
2120
                $html .= '<source src="'.$file.'" >';
2121
                $html .= '</video>';
2122
                break;
2123
        }
2124
2125
        return $html;
2126
    }
2127
2128
    /**
2129
     * @param bool   $lp_id
2130
     * @param string $target
2131
     * @param int    $session_id
2132
     * @param bool   $add_move_button
2133
     * @param string $filter_by_folder
2134
     * @param string $overwrite_url
2135
     * @param bool   $showInvisibleFiles
2136
     * @param bool   $showOnlyFolders
2137
     * @param int    $folderId
2138
     * @param bool   $addCloseButton
2139
     * @param bool   $addAudioPreview
2140
     * @param array  $filterByExtension
2141
     *
2142
     * @return string
2143
     */
2144
    public static function get_document_preview(
2145
        Course $course,
2146
               $lp_id = false,
2147
               $target = '',
2148
               $session_id = 0,
2149
               $add_move_button = false,
2150
               $filter_by_folder = null,
2151
               $overwrite_url = '',
2152
               $showInvisibleFiles = false,
2153
               $showOnlyFolders = false,
2154
               $folderId = false,
2155
               $addCloseButton = true,
2156
               $addAudioPreview = false,
2157
        array $filterByExtension = [],
2158
        array $excludeByExtension = [],
2159
               $filterByFiletype = null,
2160
        bool $flattenRoot = false
2161
    ) {
2162
        $repo = Container::getDocumentRepository();
2163
        $nodeRepository = $repo->getResourceNodeRepository();
2164
        $move = get_lang('Move');
2165
        $icon = '<i class="mdi-cursor-move mdi ch-tool-icon" style="font-size:16px;width:16px;height:16px;" title="'.htmlentities($move).'"></i>';
2166
        $folderIcon = Display::getMdiIcon(ObjectIcon::CHAPTER, 'ch-tool-icon', null, ICON_SIZE_SMALL);
2167
        $fileIcon = '<i class="mdi-file mdi ch-tool-icon" style="font-size:16px;width:16px;height:16px;" aria-hidden="true" title="'
2168
            . htmlentities(get_lang('File'))
2169
            . '"></i>';
2170
2171
        $lpItemType = ($filterByFiletype === 'video') ? 'video' : 'document';
2172
        $options = [
2173
            'decorate'   => true,
2174
            'rootOpen'   => '<ul id="doc_list" class="list-group lp_resource">',
2175
            'rootClose'  => '</ul>',
2176
            'childOpen'  => function ($child) {
2177
                return '<li id="'.$child['id'].'" data-id="'.$child['id'].'" class="list-group-item nested-'.$child['level'].'">';
2178
            },
2179
            'childClose' => '</li>',
2180
            'nodeDecorator' => function ($node) use ($icon, $fileIcon, $lpItemType) {
2181
                $link  = '<div class="flex flex-row gap-1 h-4 item_data">';
2182
                $link .= '<a class="moved ui-sortable-handle" href="#">'.$icon.'</a>';
2183
                $link .= '<a data_id="'.$node['id'].'" data_type="'.$lpItemType.'" class="moved ui-sortable-handle link_with_id">'.$fileIcon.'&nbsp;</a>';
2184
                $link .= cut(addslashes($node['title']), 150);
2185
                $link .= '</div>';
2186
2187
                return $link;
2188
            },
2189
        ];
2190
2191
        $type = $repo->getResourceType();
2192
        $em = Database::getManager();
2193
        $qb = $em
2194
            ->createQueryBuilder()
2195
            ->select('node, files')
2196
            ->from(ResourceNode::class, 'node')
2197
            ->innerJoin('node.resourceType', 'type')
2198
            ->innerJoin('node.resourceLinks', 'links')
2199
            ->innerJoin('node.resourceFiles', 'files')
2200
            ->innerJoin(
2201
                CDocument::class,
2202
                'doc',
2203
                'WITH',
2204
                'doc.resourceNode = node'
2205
            )
2206
            ->addSelect('files')
2207
            ->where('type = :type')
2208
            ->andWhere('links.course = :course')
2209
            ->setParameters([
2210
                'type' => $type,
2211
                'course' => $course,
2212
            ])
2213
            ->orderBy('node.parent', 'ASC');
2214
2215
        $sessionId = api_get_session_id();
2216
        if (empty($sessionId)) {
2217
            $qb->andWhere('links.session IS NULL');
2218
        } else {
2219
            $qb
2220
                ->andWhere('links.session = :session')
2221
                ->setParameter('session', $sessionId);
2222
        }
2223
2224
        if ($filterByFiletype !== null) {
2225
            if (is_array($filterByFiletype)) {
2226
                $qb->andWhere('doc.filetype IN (:filetypes)');
2227
                $qb->setParameter('filetypes', $filterByFiletype);
2228
            } else {
2229
                $qb->andWhere('doc.filetype = :filetype');
2230
                $qb->setParameter('filetype', $filterByFiletype);
2231
            }
2232
        }
2233
2234
        if (!empty($filterByExtension)) {
2235
            $orX = $qb->expr()->orX();
2236
            foreach ($filterByExtension as $extension) {
2237
                $paramName = 'ext_' . $extension;
2238
                $orX->add(
2239
                    $qb->expr()->like(
2240
                        'LOWER(files.originalName)',
2241
                        ':' . $paramName
2242
                    )
2243
                );
2244
                $qb->setParameter($paramName, '%.' . strtolower($extension));
2245
            }
2246
            $qb->andWhere($orX);
2247
        }
2248
2249
        if (!empty($excludeByExtension)) {
2250
            foreach ($excludeByExtension as $extension) {
2251
                $qb->andWhere(
2252
                    $qb->expr()->notLike(
2253
                        'LOWER(files.originalName)',
2254
                        ':exclude_' . $extension
2255
                    )
2256
                );
2257
                $qb->setParameter('exclude_' . $extension, '%.' . strtolower($extension));
2258
            }
2259
        }
2260
2261
        $items = $qb->getQuery()->getArrayResult();
2262
2263
        if ($flattenRoot) {
2264
            foreach ($items as &$item) {
2265
                $item['level'] = 0;
2266
            }
2267
            unset($item);
2268
        }
2269
2270
        return $nodeRepository->buildTree($items, $options);
2271
    }
2272
2273
    /**
2274
     * Index a given document.
2275
     *
2276
     * @param   int     Document ID inside its corresponding course
2277
     * @param   string  Course code
2278
     * @param   int     Session ID (not used yet)
2279
     * @param   string  Language of document's content (defaults to course language)
2280
     * @param   array   Array of specific fields (['code'=>'value',...]) @deprecated Not used anymore in Chamilo 2
2281
     * @param   string  What to do if the file already exists (default or overwrite)
2282
     * @param   bool    When set to true, this runs the indexer without actually saving anything to any database
2283
     *
2284
     * @return bool Returns true on presumed success, false on failure
2285
     *
2286
     * @deprecated since Chamilo 2.0. Specific fields indexing is removed in Chamilo 2.
2287
     *             Use the Xapian indexers (e.g. Chamilo\CoreBundle\Search\Xapian\DocumentXapianIndexer / LpXapianIndexer).
2288
     */
2289
    public static function index_document(
2290
        $docid,
2291
        $course_code,
2292
        $session_id = 0,
2293
        $lang = 'english',
2294
        $specific_fields_values = [],
2295
        $if_exists = '',
2296
        $simulation = false
2297
    ) {
2298
        if ('true' !== api_get_setting('search_enabled')) {
2299
            return false;
2300
        }
2301
        if (empty($docid) or $docid != intval($docid)) {
2302
            return false;
2303
        }
2304
        if (empty($session_id)) {
2305
            $session_id = api_get_session_id();
2306
        }
2307
        $course_info = api_get_course_info($course_code);
2308
        $course_dir = $course_info['path'].'/document';
2309
        $sys_course_path = api_get_path(SYS_COURSE_PATH);
2310
        $base_work_dir = $sys_course_path.$course_dir;
2311
2312
        $course_id = $course_info['real_id'];
2313
        $table_document = Database::get_course_table(TABLE_DOCUMENT);
2314
2315
        $qry = "SELECT path, title FROM $table_document WHERE c_id = $course_id AND id = '$docid' LIMIT 1";
2316
        $result = Database::query($qry);
2317
        if (1 == Database::num_rows($result)) {
2318
            $row = Database::fetch_array($result);
2319
            $doc_path = api_get_path(SYS_COURSE_PATH).$course_dir.$row['path'];
2320
            //TODO: mime_content_type is deprecated, fileinfo php extension is enabled by default as of PHP 5.3.0
2321
            // now versions of PHP on Debian testing(5.2.6-5) and Ubuntu(5.2.6-2ubuntu) are lower, so wait for a while
2322
            $doc_mime = mime_content_type($doc_path);
2323
            $allowed_mime_types = self::file_get_mime_type(true);
2324
2325
            // mime_content_type does not detect correctly some formats that
2326
            // are going to be supported for index, so an extensions array is used for the moment
2327
            if (empty($doc_mime)) {
2328
                $allowed_extensions = [
2329
                    'doc',
2330
                    'docx',
2331
                    'ppt',
2332
                    'pptx',
2333
                    'pps',
2334
                    'ppsx',
2335
                    'xls',
2336
                    'xlsx',
2337
                    'odt',
2338
                    'odp',
2339
                    'ods',
2340
                    'pdf',
2341
                    'txt',
2342
                    'rtf',
2343
                    'msg',
2344
                    'csv',
2345
                    'html',
2346
                    'htm',
2347
                ];
2348
                $extensions = preg_split("/[\/\\.]/", $doc_path);
2349
                $doc_ext = strtolower($extensions[count($extensions) - 1]);
2350
                if (in_array($doc_ext, $allowed_extensions)) {
2351
                    switch ($doc_ext) {
2352
                        case 'ppt':
2353
                        case 'pps':
2354
                            $doc_mime = 'application/vnd.ms-powerpoint';
2355
                            break;
2356
                        case 'xls':
2357
                            $doc_mime = 'application/vnd.ms-excel';
2358
                            break;
2359
                    }
2360
                }
2361
            }
2362
2363
            //@todo move this nightmare in a search controller or something like that!!! J.M
2364
2365
            if (in_array($doc_mime, $allowed_mime_types)) {
2366
                $file_title = $row['title'];
2367
                $file_content = self::get_text_content($doc_path, $doc_mime);
2368
                $course_code = Database::escape_string($course_code);
2369
                $ic_slide = new IndexableChunk();
2370
                $ic_slide->addValue('title', $file_title);
2371
                $ic_slide->addCourseId($course_code);
2372
                $ic_slide->addToolId(TOOL_DOCUMENT);
2373
                $xapian_data = [
2374
                    SE_COURSE_ID => $course_code,
2375
                    SE_TOOL_ID => TOOL_DOCUMENT,
2376
                    SE_DATA => ['doc_id' => $docid],
2377
                    SE_USER => api_get_user_id(),
2378
                ];
2379
2380
                $ic_slide->xapian_data = serialize($xapian_data);
2381
                $di = new ChamiloIndexer();
2382
                $return = $di->connectDb(null, null, $lang);
2383
2384
                require_once api_get_path(LIBRARY_PATH).'specific_fields_manager.lib.php';
2385
                $specific_fields = get_specific_field_list();
2386
2387
                // process different depending on what to do if file exists
2388
                /**
2389
                 * @TODO Find a way to really verify if the file had been
2390
                 * overwriten. Now all work is done at
2391
                 * handle_uploaded_document() and it's difficult to verify it
2392
                 */
2393
                if (!empty($if_exists) && 'overwrite' == $if_exists) {
2394
                    // Overwrite the file on search engine
2395
                    // Actually, it consists on a delete of terms from db,
2396
                    // insert new ones, create a new search engine document,
2397
                    // and remove the old one
2398
                    // Get search_did
2399
                    $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2400
                    $sql = 'SELECT * FROM %s WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s LIMIT 1';
2401
                    $sql = sprintf($sql, $tbl_se_ref, $course_code, TOOL_DOCUMENT, $docid);
2402
2403
                    $res = Database::query($sql);
2404
2405
                    if (Database::num_rows($res) > 0) {
2406
                        $se_ref = Database::fetch_array($res);
2407
                        if (!$simulation) {
2408
                            $di->remove_document($se_ref['search_did']);
2409
                        }
2410
                        $all_specific_terms = '';
2411
                        foreach ($specific_fields as $specific_field) {
2412
                            if (!$simulation) {
2413
                                delete_all_specific_field_value($course_code, $specific_field['id'], TOOL_DOCUMENT, $docid);
2414
                            }
2415
                            // Update search engine
2416
                            if (isset($specific_fields_values[$specific_field['code']])) {
2417
                                $sterms = trim($specific_fields_values[$specific_field['code']]);
2418
                            } else { //if the specific field is not defined, force an empty one
2419
                                $sterms = '';
2420
                            }
2421
                            $all_specific_terms .= ' '.$sterms;
2422
                            $sterms = explode(',', $sterms);
2423
                            foreach ($sterms as $sterm) {
2424
                                $sterm = trim($sterm);
2425
                                if (!empty($sterm)) {
2426
                                    $ic_slide->addTerm($sterm, $specific_field['code']);
2427
                                    // updated the last param here from $value to $sterm without being sure - see commit15464
2428
                                    if (!$simulation) {
2429
                                        add_specific_field_value(
2430
                                            $specific_field['id'],
2431
                                            $course_code,
2432
                                            TOOL_DOCUMENT,
2433
                                            $docid,
2434
                                            $sterm
2435
                                        );
2436
                                    }
2437
                                }
2438
                            }
2439
                        }
2440
                        // Add terms also to content to make terms findable by probabilistic search
2441
                        $file_content = $all_specific_terms.' '.$file_content;
2442
2443
                        if (!$simulation) {
2444
                            $ic_slide->addValue('content', $file_content);
2445
                            $di->addChunk($ic_slide);
2446
                            // Index and return a new search engine document id
2447
                            $did = $di->index();
2448
2449
                            if ($did) {
2450
                                // update the search_did on db
2451
                                $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2452
                                $sql = 'UPDATE %s SET search_did=%d WHERE id=%d LIMIT 1';
2453
                                $sql = sprintf($sql, $tbl_se_ref, (int) $did, (int) $se_ref['id']);
2454
                                Database::query($sql);
2455
                            }
2456
                        }
2457
                    }
2458
                } else {
2459
                    // Add all terms
2460
                    $all_specific_terms = '';
2461
                    foreach ($specific_fields as $specific_field) {
2462
                        if (isset($specific_fields_values[$specific_field['code']])) {
2463
                            $sterms = trim($specific_fields_values[$specific_field['code']]);
2464
                        } else { //if the specific field is not defined, force an empty one
2465
                            $sterms = '';
2466
                        }
2467
                        $all_specific_terms .= ' '.$sterms;
2468
                        if (!empty($sterms)) {
2469
                            $sterms = explode(',', $sterms);
2470
                            foreach ($sterms as $sterm) {
2471
                                if (!$simulation) {
2472
                                    $ic_slide->addTerm(trim($sterm), $specific_field['code']);
2473
                                    add_specific_field_value(
2474
                                        $specific_field['id'],
2475
                                        $course_code,
2476
                                        TOOL_DOCUMENT,
2477
                                        $docid,
2478
                                        $sterm
2479
                                    );
2480
                                }
2481
                            }
2482
                        }
2483
                    }
2484
                    // Add terms also to content to make terms findable by probabilistic search
2485
                    $file_content = $all_specific_terms.' '.$file_content;
2486
                    if (!$simulation) {
2487
                        $ic_slide->addValue('content', $file_content);
2488
                        $di->addChunk($ic_slide);
2489
                        // Index and return search engine document id
2490
                        $did = $di->index();
2491
                        if ($did) {
2492
                            // Save it to db
2493
                            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
2494
                            $sql = 'INSERT INTO %s (id, course_code, tool_id, ref_id_high_level, search_did)
2495
                            VALUES (NULL , \'%s\', \'%s\', %s, %s)';
2496
                            $sql = sprintf($sql, $tbl_se_ref, $course_code, TOOL_DOCUMENT, $docid, $did);
2497
                            Database::query($sql);
2498
                        } else {
2499
                            return false;
2500
                        }
2501
                    }
2502
                }
2503
            } else {
2504
                return false;
2505
            }
2506
        }
2507
2508
        return true;
2509
    }
2510
2511
    /**
2512
     * @param string $path
2513
     * @param bool   $is_certificate_mode
2514
     *
2515
     * @return bool
2516
     */
2517
    public static function is_folder_to_avoid($path, $is_certificate_mode = false)
2518
    {
2519
        $foldersToAvoid = [
2520
            '/HotPotatoes_files',
2521
            '/certificates',
2522
        ];
2523
        $systemFolder = api_get_course_setting('show_system_folders');
2524
2525
        if (1 == $systemFolder) {
2526
            $foldersToAvoid = [];
2527
        }
2528
2529
        if ('css' == basename($path)) {
2530
            return true;
2531
        }
2532
2533
        if (false == $is_certificate_mode) {
2534
            //Certificate results
2535
            if (strstr($path, 'certificates')) {
2536
                return true;
2537
            }
2538
        }
2539
2540
        // Admin setting for Hide/Show the folders of all users
2541
        if ('false' == api_get_setting('show_users_folders')) {
2542
            $foldersToAvoid[] = '/shared_folder';
2543
2544
            if (strstr($path, 'shared_folder_session_')) {
2545
                return true;
2546
            }
2547
        }
2548
2549
        // Admin setting for Hide/Show Default folders to all users
2550
        if ('false' == api_get_setting('show_default_folders')) {
2551
            $foldersToAvoid[] = '/images';
2552
            $foldersToAvoid[] = '/flash';
2553
            $foldersToAvoid[] = '/audio';
2554
            $foldersToAvoid[] = '/video';
2555
        }
2556
2557
        // Admin setting for Hide/Show chat history folder
2558
        if ('false' == api_get_setting('show_chat_folder')) {
2559
            $foldersToAvoid[] = '/chat_files';
2560
        }
2561
2562
        if (is_array($foldersToAvoid)) {
2563
            return in_array($path, $foldersToAvoid);
2564
        } else {
2565
            return false;
2566
        }
2567
    }
2568
2569
    /**
2570
     * @return array
2571
     */
2572
    public static function get_system_folders()
2573
    {
2574
        return [
2575
            '/certificates',
2576
            '/HotPotatoes_files',
2577
            '/chat_files',
2578
            '/images',
2579
            '/flash',
2580
            '/audio',
2581
            '/video',
2582
            '/shared_folder',
2583
            '/learning_path',
2584
        ];
2585
    }
2586
2587
    /**
2588
     * @return array
2589
     */
2590
    public static function getProtectedFolderFromStudent()
2591
    {
2592
        return [
2593
            '/certificates',
2594
            '/HotPotatoes_files',
2595
            '/chat_files',
2596
            '/shared_folder',
2597
            '/learning_path',
2598
        ];
2599
    }
2600
2601
    /**
2602
     * @param string $courseCode
2603
     *
2604
     * @return string 'visible' or 'invisible' string
2605
     */
2606
    public static function getDocumentDefaultVisibility($courseCode)
2607
    {
2608
        $settings = api_get_setting('tool_visible_by_default_at_creation');
2609
        $defaultVisibility = 'visible';
2610
2611
        if (isset($settings['documents'])) {
2612
            $portalDefaultVisibility = 'invisible';
2613
            if ('true' == $settings['documents']) {
2614
                $portalDefaultVisibility = 'visible';
2615
            }
2616
2617
            $defaultVisibility = $portalDefaultVisibility;
2618
        }
2619
2620
        if ('true' === api_get_setting('documents_default_visibility_defined_in_course')) {
2621
            $courseVisibility = api_get_course_setting('documents_default_visibility', $courseCode);
2622
            if (!empty($courseVisibility) && in_array($courseVisibility, ['visible', 'invisible'])) {
2623
                $defaultVisibility = $courseVisibility;
2624
            }
2625
        }
2626
2627
        return $defaultVisibility;
2628
    }
2629
2630
    /**
2631
     * @param array $_course
2632
     *
2633
     * @return CDocument
2634
     */
2635
    public static function createDefaultAudioFolder($_course)
2636
    {
2637
        if (!isset($_course['path'])) {
2638
            return false;
2639
        }
2640
2641
        return self::addDocument($_course, '/audio', 'folder', 0, 'Audio');
2642
    }
2643
2644
    /**
2645
     * Generate a default certificate for a courses.
2646
     *
2647
     * @todo move to certificate lib
2648
     *
2649
     * @global string $css CSS directory
2650
     * @global string $img_dir image directory
2651
     * @global string $default_course_dir Course directory
2652
     * @global string $js JS directory
2653
     *
2654
     * @param array $courseData     The course info
2655
     * @param bool  $fromBaseCourse
2656
     * @param int   $sessionId
2657
     */
2658
    public static function generateDefaultCertificate(
2659
        $courseData,
2660
        $fromBaseCourse = false,
2661
        $sessionId = 0
2662
    ) {
2663
        if (empty($courseData)) {
2664
            return false;
2665
        }
2666
2667
        $courseRealId = (int) ($courseData['real_id'] ?? 0);
2668
        $sessionId = (int) $sessionId;
2669
        $existingDefaultCertificateId = self::get_default_certificate_id($courseRealId, $sessionId);
2670
        if (!empty($existingDefaultCertificateId)) {
2671
            return $existingDefaultCertificateId;
2672
        }
2673
2674
        global $css, $img_dir, $default_course_dir, $js;
2675
        $codePath = api_get_path(REL_CODE_PATH);
2676
        $imgPath = api_get_path(WEB_PATH) . 'img/';
2677
        $dir = '/certificates';
2678
        $comment = null;
2679
        $title = get_lang('Default certificate');
2680
        $fileName = api_replace_dangerous_char($title);
2681
        $fileType = 'certificate';
2682
        $templateContent = file_get_contents(
2683
            api_get_path(SYS_CODE_PATH) . 'gradebook/certificate_template/template.html'
2684
        );
2685
2686
        $search = ['{CSS}', '{IMG_DIR}', '{REL_CODE_PATH}', '{IMG_PATH}'];
2687
        $replace = [$css . $js, $img_dir, $codePath, $imgPath];
2688
2689
        $fileContent = str_replace($search, $replace, $templateContent);
2690
        $saveFilePath = "$dir/$fileName.html";
2691
2692
        if ($fromBaseCourse) {
2693
            $baseDefaultCertificateId = self::get_default_certificate_id($courseRealId, 0);
2694
2695
            if (!empty($baseDefaultCertificateId)) {
2696
                // We have a certificate from the course base
2697
                $documentData = self::get_document_data_by_id(
2698
                    $baseDefaultCertificateId,
2699
                    $courseData['code'],
2700
                    false,
2701
                    0
2702
                );
2703
2704
                if (isset($documentData['absolute_path'])) {
2705
                    $fileContent = file_get_contents($documentData['absolute_path']);
2706
                }
2707
            }
2708
        }
2709
2710
        $document = self::addDocument(
2711
            $courseData,
2712
            $saveFilePath,
2713
            $fileType,
2714
            0,
2715
            $title,
2716
            $comment,
2717
            0,      // $readonly = 0
2718
            true,   // $save_visibility = true
2719
            null,   // $group_id = null
2720
            $sessionId,
2721
            0,
2722
            false,
2723
            $fileContent
2724
        );
2725
2726
        // Re-check and attach only if still missing (and the document was created).
2727
        $defaultCertificateId = self::get_default_certificate_id($courseRealId, $sessionId);
2728
2729
        // ✅ Use empty() (not isset) to avoid wrong truthiness edge cases
2730
        if (empty($defaultCertificateId) && $document) {
2731
            self::attach_gradebook_certificate(
2732
                $courseRealId,
2733
                (int) $document->getIid(),
2734
                $sessionId
2735
            );
2736
2737
            $defaultCertificateId = (int) $document->getIid();
2738
        }
2739
2740
        return $defaultCertificateId ?: false;
2741
    }
2742
2743
    /**
2744
     * Get folder/file suffix.
2745
     *
2746
     * @param array $courseInfo
2747
     * @param int   $sessionId
2748
     * @param int   $groupId
2749
     *
2750
     * @return string
2751
     */
2752
    public static function getDocumentSuffix($courseInfo, $sessionId, $groupId)
2753
    {
2754
        // If no session or group, then no suffix.
2755
        if (empty($sessionId) && empty($groupId)) {
2756
            return '';
2757
        }
2758
2759
        return '__'.(int) $sessionId.'__'.(int) $groupId;
2760
    }
2761
2762
    /**
2763
     * Fix a document name adding session id and group id
2764
     * Turns picture.jpg -> picture__1__2.jpg
2765
     * Where 1 = session id and 2 group id
2766
     * Of session id and group id are empty then the function returns:
2767
     * picture.jpg ->  picture.jpg.
2768
     *
2769
     * @param string $name       folder or file name
2770
     * @param string $type       'folder' or 'file'
2771
     * @param array  $courseInfo
2772
     * @param int    $sessionId
2773
     * @param int    $groupId
2774
     *
2775
     * @return string
2776
     */
2777
    public static function fixDocumentName($name, $type, $courseInfo, $sessionId, $groupId)
2778
    {
2779
        $suffix = self::getDocumentSuffix($courseInfo, $sessionId, $groupId);
2780
2781
        switch ($type) {
2782
            case 'folder':
2783
                $name = $name.$suffix;
2784
                break;
2785
            case 'file':
2786
                $name = self::addSuffixToFileName($name, $suffix);
2787
                break;
2788
        }
2789
2790
        return $name;
2791
    }
2792
2793
    /**
2794
     * Add a suffix to a file Example:
2795
     * /folder/picture.jpg => to /folder/picture_this.jpg
2796
     * where "_this" is the suffix.
2797
     *
2798
     * @param string $name
2799
     * @param string $suffix
2800
     *
2801
     * @return string
2802
     */
2803
    public static function addSuffixToFileName($name, $suffix)
2804
    {
2805
        $extension = pathinfo($name, PATHINFO_EXTENSION);
2806
        $fileName = pathinfo($name, PATHINFO_FILENAME);
2807
        $dir = pathinfo($name, PATHINFO_DIRNAME);
2808
2809
        if ('.' == $dir) {
2810
            $dir = null;
2811
        }
2812
2813
        if (!empty($dir) && '/' != $dir) {
2814
            $dir = $dir.'/';
2815
        }
2816
2817
        $name = $dir.$fileName.$suffix.'.'.$extension;
2818
2819
        return $name;
2820
    }
2821
2822
    /**
2823
     * Check if folder exist in the course base or in the session course.
2824
     *
2825
     * @param string $folder     Example: /folder/folder2
2826
     * @param array  $courseInfo
2827
     * @param int    $sessionId
2828
     * @param int    $groupId    group.id
2829
     *
2830
     * @return bool
2831
     */
2832
    public static function folderExists(
2833
        $folder,
2834
        $courseInfo,
2835
        $sessionId,
2836
        $groupId
2837
    ) {
2838
        $courseId = $courseInfo['real_id'];
2839
2840
        if (empty($courseId)) {
2841
            return false;
2842
        }
2843
2844
        $sessionId = (int) $sessionId;
2845
        $folderWithSuffix = self::fixDocumentName(
2846
            $folder,
2847
            'folder',
2848
            $courseInfo,
2849
            $sessionId,
2850
            $groupId
2851
        );
2852
2853
        $folder = Database::escape_string($folder);
2854
        $folderWithSuffix = Database::escape_string($folderWithSuffix);
2855
2856
        // Check if pathname already exists inside document table
2857
        $tbl_document = Database::get_course_table(TABLE_DOCUMENT);
2858
        $sql = "SELECT iid, path FROM $tbl_document
2859
                WHERE
2860
                    filetype = 'folder' AND
2861
                    c_id = $courseId AND
2862
                    (path = '$folder' OR path = '$folderWithSuffix') AND
2863
                    (session_id = 0 OR session_id IS NULL OR session_id = $sessionId)
2864
        ";
2865
2866
        $rs = Database::query($sql);
2867
        if (Database::num_rows($rs)) {
2868
            return true;
2869
        }
2870
2871
        return false;
2872
    }
2873
2874
    /**
2875
     * Check if file exist in the course base or in the session course.
2876
     *
2877
     * @param string $fileName   Example: /folder/picture.jpg
2878
     * @param array  $courseInfo
2879
     * @param int    $sessionId
2880
     * @param int    $groupId
2881
     *
2882
     * @return bool
2883
     */
2884
    public static function documentExists(
2885
        $fileName,
2886
        $courseInfo,
2887
        $sessionId,
2888
        $groupId
2889
    ) {
2890
        $courseId = $courseInfo['real_id'];
2891
2892
        if (empty($courseId)) {
2893
            return false;
2894
        }
2895
2896
        $sessionId = (int) $sessionId;
2897
        $fileNameEscape = Database::escape_string($fileName);
2898
2899
        $fileNameWithSuffix = self::fixDocumentName(
2900
            $fileName,
2901
            'file',
2902
            $courseInfo,
2903
            $sessionId,
2904
            $groupId
2905
        );
2906
2907
        $fileNameWithSuffix = Database::escape_string($fileNameWithSuffix);
2908
2909
        // Check if pathname already exists inside document table
2910
        $table = Database::get_course_table(TABLE_DOCUMENT);
2911
        $sql = "SELECT iid, title FROM $table
2912
                WHERE
2913
                    filetype = 'file' AND
2914
                    c_id = $courseId AND
2915
                    (
2916
                        title = '".$fileNameEscape."' OR
2917
                        title = '$fileNameWithSuffix'
2918
                    ) AND
2919
                    (session_id = 0 OR session_id = $sessionId)
2920
        ";
2921
        $rs = Database::query($sql);
2922
        if (Database::num_rows($rs)) {
2923
            return true;
2924
        }
2925
2926
        return false;
2927
    }
2928
2929
    /**
2930
     * @param string $path
2931
     * @param string $name
2932
     * @param array  $courseInfo
2933
     * @param int    $sessionId
2934
     * @param int    $groupId
2935
     *
2936
     * @return string
2937
     */
2938
    public static function getUniqueFileName($path, $name, $courseInfo, $sessionId, $groupId)
2939
    {
2940
        $counter = 1;
2941
        $filePath = $path.$name;
2942
        $uniqueName = $name;
2943
        $baseName = pathinfo($name, PATHINFO_FILENAME);
2944
        $extension = pathinfo($name, PATHINFO_EXTENSION);
2945
2946
        return uniqid($baseName.'-', true).'.'.$extension;
2947
2948
        while ($documentExists = self::documentExists(
0 ignored issues
show
Unused Code introduced by
WhileNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
2949
            $filePath,
2950
            $courseInfo,
2951
            $sessionId,
2952
            $groupId
2953
        )) {
2954
            $uniqueName = self::addSuffixToFileName($name, '_'.$counter);
2955
            $filePath = $path.$uniqueName;
2956
            $counter++;
2957
        }
2958
2959
        return $uniqueName;
2960
    }
2961
2962
    /**
2963
     * Builds the form that enables the user to
2964
     * select a directory to browse/upload in.
2965
     *
2966
     * @param array    An array containing the folders we want to be able to select
2967
     * @param string    The current folder (path inside of the "document" directory, including the prefix "/")
2968
     * @param string    Group directory, if empty, prevents documents to be uploaded
2969
     * (because group documents cannot be uploaded in root)
2970
     * @param bool    Whether to change the renderer (this will add a template <span>
2971
     * to the QuickForm object displaying the form)
2972
     *
2973
     * @return string html form
2974
     */
2975
    public static function build_directory_selector(
2976
        $folders,
2977
        $document_id,
2978
        $group_dir = '',
2979
        $change_renderer = false,
2980
        &$form = null,
2981
        $selectName = 'id'
2982
    ) {
2983
        $doc_table = Database::get_course_table(TABLE_DOCUMENT);
2984
        $course_id = api_get_course_int_id();
2985
        $folder_titles = [];
2986
2987
        if (is_array($folders)) {
2988
            $escaped_folders = [];
2989
            foreach ($folders as $key => &$val) {
2990
                $escaped_folders[$key] = Database::escape_string($val);
2991
            }
2992
            $folder_sql = implode("','", $escaped_folders);
2993
2994
            $sql = "SELECT DISTINCT docs.title, n.path
2995
                    FROM resource_node AS n
2996
                    INNER JOIN $doc_table AS docs
2997
                    ON (docs.resource_node_id = n.id)
2998
                    INNER JOIN resource_link l
2999
                    ON (l.resource_node_id = n.id)
3000
                    WHERE
3001
                        l.c_id = $course_id AND
3002
                        docs.filetype = 'folder' AND
3003
                        n.path IN ('".$folder_sql."') AND
3004
                        l.deleted_at IS NULL
3005
                         ";
3006
3007
            /*$sql = "SELECT path, title
3008
                    FROM $doc_table
3009
                    WHERE
3010
                        filetype = 'folder' AND
3011
                        c_id = $course_id AND
3012
                        path IN ('".$folder_sql."') ";*/
3013
            $res = Database::query($sql);
3014
            $folder_titles = [];
3015
            while ($obj = Database::fetch_object($res)) {
3016
                $folder_titles[$obj->path] = $obj->title;
3017
            }
3018
        }
3019
3020
        $attributes = [];
3021
        if (empty($form)) {
3022
            $form = new FormValidator('selector', 'GET', api_get_self().'?'.api_get_cidreq());
3023
            $attributes = ['onchange' => 'javascript: document.selector.submit();'];
3024
        }
3025
        $form->addElement('hidden', 'cidReq', api_get_course_id());
3026
        $form->addElement('hidden', 'cid', api_get_course_int_id());
3027
        $form->addElement('hidden', 'sid', api_get_session_id());
3028
        $form->addElement('hidden', 'gid', api_get_group_id());
3029
3030
        $parent_select = $form->addSelect(
3031
            $selectName,
3032
            get_lang('Current folder'),
3033
            [],
3034
            $attributes
3035
        );
3036
3037
        // Group documents cannot be uploaded in the root
3038
        if (empty($group_dir)) {
3039
            $parent_select->addOption(get_lang('Documents'), '/');
3040
3041
            if (is_array($folders)) {
3042
                foreach ($folders as $folder_id => &$folder) {
3043
                    if (!isset($folder_titles[$folder])) {
3044
                        continue;
3045
                    }
3046
                    $selected = ($document_id == $folder_id) ? ' selected="selected"' : '';
3047
                    $path_parts = explode('/', $folder);
3048
                    $folder_titles[$folder] = cut($folder_titles[$folder], 80);
3049
                    $counter = count($path_parts) - 2;
3050
                    if ($counter > 0) {
3051
                        $label = str_repeat('&nbsp;&nbsp;&nbsp;', $counter).' &mdash; '.$folder_titles[$folder];
3052
                    } else {
3053
                        $label = ' &mdash; '.$folder_titles[$folder];
3054
                    }
3055
                    $parent_select->addOption($label, $folder_id);
3056
                    if ('' != $selected) {
3057
                        $parent_select->setSelected($folder_id);
3058
                    }
3059
                }
3060
            }
3061
        } else {
3062
            if (!empty($folders)) {
3063
                foreach ($folders as $folder_id => &$folder) {
3064
                    $selected = ($document_id == $folder_id) ? ' selected="selected"' : '';
3065
                    $label = $folder_titles[$folder];
3066
                    if ($folder == $group_dir) {
3067
                        $label = get_lang('Documents');
3068
                    } else {
3069
                        $path_parts = explode('/', str_replace($group_dir, '', $folder));
3070
                        $label = cut($label, 80);
3071
                        $label = str_repeat('&nbsp;&nbsp;&nbsp;', count($path_parts) - 2).' &mdash; '.$label;
3072
                    }
3073
                    $parent_select->addOption($label, $folder_id);
3074
                    if ('' != $selected) {
3075
                        $parent_select->setSelected($folder_id);
3076
                    }
3077
                }
3078
            }
3079
        }
3080
3081
        return $form->toHtml();
3082
    }
3083
3084
    /**
3085
     * Builds an img html tag for the file type.
3086
     *
3087
     * @param string $type            (file/folder)
3088
     * @param string $path
3089
     * @param bool   $isAllowedToEdit
3090
     *
3091
     * @return string img html tag
3092
     */
3093
    public static function build_document_icon_tag($type, $path, $isAllowedToEdit = null)
3094
    {
3095
        $basename = basename($path);
3096
        $sessionId = api_get_session_id();
3097
        if (is_null($isAllowedToEdit)) {
3098
            $isAllowedToEdit = api_is_allowed_to_edit(null, true);
3099
        }
3100
        $user_image = false;
3101
        if ('file' == $type) {
3102
            $icon = choose_image($basename);
3103
            $basename = substr(strrchr($basename, '.'), 1);
3104
        } elseif ('link' == $type) {
3105
            $icon = 'clouddoc.png';
3106
            $basename = get_lang('Cloud file link');
3107
        } else {
3108
            if ('/shared_folder' == $path) {
3109
                $icon = 'folder_users.png';
3110
                if ($isAllowedToEdit) {
3111
                    $basename = get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:
3112
The users folder contains a folder for each user who has accessed it through the documents tool, or when any file has been sent in the course through the online editor. If neither circumstances has occurred, then no user folder will have been created. In the case of groups, files that are sent through the editor will be added in the folder of each group, which is only accessible by students from this group.');
3113
                } else {
3114
                    $basename = get_lang('Folders of users');
3115
                }
3116
            } elseif (strstr($basename, 'sf_user_')) {
3117
                $userInfo = api_get_user_info(substr($basename, 8));
3118
                $icon = $userInfo['avatar_small'];
3119
                $basename = get_lang('User folder').' '.$userInfo['complete_name'];
3120
                $user_image = true;
3121
            } elseif (strstr($path, 'shared_folder_session_')) {
3122
                $sessionName = api_get_session_name($sessionId);
3123
                if ($isAllowedToEdit) {
3124
                    $basename = '***('.$sessionName.')*** '.get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:
3125
The users folder contains a folder for each user who has accessed it through the documents tool, or when any file has been sent in the course through the online editor. If neither circumstances has occurred, then no user folder will have been created. In the case of groups, files that are sent through the editor will be added in the folder of each group, which is only accessible by students from this group.');
3126
                } else {
3127
                    $basename = get_lang('Folders of users').' ('.$sessionName.')';
3128
                }
3129
                $icon = 'folder_users.png';
3130
            } else {
3131
                $icon = 'folder_document.png';
3132
3133
                if ('/audio' == $path) {
3134
                    $icon = 'folder_audio.png';
3135
                    if ($isAllowedToEdit) {
3136
                        $basename = get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:
3137
This folder contains the default archives. You can clear files or add new ones, but if a file is hidden when it is inserted in a web document, the students will not be able to see it in this document. When inserting a file in a web document, first make sure it is visible. The folders can remain hidden.');
3138
                    } else {
3139
                        $basename = get_lang('Audio');
3140
                    }
3141
                } elseif ('/flash' == $path) {
3142
                    $icon = 'folder_flash.png';
3143
                    if ($isAllowedToEdit) {
3144
                        $basename = get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:
3145
This folder contains the default archives. You can clear files or add new ones, but if a file is hidden when it is inserted in a web document, the students will not be able to see it in this document. When inserting a file in a web document, first make sure it is visible. The folders can remain hidden.');
3146
                    } else {
3147
                        $basename = get_lang('Flash');
3148
                    }
3149
                } elseif ('/images' == $path) {
3150
                    $icon = 'folder_images.png';
3151
                    if ($isAllowedToEdit) {
3152
                        $basename = get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:
3153
This folder contains the default archives. You can clear files or add new ones, but if a file is hidden when it is inserted in a web document, the students will not be able to see it in this document. When inserting a file in a web document, first make sure it is visible. The folders can remain hidden.');
3154
                    } else {
3155
                        $basename = get_lang('Images');
3156
                    }
3157
                } elseif ('/video' == $path) {
3158
                    $icon = 'folder_video.png';
3159
                    if ($isAllowedToEdit) {
3160
                        $basename = get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:
3161
This folder contains the default archives. You can clear files or add new ones, but if a file is hidden when it is inserted in a web document, the students will not be able to see it in this document. When inserting a file in a web document, first make sure it is visible. The folders can remain hidden.');
3162
                    } else {
3163
                        $basename = get_lang('Video');
3164
                    }
3165
                } elseif ('/images/gallery' == $path) {
3166
                    $icon = 'folder_gallery.png';
3167
                    if ($isAllowedToEdit) {
3168
                        $basename = get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:
3169
This folder contains the default archives. You can clear files or add new ones, but if a file is hidden when it is inserted in a web document, the students will not be able to see it in this document. When inserting a file in a web document, first make sure it is visible. The folders can remain hidden.');
3170
                    } else {
3171
                        $basename = get_lang('Gallery');
3172
                    }
3173
                } elseif ('/chat_files' == $path) {
3174
                    $icon = 'folder_chat.png';
3175
                    if ($isAllowedToEdit) {
3176
                        $basename = get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:
3177
This folder contains all sessions that have been opened in the chat. Although the chat sessions can often be trivial, others can be really interesting and worthy of being incorporated as an additional work document. To do this without changing the visibility of this folder, make the file visible and link it from where you deem appropriate. It is not recommended to make this folder visible to all.');
3178
                    } else {
3179
                        $basename = get_lang('Chat conversations history');
3180
                    }
3181
                } elseif ('/learning_path' == $path) {
3182
                    $icon = 'folder_learningpath.png';
3183
                    if ($isAllowedToEdit) {
3184
                        $basename = get_lang('INFORMATION VISIBLE TO THE TEACHER ONLY:\nThis folder contains documents which are created by the Learning Path tool. Inside this folder you can edit the HTML file which are generated by importing content from the Learning Path, as the ones imported by Chamilo Rapid, for example. We recommend hiding this folder to your students.');
3185
                    } else {
3186
                        $basename = get_lang('Learning paths');
3187
                    }
3188
                }
3189
            }
3190
        }
3191
3192
        if ($user_image) {
3193
            return Display::img($icon, $basename, [], false);
3194
        }
3195
3196
        return Display::return_icon($icon, $basename);
3197
    }
3198
3199
    public static function isBasicCourseFolder($path, $sessionId)
3200
    {
3201
        $cleanPath = Security::remove_XSS($path);
3202
        $basicCourseFolder = '/basic-course-documents__'.$sessionId.'__0';
3203
3204
        return $cleanPath == $basicCourseFolder;
3205
    }
3206
3207
    /**
3208
     * Adds a new document to the database.
3209
     *
3210
     * @param array  $courseInfo
3211
     * @param string $path
3212
     * @param string $fileType
3213
     * @param int    $fileSize
3214
     * @param string $title
3215
     * @param string $comment
3216
     * @param int    $readonly
3217
     * @param int    $visibility       see ResourceLink constants
3218
     * @param int    $groupId          group.id
3219
     * @param int    $sessionId        Session ID, if any
3220
     * @param int    $userId           creator user id
3221
     * @param bool   $sendNotification
3222
     * @param string $content
3223
     * @param int    $parentId
3224
     * @param string $realPath
3225
     *
3226
     * @return CDocument|false
3227
     */
3228
    public static function addDocument(
3229
        $courseInfo,
3230
        $path,
3231
        $fileType,
3232
        $fileSize,
3233
        $title,
3234
        $comment = null,
3235
        $readonly = 0,
3236
        $visibility = null,
3237
        $groupId = 0,
3238
        $sessionId = 0,
3239
        $userId = 0,
3240
        $sendNotification = true,
3241
        $content = '',
3242
        $parentId = 0,
3243
        $realPath = ''
3244
    ) {
3245
        $userId = empty($userId) ? api_get_user_id() : $userId;
3246
        if (empty($userId)) {
3247
            return false;
3248
        }
3249
3250
        $userEntity = api_get_user_entity($userId);
3251
        if (null === $userEntity) {
3252
            return false;
3253
        }
3254
3255
        $courseEntity = api_get_course_entity($courseInfo['real_id']);
3256
        if (null === $courseEntity) {
3257
            return false;
3258
        }
3259
3260
        $sessionId = empty($sessionId) ? api_get_session_id() : $sessionId;
3261
        $session = api_get_session_entity($sessionId);
3262
        $group = api_get_group_entity($groupId);
3263
        $readonly = (int) $readonly;
3264
        $documentRepo = Container::getDocumentRepository();
3265
3266
        /** @var \Chamilo\CoreBundle\Entity\AbstractResource $parentResource */
3267
        $parentResource = $courseEntity;
3268
        if (!empty($parentId)) {
3269
            $parent = $documentRepo->find($parentId);
3270
            if ($parent) {
3271
                $parentResource = $parent;
3272
            }
3273
        }
3274
3275
        $document = $documentRepo->findCourseResourceByTitle(
3276
            $title,
3277
            $parentResource->getResourceNode(),
3278
            $courseEntity,
3279
            $session,
3280
            $group
3281
        );
3282
3283
        // Document already exists
3284
        if (null !== $document) {
3285
            // Keep the contextual tree consistent: set ResourceLink.parent when needed.
3286
            self::syncResourceLinkParentForContext($document, $parentResource, $courseEntity, $session, $group);
3287
3288
            return $document;
3289
        }
3290
3291
        // is updated using the title
3292
        $document = (new CDocument())
3293
            ->setFiletype($fileType)
3294
            ->setTitle($title)
3295
            ->setComment($comment)
3296
            ->setReadonly(1 === $readonly)
3297
            ->setCreator(api_get_user_entity())
3298
            ->setParent($parentResource)
3299
            ->addCourseLink($courseEntity, $session, $group)
3300
        ;
3301
3302
        $em = Database::getManager();
3303
        $em->persist($document);
3304
        $em->flush();
3305
3306
        // Ensure contextual hierarchy (course/session/group) uses ResourceLink.parent.
3307
        self::syncResourceLinkParentForContext($document, $parentResource, $courseEntity, $session, $group);
3308
3309
        $repo = Container::getDocumentRepository();
3310
        if (!empty($content)) {
3311
            $repo->addFileFromString($document, $title, 'text/html', $content, true);
3312
        } else {
3313
            if (!empty($realPath) && !is_dir($realPath) && file_exists($realPath)) {
3314
                $repo->addFileFromPath($document, $title, $realPath);
3315
            }
3316
        }
3317
3318
        if ($document) {
3319
            $allowNotification = ('true' === api_get_setting('document.send_notification_when_document_added'));
3320
            if ($sendNotification && $allowNotification) {
3321
                $courseTitle = $courseEntity->getTitle();
3322
                if (!empty($sessionId)) {
3323
                    $sessionInfo = api_get_session_info($sessionId);
3324
                    $courseTitle .= ' ( '.$sessionInfo['name'].') ';
3325
                }
3326
3327
                $url = api_get_path(WEB_CODE_PATH).
3328
                    'document/showinframes.php?cid='.$courseEntity->getId().'&sid='.$sessionId.'&id='.$document->getIid();
3329
                $link = Display::url(basename($title), $url, ['target' => '_blank']);
3330
                $userInfo = api_get_user_info($userId);
3331
                $message = sprintf(
3332
                    get_lang('A new document %s has been added to the document tool in your course %s by %s.'),
3333
                    $link,
3334
                    $courseTitle,
3335
                    $userInfo['complete_name']
3336
                );
3337
                $subject = sprintf(get_lang('New document added to course %s'), $courseTitle);
3338
                MessageManager::sendMessageToAllUsersInCourse($subject, $message, $courseEntity, $sessionId);
3339
            }
3340
3341
            return $document;
3342
        }
3343
3344
        return false;
3345
    }
3346
3347
    private static function syncResourceLinkParentForContext(
3348
        CDocument $child,
3349
                  $parentResource,
3350
                  $courseEntity,
3351
                  $session,
3352
                  $group
3353
    ): void {
3354
        // Only set a parent link when the parent is another document (folder).
3355
        if (!$parentResource instanceof CDocument) {
3356
            return; // Root items must keep rl.parent = NULL
3357
        }
3358
3359
        if (!method_exists($child, 'getResourceNode') || !method_exists($parentResource, 'getResourceNode')) {
3360
            return;
3361
        }
3362
3363
        $childNode = $child->getResourceNode();
3364
        $parentNode = $parentResource->getResourceNode();
3365
3366
        if (!$childNode instanceof ResourceNode || !$parentNode instanceof ResourceNode) {
3367
            return;
3368
        }
3369
3370
        try {
3371
            $em = Database::getManager();
3372
3373
            $childLink = self::findResourceLinkForContext($em, $childNode, $courseEntity, $session, $group);
3374
            $parentLink = self::findResourceLinkForContext($em, $parentNode, $courseEntity, $session, $group);
3375
3376
            if (!$childLink instanceof ResourceLink || !$parentLink instanceof ResourceLink) {
3377
                // If a link is missing, we don't hard-fail the import.
3378
                error_log('[IMPORT] ResourceLink not found for context when syncing parent.', [
3379
                    'childNodeId' => method_exists($childNode, 'getId') ? $childNode->getId() : null,
3380
                    'parentNodeId' => method_exists($parentNode, 'getId') ? $parentNode->getId() : null,
3381
                ]);
3382
3383
                return;
3384
            }
3385
3386
            $currentParent = method_exists($childLink, 'getParent') ? $childLink->getParent() : null;
3387
3388
            // Avoid useless flushes
3389
            if ($currentParent && method_exists($currentParent, 'getId') && method_exists($parentLink, 'getId')) {
3390
                if ((int) $currentParent->getId() === (int) $parentLink->getId()) {
3391
                    return;
3392
                }
3393
            }
3394
3395
            $childLink->setParent($parentLink);
3396
            $em->persist($childLink);
3397
            $em->flush();
3398
        } catch (\Throwable $e) {
3399
            error_log('[IMPORT] Failed to sync ResourceLink.parent for context: '.$e->getMessage());
3400
        }
3401
    }
3402
3403
    private static function findResourceLinkForContext(
3404
        $em,
3405
        ResourceNode $node,
3406
        $courseEntity,
3407
        $session,
3408
        $group
3409
    ): ?ResourceLink {
3410
        try {
3411
            $qb = $em->createQueryBuilder()
3412
                ->select('rl')
3413
                ->from(ResourceLink::class, 'rl')
3414
                ->andWhere('rl.resourceNode = :node')
3415
                ->setParameter('node', $node);
3416
3417
            // Course context
3418
            if ($courseEntity) {
3419
                $qb->andWhere('rl.course = :course')->setParameter('course', $courseEntity);
3420
            } else {
3421
                $qb->andWhere('rl.course IS NULL');
3422
            }
3423
3424
            // Session context
3425
            if ($session) {
3426
                $qb->andWhere('rl.session = :session')->setParameter('session', $session);
3427
            } else {
3428
                $qb->andWhere('rl.session IS NULL');
3429
            }
3430
3431
            // Group context
3432
            if ($group) {
3433
                $qb->andWhere('rl.group = :group')->setParameter('group', $group);
3434
            } else {
3435
                $qb->andWhere('rl.group IS NULL');
3436
            }
3437
3438
            $qb->setMaxResults(1);
3439
3440
            /** @var ResourceLink|null $link */
3441
            $link = $qb->getQuery()->getOneOrNullResult();
3442
3443
            return $link;
3444
        } catch (\Throwable $e) {
3445
            error_log('[IMPORT] Failed to find ResourceLink for context: '.$e->getMessage());
3446
3447
            return null;
3448
        }
3449
    }
3450
}
3451