Passed
Pull Request — master (#6894)
by
unknown
09:02
created

FileExport   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 423
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 227
c 1
b 0
f 0
dl 0
loc 423
rs 6.96
wmc 53

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getMimeTypes() 0 19 1
A createPlaceholderFile() 0 4 1
B copyFileToExportDir() 0 27 7
A ensureTrailingSlash() 0 9 4
A createFileXmlEntry() 0 25 1
C getFilesData() 0 73 13
A getFolderFileData() 0 30 1
B exportFiles() 0 41 7
A createFilesXml() 0 11 2
B processDocument() 0 43 11
A __construct() 0 3 1
A getFileData() 0 39 3
A getMimeType() 0 6 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CourseBundle\Component\CourseCopy\Moodle\Builder;
8
9
use Chamilo\CoreBundle\Framework\Container;
10
use Chamilo\CourseBundle\Entity\CDocument;
11
use DocumentManager;
12
use Exception;
13
14
use const PATHINFO_EXTENSION;
15
use const PHP_EOL;
16
17
/**
18
 * Class FileExport.
19
 * Handles the export of files and metadata from Moodle courses.
20
 */
21
class FileExport
22
{
23
    /**
24
     * @var object
25
     */
26
    private $course;
27
28
    /**
29
     * Constructor to initialize course data.
30
     *
31
     * @param object $course course object containing resources and path data
32
     */
33
    public function __construct(object $course)
34
    {
35
        $this->course = $course;
36
    }
37
38
    /**
39
     * Export files and metadata from files.xml to the specified directory.
40
     */
41
    public function exportFiles(array $filesData, string $exportDir): void
42
    {
43
        $filesDir = $exportDir.'/files';
44
        if (!is_dir($filesDir)) {
45
            mkdir($filesDir, api_get_permissions_for_new_directories(), true);
46
        }
47
        $this->createPlaceholderFile($filesDir);
48
49
        $unique = ['files' => []];
50
        $seenKeys = []; // string => true
51
52
        foreach (($filesData['files'] ?? []) as $file) {
53
            $ch = (string) ($file['contenthash'] ?? '');
54
            $comp = (string) ($file['component'] ?? '');
55
            $area = (string) ($file['filearea'] ?? '');
56
            $path = $this->ensureTrailingSlash((string) ($file['filepath'] ?? '/'));
57
            $name = (string) ($file['filename'] ?? '');
58
59
            if ('' === $ch || '' === $name) {
60
                continue;
61
            }
62
63
            $dedupeKey = implode('|', [$ch, $comp, $area, $path, $name]);
64
            if (isset($seenKeys[$dedupeKey])) {
65
                continue;
66
            }
67
            $seenKeys[$dedupeKey] = true;
68
69
            FileIndex::register($file);
70
71
            $file['filepath'] = $path;
72
            $unique['files'][] = $file;
73
        }
74
75
        foreach ($unique['files'] as $file) {
76
            $ch = (string) $file['contenthash'];
77
            $subdir = FileIndex::resolveSubdirByContenthash($ch);
78
            $this->copyFileToExportDir($file, $filesDir, $subdir);
79
        }
80
81
        $this->createFilesXml($unique, $exportDir);
82
    }
83
84
    /**
85
     * Get file data from course resources. This is for testing purposes.
86
     *
87
     * @return array<string,mixed>
88
     */
89
    public function getFilesData(): array
90
    {
91
        $adminData = MoodleExport::getAdminUserData();
92
        $adminId = $adminData['id'] ?? 0;
93
94
        $filesData = ['files' => []];
95
96
        // Defensive read: documents may be missing
97
        $docResources = $this->course->resources[RESOURCE_DOCUMENT] ?? [];
98
        if (!\is_array($docResources)) {
99
            $docResources = [];
100
        }
101
102
        foreach ($docResources as $document) {
103
            $filesData = $this->processDocument($filesData, $document);
104
        }
105
106
        // Defensive read: works may be missing (avoids "Undefined array key 'work'")
107
        $workResources = $this->course->resources[RESOURCE_WORK] ?? [];
108
        if (!\is_array($workResources)) {
109
            $workResources = [];
110
        }
111
112
        foreach ($workResources as $work) {
113
            // getAllDocumentToWork might not exist in some installs; guard it
114
            $workFiles = \function_exists('getAllDocumentToWork')
115
                ? (getAllDocumentToWork($work->params['id'] ?? 0, $this->course->info['real_id'] ?? 0) ?: [])
116
                : [];
117
118
            if (!\is_array($workFiles) || empty($workFiles)) {
119
                continue;
120
            }
121
122
            foreach ($workFiles as $file) {
123
                // Safely fetch doc data
124
                $docId = (int) ($file['document_id'] ?? 0);
125
                if ($docId <= 0) {
126
                    continue;
127
                }
128
129
                $docData = DocumentManager::get_document_data_by_id(
0 ignored issues
show
Deprecated Code introduced by
The function DocumentManager::get_document_data_by_id() has been deprecated: use $repo->find() ( Ignorable by Annotation )

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

129
                $docData = /** @scrutinizer ignore-deprecated */ DocumentManager::get_document_data_by_id(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
130
                    $docId,
131
                    (string) ($this->course->info['code'] ?? '')
132
                );
133
134
                if (!\is_array($docData) || empty($docData['path'])) {
135
                    continue;
136
                }
137
138
                $filesData['files'][] = [
139
                    'id' => $docId,
140
                    'contenthash' => hash('sha1', basename($docData['path'])),
141
                    'contextid' => (int) ($this->course->info['real_id'] ?? 0),
142
                    'component' => 'mod_assign',
143
                    'filearea' => 'introattachment',
144
                    'itemid' => (int) ($work->params['id'] ?? 0),
145
                    'filepath' => '/Documents/',
146
                    'documentpath' => 'document/'.$docData['path'],
147
                    'filename' => basename($docData['path']),
148
                    'userid' => $adminId,
149
                    'filesize' => (int) ($docData['size'] ?? 0),
150
                    'mimetype' => $this->getMimeType($docData['path']),
151
                    'status' => 0,
152
                    'timecreated' => time() - 3600,
153
                    'timemodified' => time(),
154
                    'source' => (string) ($docData['title'] ?? ''),
155
                    'author' => 'Unknown',
156
                    'license' => 'allrightsreserved',
157
                ];
158
            }
159
        }
160
161
        return $filesData;
162
    }
163
164
    /**
165
     * Create a placeholder index.html file to prevent an empty directory.
166
     */
167
    private function createPlaceholderFile(string $filesDir): void
168
    {
169
        $placeholderFile = $filesDir.'/index.html';
170
        file_put_contents($placeholderFile, '<!-- Placeholder file to ensure the directory is not empty -->');
171
    }
172
173
    /**
174
     * Copy a file to the export directory using its contenthash.
175
     *
176
     * @param array<string,mixed> $file
177
     */
178
    private function copyFileToExportDir(array $file, string $filesDir, ?string $precomputedSubdir = null): void
179
    {
180
        if (($file['filepath'] ?? '.') === '.') {
181
            return;
182
        }
183
184
        $contenthash = (string) $file['contenthash'];
185
        $subDir = $precomputedSubdir ?: substr($contenthash, 0, 2);
186
        $exportSubDir = $filesDir.'/'.$subDir;
187
188
        if (!is_dir($exportSubDir)) {
189
            mkdir($exportSubDir, api_get_permissions_for_new_directories(), true);
190
        }
191
192
        $destinationFile = $exportSubDir.'/'.$contenthash;
193
194
        $filePath = $file['abs_path'] ?? null;
195
        if (!$filePath) {
196
            $filePath = $this->course->path.$file['documentpath'];
197
        }
198
199
        if (is_file($filePath)) {
200
            if (!is_file($destinationFile)) {
201
                copy($filePath, $destinationFile);
202
            }
203
        } else {
204
            throw new Exception("Source file not found: {$filePath}");
205
        }
206
    }
207
208
    /**
209
     * Create the files.xml with the provided file data.
210
     *
211
     * @param array<string,mixed> $filesData
212
     */
213
    private function createFilesXml(array $filesData, string $destinationDir): void
214
    {
215
        $xmlContent = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
216
        $xmlContent .= '<files>'.PHP_EOL;
217
218
        foreach (($filesData['files'] ?? []) as $file) {
219
            $xmlContent .= $this->createFileXmlEntry($file);
220
        }
221
222
        $xmlContent .= '</files>'.PHP_EOL;
223
        file_put_contents($destinationDir.'/files.xml', $xmlContent);
224
    }
225
226
    /**
227
     * Create an XML entry for a file.
228
     *
229
     * @param array<string,mixed> $file
230
     */
231
    private function createFileXmlEntry(array $file): string
232
    {
233
        // itemid is forced to 0 in V1 files.xml for consistency with restore
234
        return '  <file id="'.(int) $file['id'].'">'.PHP_EOL.
235
            '    <contenthash>'.htmlspecialchars((string) $file['contenthash']).'</contenthash>'.PHP_EOL.
236
            '    <contextid>'.(int) $file['contextid'].'</contextid>'.PHP_EOL.
237
            '    <component>'.htmlspecialchars((string) $file['component']).'</component>'.PHP_EOL.
238
            '    <filearea>'.htmlspecialchars((string) $file['filearea']).'</filearea>'.PHP_EOL.
239
            '    <itemid>0</itemid>'.PHP_EOL.
240
            '    <filepath>'.htmlspecialchars((string) $file['filepath']).'</filepath>'.PHP_EOL.
241
            '    <filename>'.htmlspecialchars((string) $file['filename']).'</filename>'.PHP_EOL.
242
            '    <userid>'.(int) $file['userid'].'</userid>'.PHP_EOL.
243
            '    <filesize>'.(int) $file['filesize'].'</filesize>'.PHP_EOL.
244
            '    <mimetype>'.htmlspecialchars((string) $file['mimetype']).'</mimetype>'.PHP_EOL.
245
            '    <status>'.(int) $file['status'].'</status>'.PHP_EOL.
246
            '    <timecreated>'.(int) $file['timecreated'].'</timecreated>'.PHP_EOL.
247
            '    <timemodified>'.(int) $file['timemodified'].'</timemodified>'.PHP_EOL.
248
            '    <source>'.htmlspecialchars((string) $file['source']).'</source>'.PHP_EOL.
249
            '    <author>'.htmlspecialchars((string) $file['author']).'</author>'.PHP_EOL.
250
            '    <license>'.htmlspecialchars((string) $file['license']).'</license>'.PHP_EOL.
251
            '    <sortorder>0</sortorder>'.PHP_EOL.
252
            '    <repositorytype>$@NULL@$</repositorytype>'.PHP_EOL.
253
            '    <repositoryid>$@NULL@$</repositoryid>'.PHP_EOL.
254
            '    <reference>$@NULL@$</reference>'.PHP_EOL.
255
            '  </file>'.PHP_EOL;
256
    }
257
258
    /**
259
     * Process a document or folder and add its data to the files array.
260
     *
261
     * @param array<string,mixed> $filesData
262
     */
263
    private function processDocument(array $filesData, object $document): array
264
    {
265
        // Skip files already embedded/handled by PageExport
266
        if (
267
            ($document->file_type ?? null) === 'file'
268
            && isset($this->course->used_page_doc_ids)
269
            && \in_array($document->source_id, (array) $this->course->used_page_doc_ids, true)
270
        ) {
271
            return $filesData;
272
        }
273
274
        // Skip top-level HTML documents that are exported as Page
275
        if (
276
            ($document->file_type ?? null) === 'file'
277
            && 'html' === pathinfo($document->path, PATHINFO_EXTENSION)
278
            && 1 === substr_count($document->path, '/')
279
        ) {
280
            return $filesData;
281
        }
282
283
        if (($document->file_type ?? null) === 'file') {
284
            $extension = strtolower((string) pathinfo($document->path, PATHINFO_EXTENSION));
285
            if (!\in_array($extension, ['html', 'htm'], true)) {
286
                $fileData = $this->getFileData($document);
287
                $fileData['filepath'] = '/Documents/';
288
                $fileData['contextid'] = 0;
289
                $fileData['component'] = 'mod_folder';
290
                $filesData['files'][] = $fileData;
291
            }
292
        } elseif (($document->file_type ?? null) === 'folder') {
293
            $docRepo = Container::getDocumentRepository();
294
            $folderFiles = $docRepo->listFilesByParentIid((int) $document->source_id);
295
296
            foreach ($folderFiles as $file) {
297
                $filesData['files'][] = $this->getFolderFileData(
298
                    $file,
299
                    (int) $document->source_id,
300
                    '/Documents/'.\dirname($file['path']).'/'
301
                );
302
            }
303
        }
304
305
        return $filesData;
306
    }
307
308
    /**
309
     * Get file data for a single document.
310
     *
311
     * @return array<string,mixed>
312
     */
313
    private function getFileData(object $document): array
314
    {
315
        $adminData = MoodleExport::getAdminUserData();
316
        $adminId = $adminData['id'] ?? 0;
317
318
        $contenthash = hash('sha1', basename($document->path));
319
        $mimetype = $this->getMimeType($document->path);
320
321
        // Try to resolve absolute path for single file documents
322
        $absPath = null;
323
        if (isset($document->source_id)) {
324
            $repo = Container::getDocumentRepository();
325
            $doc = $repo->findOneBy(['iid' => (int) $document->source_id]);
326
            if ($doc instanceof CDocument) {
327
                $absPath = $repo->getAbsolutePathForDocument($doc);
328
            }
329
        }
330
331
        return [
332
            'id' => (int) $document->source_id,
333
            'contenthash' => $contenthash,
334
            'contextid' => (int) $document->source_id,
335
            'component' => 'mod_resource',
336
            'filearea' => 'content',
337
            'itemid' => (int) $document->source_id,
338
            'filepath' => '/',
339
            'documentpath' => (string) $document->path,
340
            'filename' => basename($document->path),
341
            'userid' => $adminId,
342
            'filesize' => (int) $document->size,
343
            'mimetype' => $mimetype,
344
            'status' => 0,
345
            'timecreated' => time() - 3600,
346
            'timemodified' => time(),
347
            'source' => (string) $document->title,
348
            'author' => 'Unknown',
349
            'license' => 'allrightsreserved',
350
            // New: absolute path for reliable copy
351
            'abs_path' => $absPath,
352
        ];
353
    }
354
355
    /**
356
     * Get file data for files inside a folder.
357
     *
358
     * @param array<string,mixed> $file
359
     *
360
     * @return array<string,mixed>
361
     */
362
    private function getFolderFileData(array $file, int $sourceId, string $parentPath = '/Documents/'): array
363
    {
364
        $adminData = MoodleExport::getAdminUserData();
365
        $adminId = $adminData['id'] ?? 0;
366
367
        $contenthash = hash('sha1', basename($file['path']));
368
        $mimetype = $this->getMimeType($file['path']);
369
        $filename = basename($file['path']);
370
        $filepath = $this->ensureTrailingSlash($parentPath);
371
372
        return [
373
            'id' => (int) $file['id'],
374
            'contenthash' => $contenthash,
375
            'contextid' => $sourceId,
376
            'component' => 'mod_folder',
377
            'filearea' => 'content',
378
            'itemid' => (int) $file['id'],
379
            'filepath' => $filepath,
380
            'documentpath' => 'document/'.$file['path'],
381
            'filename' => $filename,
382
            'userid' => $adminId,
383
            'filesize' => (int) $file['size'],
384
            'mimetype' => $mimetype,
385
            'status' => 0,
386
            'timecreated' => time() - 3600,
387
            'timemodified' => time(),
388
            'source' => (string) $file['title'],
389
            'author' => 'Unknown',
390
            'license' => 'allrightsreserved',
391
            'abs_path' => $file['abs_path'] ?? null,
392
        ];
393
    }
394
395
    /**
396
     * Ensure the directory path has a trailing slash.
397
     */
398
    private function ensureTrailingSlash(string $path): string
399
    {
400
        if ('' === $path || '.' === $path || '/' === $path) {
401
            return '/';
402
        }
403
404
        $path = (string) preg_replace('/\/+/', '/', $path);
405
406
        return rtrim($path, '/').'/';
407
    }
408
409
    /**
410
     * Get MIME type based on the file extension.
411
     */
412
    public function getMimeType(string $filePath): string
413
    {
414
        $extension = strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION));
415
        $mimeTypes = $this->getMimeTypes();
416
417
        return $mimeTypes[$extension] ?? 'application/octet-stream';
418
    }
419
420
    /**
421
     * Get an array of file extensions and their corresponding MIME types.
422
     *
423
     * @return array<string,string>
424
     */
425
    private function getMimeTypes(): array
426
    {
427
        return [
428
            'pdf' => 'application/pdf',
429
            'jpg' => 'image/jpeg',
430
            'jpeg' => 'image/jpeg',
431
            'png' => 'image/png',
432
            'gif' => 'image/gif',
433
            'html' => 'text/html',
434
            'htm' => 'text/html',
435
            'txt' => 'text/plain',
436
            'doc' => 'application/msword',
437
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
438
            'xls' => 'application/vnd.ms-excel',
439
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
440
            'ppt' => 'application/vnd.ms-powerpoint',
441
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
442
            'zip' => 'application/zip',
443
            'rar' => 'application/x-rar-compressed',
444
        ];
445
    }
446
}
447