LaravelH5pStorage   F
last analyzed

Complexity

Total Complexity 73

Size/Duplication

Total Lines 575
Duplicated Lines 0 %

Importance

Changes 7
Bugs 3 Features 1
Metric Value
eloc 161
c 7
b 3
f 1
dl 0
loc 575
rs 2.56
wmc 73

28 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getContent() 0 3 1
A saveExport() 0 10 3
A hasWriteAccess() 0 3 1
A exportLibrary() 0 5 2
A getEditorPath() 0 3 1
A exportContent() 0 9 2
A deleteCachedAssets() 0 7 4
A dirReady() 0 24 5
A cloneContentFile() 0 26 4
A saveContent() 0 8 1
B cacheAssets() 0 36 7
A getCachedAssets() 0 21 4
A moveContentDirectory() 0 30 6
A hasExport() 0 5 1
A getContentFile() 0 5 2
A saveLibrary() 0 9 1
A saveFile() 0 25 3
A removeContentFile() 0 5 2
A deleteContent() 0 3 1
A deleteExport() 0 5 2
A getIgnoredFiles() 0 12 3
A getTmpPath() 0 6 1
A cloneContent() 0 5 2
B copyFileTree() 0 25 10
A saveFileFromZip() 0 3 1
A getUpgradeScript() 0 2 1
A hasPresave() 0 2 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
/*
4
 *
5
 * @Project        Expression project.displayName is undefined on line 5, column 35 in Templates/Licenses/license-default.txt.
6
 * @Copyright      Djoudi
7
 * @Created        2017-02-11
8
 * @Filename       FilesStorage.php
9
 * @Description
10
 *
11
 */
