Passed
Push — master ( c0e398...586d6e )
by Nicolaas
06:47 queued 04:28
created

SortOutFolders::scaleUploadedImage()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 26
c 1
b 0
f 0
nc 9
nop 3
dl 0
loc 50
rs 6.9666

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace Sunnysideup\PerfectCmsImages\Api;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\HTTPRequest;
8
use SilverStripe\Dev\BuildTask;
9
use SilverStripe\ORM\DB;
10
11
use SilverStripe\Assets\Image;
12
use SilverStripe\Assets\Folder;
13
use SilverStripe\Assets\File;
14
15
use Sunnysideup\PerfectCmsImages\Api\PerfectCMSImages;
16
17
use SilverStripe\Core\Config\Config;
18
use SilverStripe\Core\Config\Configurable;
19
20
use SilverStripe\Core\ClassInfo;
21
22
use SilverStripe\Core\Injector\Injector;
23
24
25
/**
26
 * the assumption we make here is that a particular group of images (e.g. Page.Image) live
27
 * live in a particular folder.
28
 */
29
class SortOutFolders
30
{
31
32
    use Configurable;
33
34
    /**
35
     * the folder where we move images that are not in use
36
     * @var Folder
37
     */
38
    protected $unusedImagesFolder = null;
39
40
    /**
41
     * if set to true then dont do it for real!
42
     * @var bool
43
     */
44
    protected $dryRun = false;
45
46
    /**
47
     *
48
     * @var bool
49
     */
50
    protected $verbose = true;
51
52
    public function setVerbose(?bool $b = true)
53
    {
54
        $this->verbose = $b;
55
        return $this;
56
    }
57
58
    public function setDryRun(?bool $b = true)
59
    {
60
        $this->dryRun = $b;
61
        return $this;
62
    }
63
64
    public function runStandard()
65
    {
66
        $this->runAdvanced(
67
            Config::inst()->get(PerfectCMSImages::class, 'unused_images_folder_name'),
68
            PerfectCMSImages::get_all_values_for_images()
69
        );
70
    }
71
72
    /**
73
     * @param string $unusedFolderName
74
     * @param array $data
75
     * Create test jobs for the purposes of testing.
76
     * The array must contains arrays with
77
     * - folder
78
     * - used_by
79
     * used_by is an array that has ClassNames and Relations
80
     * (has_one / has_many / many_many relations)
81
     * e.g. Page.Image, MyDataObject.MyImages
82
     *
83
     * @param HTTPRequest $request
84
     */
85
    public function runAdvanced(string $unusedFolderName, array $data) // phpcs:ignore
86
    {
87
        $this->unusedImagesFolder = Folder::find_or_make($unusedFolderName);
88
89
        $folderArray = $this->getFolderArray($data);
90
        if ($this->verbose) {
91
            DB::alteration_message('==== List of folders ====');
92
            echo '<pre>'.print_r($folderArray, 1).'</pre>';
0 ignored issues
show
Bug introduced by
Are you sure print_r($folderArray, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

92
            echo '<pre>'./** @scrutinizer ignore-type */ print_r($folderArray, 1).'</pre>';
Loading history...
93
        }
94
95
        $listOfImageIds = $this->getListOfImages($folderArray);
96
97
        // remove
98
        foreach($listOfImageIds as $folderName => $listOfIds) {
99
            if($this->verbose) {
100
                DB::alteration_message('<br /><br /><br />==== Checking for images to remove from <u>'.$folderName.'</u>; there are '. count($listOfIds).' images to keep');
101
            }
102
            $imagesLeft[$folderName] = $this->removeUnusedFiles($folderName, $listOfIds);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $imagesLeft[$folderName] is correct as $this->removeUnusedFiles($folderName, $listOfIds) targeting Sunnysideup\PerfectCmsIm...rs::removeUnusedFiles() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
103
        }
104
105
        DB::alteration_message('==========================================');
106
        // move to right folder
107
        foreach($listOfImageIds as $folderName => $listOfIds) {
108
            if($this->verbose) {
109
                DB::alteration_message('<br /><br /><br />==== Checking for images to move to <u>'.$folderName.'</u>');
110
            }
111
            $this->moveUsedFilesIntoFolder($folderName, $listOfIds);
112
        }
113
114
        DB::alteration_message('==========================================');
115
116
        // check for rogue files
117
        foreach(array_keys($listOfImageIds) as $folderName) {
118
            if($this->verbose) {
119
                DB::alteration_message('<br /><br /><br />==== Checking for rogue FILES in <u>'.$folderName.'</u>');
120
            }
121
            $this->findRoqueFilesInFolder($folderName);
122
        }
123
    }
124
125
126
    public function getFolderArray(array $data) :array
127
    {
128
        // check folders
129
        $folderArray = [];
130
        foreach($data as $dataInner) {
131
            $folderName = $dataInner['folder'] ?? '';
132
            if($folderName) {
133
                $folderArray[$folderName] = [];
134
                $folderArray[$folderName]['classesAndMethods'] = [];
135
                $folderArray[$folderName]['resize'] = isset($dataInner['resize']) && $dataInner['resize'] === true  ? true : false;
136
                $classes = $dataInner['used_by'] ?? [];
137
                if(! empty($classes)) {
138
                    if(is_array($classes)) {
139
                        foreach($classes as $classAndMethod) {
140
                            $folderArray[$folderName]['classesAndMethods'][$classAndMethod] = $classAndMethod;
141
                        }
142
                    } else {
143
                        user_error('Bad definition for: '.print_r($dataInner, 1));
0 ignored issues
show
Bug introduced by
Are you sure print_r($dataInner, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

143
                        user_error('Bad definition for: './** @scrutinizer ignore-type */ print_r($dataInner, 1));
Loading history...
144
                    }
145
                }
146
            }
147
        }
148
        $test = [];
149
        foreach($folderArray as $folderName => $folderData) {
150
            $classAndMethodList = $folderData['classesAndMethods'];
151
            foreach($classAndMethodList as $classAndMethod) {
152
                if(! isset($test[$classAndMethod])) {
153
                    $test[$classAndMethod] = true;
154
                } else {
155
                    user_error('You have doubled up on folder for Class and Method: '.$classAndMethod);
156
                }
157
            }
158
        }
159
        return $folderArray;
160
    }
161
162
    public function getListOfImages(array $folderArray) : array
163
    {
164
        $listOfImageIds = [];
165
        foreach($folderArray as $folderName => $folderData) {
166
            $classAndMethodList = $folderData['classesAndMethods'];
167
168
            // find all images that should be there...
169
            $listOfIds = [];
170
            foreach($classAndMethodList as $classAndMethod) {
171
                $dataClassName = '';
172
                list($className, $method) = explode('.', $classAndMethod);
173
                $fieldDetails = $this->getFieldDetails($className, $method);
174
                if(empty($fieldDetails)) {
175
                    user_error('Could not find relation: '.$className.'.'.$method);
176
                }
177
                if($fieldDetails['dataType'] === 'has_one') {
178
                    $list = $className::get()->columnUnique($method.'ID');
179
                } else {
180
                    $dataClassName = $fieldDetails['dataClassName'];
0 ignored issues
show
Unused Code introduced by
The assignment to $dataClassName is dead and can be removed.
Loading history...
181
                    $outerList = $className::get();
182
                    $list = [];
183
                    foreach($outerList as $obj) {
184
                        $list = array_merge($list, $obj->$method()->columnUnique('ID'));
185
                    }
186
                    $list = array_unique($list);
187
                }
188
                DB::alteration_message($className . '::' .$method . ' resulted in '.count($list));
189
                $listOfIds = array_unique(
190
                    array_merge(
191
                        $listOfIds,
192
                        $list
193
                    )
194
                );
195
            }
196
            if(count($listOfIds)) {
197
                $listOfImageIds[$folderName] = $listOfIds;
198
            }
199
        }
200
        return $listOfImageIds;
201
    }
202
203
    /**
204
     * returns the images in the ID list that were not found in the folder.
205
     * @param  string $folderName                   Folder moving to
206
     * @param  array  $listOfImageIds               Images that should be in the folder
207
     */
208
    public function removeUnusedFiles(string $folderName, array $listOfImageIds)
209
    {
210
        $unusedFolderName = $this->unusedImagesFolder->Name;
211
        $folder = Folder::find_or_make($folderName);
212
        $this->writeFileOrFolder($folder);
213
        $listAsString = implode(',', $listOfImageIds);
214
        $where = ' ParentID = ' . $folder->ID. ' AND File.ID NOT IN('.$listAsString.')';
215
        $unused = Image::get()->where($where);
216
        if ($unused->exists()) {
217
            foreach ($unused as $file) {
218
                $oldName = $file->getFilename();
219
                if($this->verbose) {
220
                    DB::alteration_message('moving '.$file->getFilename().' to '.$unusedFolderName);
221
                }
222
                if($this->dryRun === false) {
223
                    $file->ParentID = $this->unusedImagesFolder->ID;
224
                    $this->writeFileOrFolder($file);
225
                    $newName = str_replace($folder->Name, $unusedFolderName, $oldName);
226
                    if($newName !== $file->getFilename()) {
227
                        DB::alteration_message('ERROR: file names do not match. Compare: '.$newName. ' with ' . $file->getFilename(), 'deleted');
228
                    }
229
                    $this->physicallyMovingImage($oldName, $newName);
230
                }
231
            }
232
        }
233
    }
234
235
    public function moveUsedFilesIntoFolder(string $folderName, array $listOfImageIds)
236
    {
237
        $folder = Folder::find_or_make($folderName);
238
        $this->writeFileOrFolder($folder);
239
        $listAsString = implode(',', $listOfImageIds);
240
        $where = ' ParentID <> ' . $folder->ID. ' AND File.ID IN('.$listAsString.')';
241
        $used = Image::get()->where($where);
242
        if ($used->exists()) {
243
            foreach ($used as $file) {
244
                $oldName = $file->getFilename();
245
246
                $oldFolderName = $file->Parent()->getFilename();
0 ignored issues
show
Unused Code introduced by
The assignment to $oldFolderName is dead and can be removed.
Loading history...
247
                $newFolderName = $folder->getFilename();
248
249
                if($this->verbose) {
250
                    DB::alteration_message('moving '.$file->getFilename().' to '.$newFolderName, 'created');
251
                }
252
                if($this->dryRun === false) {
253
                    $newName =  Controller::join_links($newFolderName, $file->Name);
254
                    $file->setFilename($newName);
255
                    $file->ParentID = $folder->ID;
256
                    $this->writeFileOrFolder($file);
257
                    if($this->verbose && $newName !== $file->getFilename()) {
258
                        DB::alteration_message('ERROR: file names do not match. Compare: '.$newName. ' with ' . $file->getFilename(), 'deleted');
259
                    } else {
260
                        $this->physicallyMovingImage($oldName, $newName);
261
                    }
262
                }
263
            }
264
        }
265
    }
266
267
268
    public function findRoqueFilesInFolder(string $folderName)
269
    {
270
        $unusedFolderName = $this->unusedImagesFolder->Name;
271
        $folder = Folder::find_or_make($folderName);
272
        $this->writeFileOrFolder($folder);
273
        $path = Controller::join_links(ASSETS_PATH, $folder->getFilename());
274
        $excludeArray = Image::get()->filter(['ParentID' => $folder->ID])->columnUnique('Name');
275
        if (is_dir($path)) {
276
            $files = array_diff(scandir($path), array('.', '..'));
277
            foreach ($files as $fileName) {
278
                if(! in_array($fileName, $excludeArray)) {
279
                    $associatedClassName = File::get_class_for_file_extension(pathinfo($fileName, PATHINFO_EXTENSION));
280
                    if($associatedClassName === Image::class) {
281
                        $filePath = Controller::join_links($path, $fileName);
282
                        if (is_file($filePath)) {
283
                            $oldName = $folderName . '/' . $fileName;
284
                            $newName = $unusedFolderName . '/' . $fileName;
285
                            if($this->verbose) {
286
                                DB::alteration_message('moving '.$oldName.' to '.$unusedFolderName);
287
                            }
288
                            if ($this->dryRun === false) {
289
                                $this->physicallyMovingImage($oldName, $newName);
290
                            }
291
                        } elseif($this->verbose) {
292
                            DB::alteration_message('skippping '.$fileName. ', because it is not a valid file.');
293
                        }
294
                    } elseif($this->verbose) {
295
                        DB::alteration_message('skippping '.$fileName. ', because it is not an image.');
296
                    }
297
                }
298
            }
299
        } elseif($this->verbose) {
300
            DB::alteration_message('skippping '.$path. ', because it is not a valid directory.');
301
        }
302
    }
303
304
    protected static $my_field_cache = [];
305
306
    protected function getFieldDetails(string $originClassName, string $originMethod) : array
307
    {
308
        $key = $originClassName.'_'.$originMethod;
309
        if(! isset(self::$my_field_cache[$key])) {
310
            $types = ['has_one', 'has_many', 'many_many'];
311
            $classNames = ClassInfo::ancestry($originClassName, true);
312
            foreach ($classNames as $className) {
313
                $obj = Injector::inst()->get($className);
0 ignored issues
show
Unused Code introduced by
The assignment to $obj is dead and can be removed.
Loading history...
314
                foreach ($types as $type) {
315
                    $rels = Config::inst()->get($className, $type, Config::UNINHERITED);
316
                    if (is_array($rels) && ! empty($rels)) {
317
                        foreach ($rels as $relName => $relType) {
318
                            if (Image::class === $relType && $relName === $originMethod) {
319
                                self::$my_field_cache[$key] = [
320
                                    'dataClassName' => $className,
321
                                    'dataType' => $type,
322
                                ];
323
                            }
324
                        }
325
                    }
326
                }
327
            }
328
        }
329
        return self::$my_field_cache[$key];
330
    }
331
332
    protected function physicallyMovingImage(string $oldName, string $newName)
333
    {
334
        if ($oldName !== $newName) {
335
            $oldNameFull = Controller::join_links(ASSETS_PATH, $oldName);
336
            $newNameFull = Controller::join_links(ASSETS_PATH, $newName);
337
            if (file_exists($oldNameFull)) {
338
                if(file_exists($newNameFull)) {
339
                    if ($this->verbose) {
340
                        DB::alteration_message('... ... Deleting '.$newName.' to make place for a new file.', 'deleted');
341
                    }
342
                    if($this->dryRun === false) {
343
                        unlink($newNameFull);
344
                    }
345
                }
346
                if ($this->verbose) {
347
                    DB::alteration_message('... Moving '.$oldNameFull.' to '.$newNameFull, 'created');
348
                }
349
                if($this->dryRun === false) {
350
                    rename($oldNameFull, $newNameFull);
351
                }
352
            } elseif($this->verbose) {
353
                DB::alteration_message('... Error could not find:  '.$oldNameFull, 'created');
354
            }
355
        } elseif($this->verbose) {
356
            DB::alteration_message('... ERROR: old and new file names are the same '.$oldName, 'deleted');
357
        }
358
    }
359
360
    protected function writeFileOrFolder($fileOrFolder)
361
    {
362
        $fileOrFolder->write();
363
        $fileOrFolder->publishSingle();
364
        $fileOrFolder->publishFile();
365
        $fileOrFolder->flushCache();
366
367
        return $fileOrFolder;
368
    }
369
370
    /**
371
     * code copied from: https://github.com/axllent/silverstripe-scaled-uploads/blob/master/src/ScaledUploads.php
372
     * @param  Image  $image
373
     * @param  int    $maxWidth
374
     * @param  int    $maxHeight
375
     * @return Image
376
     */
377
    public function scaleUploadedImage(Image $image, int $maxWidth, int $maxHeight) : ?Image
378
    {
379
        $backend = $image->getImageBackend();
380
381
        // temporary location for image manipulation
382
        $tmp_image = TEMP_FOLDER . '/resampled-' . mt_rand(100000, 999999) . '.' . $image->getExtension();
383
384
        $tmp_contents = $image->getString();
385
386
        // write to tmp file
387
        @file_put_contents($tmp_image, $tmp_contents);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for file_put_contents(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

387
        /** @scrutinizer ignore-unhandled */ @file_put_contents($tmp_image, $tmp_contents);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
388
389
        $backend->loadFrom($tmp_image);
390
391
        if ($backend->getImageResource()) {
392
            $modified = false;
393
394
            // clone original
395
            $transformed = $backend;
396
397
            // resize to max values
398
            if (
399
                $transformed &&
400
                (
401
                    ($maxWidth && $transformed->getWidth() > $maxWidth) ||
402
                    ($maxHeight && $transformed->getHeight() > $maxHeight)
403
                )
404
            ) {
405
                if ($maxWidth && $maxHeight) {
406
                    $transformed = $transformed->resizeRatio($maxWidth, $maxHeight);
407
                } elseif ($maxWidth) {
408
                    $transformed = $transformed->resizeByWidth($maxWidth);
409
                } else {
410
                    $transformed = $transformed->resizeByHeight($maxHeight);
411
                }
412
                $modified = true;
413
            }
414
415
            // write to tmp file and then overwrite original
416
            if ($transformed && $modified) {
417
                $transformed->writeTo($tmp_image);
418
                // if !legacy_filenames then delete original, else rogue copies are left on filesystem
419
                $image->setFromLocalFile($tmp_image, $image->FileName); // set new image
420
                $image->write();
421
                $image->publishSingle();
422
            }
423
        }
424
425
        @unlink($tmp_image); // delete tmp file
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

425
        /** @scrutinizer ignore-unhandled */ @unlink($tmp_image); // delete tmp file

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
426
        return $image;
427
    }
428
429
430
431
}
432