Passed
Push — master ( cbf27f...2edf90 )
by Nicolaas
17:11 queued 08:06
created

SortOutFolders::writeFolder()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
namespace Sunnysideup\PerfectCmsImages\Api;
4
5
use SilverStripe\Assets\File;
6
use SilverStripe\Assets\Folder;
7
use SilverStripe\Assets\Image;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Core\ClassInfo;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Config\Configurable;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\ORM\DB;
14
use SilverStripe\Versioned\Versioned;
15
16
/**
17
 * the assumption we make here is that a particular group of images (e.g. Page.Image) live
18
 * live in a particular folder.
19
 */
20
class SortOutFolders
21
{
22
    use Configurable;
23
24
    /**
25
     * the folder where we move images that are not in use.
26
     *
27
     * @var Folder
28
     */
29
    protected $unusedImagesFolder;
30
31
    /**
32
     * if set to true then dont do it for real!
33
     *
34
     * @var bool
35
     */
36
    protected $dryRun = false;
37
38
    /**
39
     * @var bool
40
     */
41
    protected $verbose = true;
42
43
    protected static $my_field_cache = [];
44
45
    public function setVerbose(?bool $b = true)
46
    {
47
        $this->verbose = $b;
48
49
        return $this;
50
    }
51
52
    public function setDryRun(?bool $b = true)
53
    {
54
        $this->dryRun = $b;
55
56
        return $this;
57
    }
58
59
    public function runStandard()
60
    {
61
        $this->runAdvanced(
62
            Config::inst()->get(PerfectCMSImages::class, 'unused_images_folder_name'),
63
            PerfectCMSImages::get_all_values_for_images()
64
        );
65
    }
66
67
    /**
68
     * @param array $data
69
     *                    Create test jobs for the purposes of testing.
70
     *                    The array must contains arrays with
71
     *                    - folder
72
     *                    - used_by
73
     *                    used_by is an array that has ClassNames and Relations
74
     *                    (has_one / has_many / many_many relations)
75
     *                    e.g. Page.Image, MyDataObject.MyImages
76
     */
77
    public function runAdvanced(string $unusedFolderName, array $data)
78
    {
79
        $this->unusedImagesFolder = Folder::find_or_make($unusedFolderName);
80
81
        $folderArray = $this->getFolderArray($data);
82
        if ($this->verbose) {
83
            DB::alteration_message('==== List of folders ====');
84
            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

84
            echo '<pre>' . /** @scrutinizer ignore-type */ print_r($folderArray, 1) . '</pre>';
Loading history...
85
        }
86
87
        DB::alteration_message('==========================================');
88
89
        $listOfImageIds = $this->getListOfImages($folderArray);
90
91
        // remove
92
        foreach ($listOfImageIds as $folderName => $listOfIds) {
93
            if ($this->verbose) {
94
                DB::alteration_message('<br /><br /><br />==== Checking for images to remove from <u>' . $folderName . '</u>; there are ' . count($listOfIds) . ' images to keep');
95
            }
96
            $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...
97
        }
98
99
        DB::alteration_message('==========================================');
100
        // move to right folder
101
        foreach ($listOfImageIds as $folderName => $listOfIds) {
102
            if ($this->verbose) {
103
                DB::alteration_message('<br /><br /><br />==== Checking for images to move to <u>' . $folderName . '</u>');
104
            }
105
            $this->moveUsedFilesIntoFolder($folderName, $listOfIds);
106
        }
107
108
        DB::alteration_message('==========================================');
109
110
        // check for rogue files
111
        foreach (array_keys($listOfImageIds) as $folderName) {
112
            if ($this->verbose) {
113
                DB::alteration_message('<br /><br /><br />==== Checking for rogue FILES in <u>' . $folderName . '</u>');
114
            }
115
            $this->findRoqueFilesInFolder($folderName);
116
        }
117
    }
118
119
    public function getFolderArray(array $data): array
120
    {
121
        // check folders
122
        $folderArray = [];
123
        foreach ($data as $dataInner) {
124
            $folderName = $dataInner['folder'] ?? '';
125
            if ($folderName) {
126
                $folderArray[$folderName] = [];
127
                $folderArray[$folderName]['classesAndMethods'] = [];
128
                // $folderArray[$folderName]['resize'] = isset($dataInner['force_resize']) && $dataInner['force_resize'] === true  ? true : false;
129
                $classes = $dataInner['used_by'] ?? [];
130
                if (! empty($classes)) {
131
                    if (is_array($classes)) {
132
                        foreach ($classes as $classAndMethod) {
133
                            $folderArray[$folderName]['classesAndMethods'][$classAndMethod] = $classAndMethod;
134
                        }
135
                    } else {
136
                        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

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