12
13
namespace Djoudi\LaravelH5p\Storages;
14
15
use H5PFileStorage;
16
17
//use Illuminate\Filesystem\Filesystem;
18
//use Symfony\Component\Finder\Finder;
19
20
class LaravelH5pStorage implements H5PFileStorage
21
{
22
    private $path;
23
    private $alteditorpath;
24
25
    /**
26
     * The great Constructor!
27
     *
28
     * @param string $path
29
     *                              The base location of H5P files
30
     * @param string $alteditorpath
31
     *                              Optional. Use a different editor path
32
     */
33
    public function __construct($path, $alteditorpath = null)
34
    {
35
        // Set H5P storage path
36
        $this->path = $path;
37
        $this->alteditorpath = $alteditorpath;
38
    }
39
40
    public function hasPresave($libraryName, $developmentPath = null)
41
    {
42
    }
43
44
    public function getUpgradeScript($machineName, $majorVersion, $minorVersion)
45
    {
46
    }
47
48
    /**
49
     * Store the library folder.
50
     *
51
     * @param array $library
52
     *                       Library properties
53
     */
54
    public function saveLibrary($library)
55
    {
56
        $dest = $this->path.'/libraries/'.\H5PCore::libraryToString($library, true);
57
58
        // Make sure destination dir doesn't exist
59
        \H5PCore::deleteFileTree($dest);
60
61
        // Move library folder
62
        self::copyFileTree($library['uploadDirectory'], $dest);
63
    }
64
65
    /**
66
     * Store the content folder.
67
     *
68
     * @param string $source
69
     *                        Path on file system to content directory.
70
     * @param array  $content
71
     *                        Content properties
72
     */
73
    public function saveContent($source, $content)
74
    {
75
        $dest = "{$this->path}/content/{$content['id']}";
76
77
        // Remove any old content
78
        \H5PCore::deleteFileTree($dest);
79
80
        self::copyFileTree($source, $dest);
81
    }
82
83
    /**
84
     * Remove content folder.
85
     *
86
     * @param array $content
87
     *                       Content properties
88
     */
89
    public function deleteContent($content)
90
    {
91
        \H5PCore::deleteFileTree("{$this->path}/content/{$content['id']}");
92
    }
93
94
    /**
95
     * Creates a stored copy of the content folder.
96
     *
97
     * @param string $id
98
     *                      Identifier of content to clone.
99
     * @param int    $newId
100
     *                      The cloned content's identifier
101
     */
102
    public function cloneContent($id, $newId)
103
    {
104
        $path = $this->path.'/content/';
105
        if (file_exists($path.$id)) {
106
            self::copyFileTree($path.$id, $path.$newId);
107
        }
108
    }
109
110
    /**
111
     * Get path to a new unique tmp folder.
112
     *
113
     * @return string
114
     *                Path
115
     */
116
    public function getTmpPath()
117
    {
118
        $temp = "{$this->path}/temp";
119
        self::dirReady($temp);
120
121
        return "{$temp}/".uniqid('h5p-');
122
    }
123
124
    /**
125
     * Fetch content folder and save in target directory.
126
     *
127
     * @param int    $id
128
     *                       Content identifier
129
     * @param string $target
130
     *                       Where the content folder will be saved
131
     */
132
    public function exportContent($id, $target)
133
    {
134
        $source = "{$this->path}/content/{$id}";
135
        if (file_exists($source)) {
136
            // Copy content folder if it exists
137
            self::copyFileTree($source, $target);
138
        } else {
139
            // No contnet folder, create emty dir for content.json
140
            self::dirReady($target);
141
        }
142
    }
143
144
    /**
145
     * Fetch library folder and save in target directory.
146
     *
147
     * @param array  $library
148
     *                                Library properties
149
     * @param string $target
150
     *                                Where the library folder will be saved
151
     * @param string $developmentPath
152
     *                                Folder that library resides in
153
     */
154
    public function exportLibrary($library, $target, $developmentPath = null)
155
    {
156
        $folder = \H5PCore::libraryToString($library, true);
157
        $srcPath = ($developmentPath === null ? "/libraries/{$folder}" : $developmentPath);
158
        self::copyFileTree("{$this->path}{$srcPath}", "{$target}/{$folder}");
159
    }
160
161
    /**
162
     * Save export in file system.
163
     *
164
     * @param string $source
165
     *                         Path on file system to temporary export file.
166
     * @param string $filename
167
     *                         Name of export file.
168
     *
169
     * @throws Exception Unable to save the file
170
     */
171
    public function saveExport($source, $filename)
172
    {
173
        $this->deleteExport($filename);
174
175
        if (!self::dirReady("{$this->path}/exports")) {
176
            throw new Exception('Unable to create directory for H5P export file.');
0 ignored issues
show
Bug introduced by
The type Djoudi\LaravelH5p\Storages\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
177
        }
178
179
        if (!copy($source, "{$this->path}/exports/{$filename}")) {
180
            throw new Exception('Unable to save H5P export file.');
181
        }
182
    }
183
184
    /**
185
     * Removes given export file.
186
     *
187
     * @param string $filename
188
     */
189
    public function deleteExport($filename)
190
    {
191
        $target = "{$this->path}/exports/{$filename}";
192
        if (file_exists($target)) {
193
            unlink($target);
194
        }
195
    }
196
197
    /**
198
     * Check if the given export file exists.
199
     *
200
     * @param string $filename
201
     *
202
     * @return bool
203
     */
204
    public function hasExport($filename)
205
    {
206
        $target = "{$this->path}/exports/{$filename}";
207
208
        return file_exists($target);
209
    }
210
211
    /**
212
     * Will concatenate all JavaScrips and Stylesheets into two files in order
213
     * to improve page performance.
214
     *
215
     * @param array  $files
216
     *                      A set of all the assets required for content to display
217
     * @param string $key
218
     *                      Hashed key for cached asset
219
     */
220
    public function cacheAssets(&$files, $key)
221
    {
222
        foreach ($files as $type => $assets) {
223
            if (empty($assets)) {
224
                continue; // Skip no assets
225
            }
226
227
            $content = '';
228
            foreach ($assets as $asset) {
229
                // Get content from asset file
230
                $assetContent = file_get_contents($this->path.$asset->path);
231
                $cssRelPath = preg_replace('/[^\/]+$/', '', $asset->path);
232
233
                // Get file content and concatenate
234
                if ($type === 'scripts') {
235
                    $content .= $assetContent.";\n";
236
                } else {
237
                    // Rewrite relative URLs used inside stylesheets
238
                    $content .= preg_replace_callback(
239
                        '/url\([\'"]?([^"\')]+)[\'"]?\)/i', function ($matches) use ($cssRelPath) {
240
                            if (preg_match("/^(data:|([a-z0-9]+:)?\/)/i", $matches[1]) === 1) {
241
                                return $matches[0]; // Not relative, skip
242
                            }
243
244
                            return 'url("../'.$cssRelPath.$matches[1].'")';
245
                        }, $assetContent)."\n";
246
                }
247
            }
248
249
            self::dirReady("{$this->path}/cachedassets");
250
            $ext = ($type === 'scripts' ? 'js' : 'css');
251
            $outputfile = "/cachedassets/{$key}.{$ext}";
252
            file_put_contents($this->path.$outputfile, $content);
253
            $files[$type] = [(object) [
254
                'path'    => $outputfile,
255
                'version' => '',
256
            ]];
257
        }
258
    }
259
260
    /**
261
     * Will check if there are cache assets available for content.
262
     *
263
     * @param string $key
264
     *                    Hashed key for cached asset
265
     *
266
     * @return array
267
     */
268
    public function getCachedAssets($key)
269
    {
270
        $files = [];
271
272
        $js = "/cachedassets/{$key}.js";
273
        if (file_exists($this->path.$js)) {
274
            $files['scripts'] = [(object) [
275
                'path'    => $js,
276
                'version' => '',
277
            ]];
278
        }
279
280
        $css = "/cachedassets/{$key}.css";
281
        if (file_exists($this->path.$css)) {
282
            $files['styles'] = [(object) [
283
                'path'    => $css,
284
                'version' => '',
285
            ]];
286
        }
287
288
        return empty($files) ? null : $files;
289
    }
290
291
    /**
292
     * Remove the aggregated cache files.
293
     *
294
     * @param array $keys
295
     *                    The hash keys of removed files
296
     */
297
    public function deleteCachedAssets($keys)
298
    {
299
        foreach ($keys as $hash) {
300
            foreach (['js', 'css'] as $ext) {
301
                $path = "{$this->path}/cachedassets/{$hash}.{$ext}";
302
                if (file_exists($path)) {
303
                    unlink($path);
304
                }
305
            }
306
        }
307
    }
308
309
    /**
310
     * Read file content of given file and then return it.
311
     *
312
     * @param string $file_path
313
     *
314
     * @return string
315
     */
316
    public function getContent($file_path)
317
    {
318
        return file_get_contents($file_path);
319
    }
320
321
    /**
322
     * Save files uploaded through the editor.
323
     * The files must be marked as temporary until the content form is saved.
324
     *
325
     * @param \H5peditorFile $file
326
     * @param int            $contentid
327
     */
328
    public function saveFile($file, $contentId)
329
    {
330
        // Prepare directory
331
        if (empty($contentId)) {
332
            // Should be in editor tmp folder
333
            $path = $this->getEditorPath();
334
        } else {
335
            // Should be in content folder
336
            $path = $this->path.'/content/'.$contentId;
337
        }
338
        $path .= '/'.$file->getType().'s';
339
340
        self::dirReady($path);
341
342
        // Add filename to path
343
        $path .= '/'.$file->getName();
344
345
        $fileData = $file->getData();
0 ignored issues
show
Bug introduced by
The method getData() does not exist on H5peditorFile. ( Ignorable by Annotation )

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

345
        /** @scrutinizer ignore-call */ 
346
        $fileData = $file->getData();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
346
        if ($fileData) {
347
            file_put_contents($path, $fileData);
348
        } else {
349
            copy($_FILES['file']['tmp_name'], $path);
350
        }
351
352
        return $file;
353
    }
354
355
    /**
356
     * Copy a file from another content or editor tmp dir.
357
     * Used when copy pasting content in H5P Editor.
358
     *
359
     * @param string     $file   path + name
360
     * @param string|int $fromid Content ID or 'editor' string
361
     * @param int        $toid   Target Content ID
362
     */
363
    public function cloneContentFile($file, $fromId, $toId)
364
    {
365
        // Determine source path
366
        if ($fromId === 'editor') {
367
            $sourcepath = $this->getEditorPath();
368
        } else {
369
            $sourcepath = "{$this->path}/content/{$fromId}";
370
        }
371
        $sourcepath .= '/'.$file;
372
373
        // Determine target path
374
        $filename = basename($file);
375
        $filedir = str_replace($filename, '', $file);
376
        $targetpath = "{$this->path}/content/{$toId}/{$filedir}";
377
378
        // Make sure it's ready
379
        self::dirReady($targetpath);
380
381
        $targetpath .= $filename;
382
383
        // Check to see if source exist and if target doesn't
384
        if (!file_exists($sourcepath) || file_exists($targetpath)) {
385
            return; // Nothing to copy from or target already exists
386
        }
387
388
        copy($sourcepath, $targetpath);
389
    }
390
391
    /**
392
     * Copy a content from one directory to another. Defaults to cloning
393
     * content from the current temporary upload folder to the editor path.
394
     *
395
     * @param string $source    path to source directory
396
     * @param string $contentId Id of content
397
     *
398
     * @return object Object containing h5p json and content json data
399
     */
400
    public function moveContentDirectory($source, $contentId = null)
401
    {
402
        if ($source === null) {
0 ignored issues
show
introduced by
The condition $source === null is always false.
Loading history...
403
            return;
404
        }
405
406
        if ($contentId === null || $contentId == 0) {
407
            $target = $this->getEditorPath();
408
        } else {
409
            // Use content folder
410
            $target = "{$this->path}/content/{$contentId}";
411
        }
412
413
        $contentSource = $source.DIRECTORY_SEPARATOR.'content';
414
        $contentFiles = array_diff(scandir($contentSource), ['.', '..', 'content.json']);
0 ignored issues
show
Bug introduced by
It seems like scandir($contentSource) can also be of type false; however, parameter $array1 of array_diff() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

414
        $contentFiles = array_diff(/** @scrutinizer ignore-type */ scandir($contentSource), ['.', '..', 'content.json']);
Loading history...
415
        foreach ($contentFiles as $file) {
416
            if (is_dir("{$contentSource}/{$file}")) {
417
                self::copyFileTree("{$contentSource}/{$file}", "{$target}/{$file}");
418
            } else {
419
                copy("{$contentSource}/{$file}", "{$target}/{$file}");
420
            }
421
        }
422
423
        // Successfully loaded content json of file into editor
424
        $h5pJson = $this->getContent($source.DIRECTORY_SEPARATOR.'h5p.json');
425
        $contentJson = $this->getContent($contentSource.DIRECTORY_SEPARATOR.'content.json');
426
427
        return (object) [
428
            'h5pJson'     => $h5pJson,
429
            'contentJson' => $contentJson,
430
        ];
431
    }
432
433
    /**
434
     * Checks to see if content has the given file.
435
     * Used when saving content.
436
     *
437
     * @param string $file      path + name
438
     * @param int    $contentId
439
     *
440
     * @return string File ID or NULL if not found
441
     */
442
    public function getContentFile($file, $contentId)
443
    {
444
        $path = "{$this->path}/content/{$contentId}/{$file}";
445
446
        return file_exists($path) ? $path : null;
447
    }
448
449
    /**
450
     * Checks to see if content has the given file.
451
     * Used when saving content.
452
     *
453
     * @param string $file      path + name
454
     * @param int    $contentid
455
     *
456
     * @return string|int File ID or NULL if not found
457
     */
458
    public function removeContentFile($file, $contentId)
459
    {
460
        $path = "{$this->path}/content/{$contentId}/{$file}";
461
        if (file_exists($path)) {
462
            unlink($path);
463
        }
464
    }
465
466
    /**
467
     * Check if server setup has write permission to
468
     * the required folders.
469
     *
470
     * @return bool True if site can write to the H5P files folder
471
     */
472
    public function hasWriteAccess()
473
    {
474
        return self::dirReady($this->path);
475
    }
476
477
    /**
478
     * Recursive function for copying directories.
479
     *
480
     * @param string $source
481
     *                            From path
482
     * @param string $destination
483
     *                            To path
484
     *
485
     * @throws Exception Unable to copy the file
486
     *
487
     * @return bool
488
     *              Indicates if the directory existed.
489
     */
490
    private static function copyFileTree($source, $destination)
491
    {
492
        if (!self::dirReady($destination)) {
493
            throw new \Exception('unabletocopy');
494
        }
495
496
        $ignoredFiles = self::getIgnoredFiles("{$source}/.h5pignore");
497
498
        $dir = opendir($source);
499
        if ($dir === false) {
500
            trigger_error('Unable to open directory '.$source, E_USER_WARNING);
501
502
            throw new \Exception('unabletocopy');
503
        }
504
505
        while (false !== ($file = readdir($dir))) {
506
            if (($file != '.') && ($file != '..') && $file != '.git' && $file != '.gitignore' && !in_array($file, $ignoredFiles)) {
507
                if (is_dir("{$source}/{$file}")) {
508
                    self::copyFileTree("{$source}/{$file}", "{$destination}/{$file}");
509
                } else {
510
                    copy("{$source}/{$file}", "{$destination}/{$file}");
511
                }
512
            }
513
        }
514
        closedir($dir);
515
    }
516
517
    /**
518
     * Retrieve array of file names from file.
519
     *
520
     * @param string $file
521
     *
522
     * @return array Array with files that should be ignored
523
     */
524
    private static function getIgnoredFiles($file)
525
    {
526
        if (file_exists($file) === false) {
527
            return [];
528
        }
529
530
        $contents = file_get_contents($file);
531
        if ($contents === false) {
532
            return [];
533
        }
534
535
        return preg_split('/\s+/', $contents);
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_split('/\s+/', $contents) could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
536
    }
537
538
    /**
539
     * Recursive function that makes sure the specified directory exists and
540
     * is writable.
541
     *
542
     * @param string $path
543
     *
544
     * @return bool
545
     */
546
    private static function dirReady($path)
547
    {
548
        if (!file_exists($path)) {
549
            $parent = preg_replace("/\/[^\/]+\/?$/", '', $path);
550
            if (!self::dirReady($parent)) {
551
                return false;
552
            }
553
554
            mkdir($path, 0777, true);
555
        }
556
557
        if (!is_dir($path)) {
558
            trigger_error('Path is not a directory '.$path, E_USER_WARNING);
559
560
            return false;
561
        }
562
563
        if (!is_writable($path)) {
564
            trigger_error('Unable to write to '.$path.' – check directory permissions –', E_USER_WARNING);
565
566
            return false;
567
        }
568
569
        return true;
570
    }
571
572
    /**
573
     * Easy helper function for retrieving the editor path.
574
     *
575
     * @return string Path to editor files
576
     */
577
    private function getEditorPath()
578
    {
579
        return $this->path.'/editor';
580
//        return ($this->alteditorpath !== NULL ? $this->alteditorpath : "{$this->path}/editor");
581
    }
582
583
    /**
584
     * Store the given stream into the given file.
585
     *
586
     * @param string   $path
587
     * @param string   $file
588
     * @param resource $stream
589
     *
590
     * @return bool
591
     */
592
    public function saveFileFromZip($path, $file, $stream)
593
    {
594
        return true;
595
    }
596
}
597