Passed
Push — master ( 91b4c0...0c19e0 )
by Nicolaas
04:18
created

SortOutFolders::writeFileOrFolder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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
        // move to right folder
106
        foreach($listOfImageIds as $folderName => $listOfIds) {
107
            if($this->verbose) {
108
                DB::alteration_message('<br /><br /><br />==== Checking for images to move to <u>'.$folderName.'</u>');
109
            }
110
            $this->moveUsedFilesIntoFolder($folderName, $listOfIds);
111
        }
112
113
        // check for rogue files
114
        foreach(array_keys($listOfImageIds) as $folderName) {
115
            if($this->verbose) {
116
                DB::alteration_message('<br /><br /><br />==== Checking for rogue files in <u>'.$folderName.'</u>');
117
            }
118
            $this->findRoqueFilesInFolder($folderName);
119
        }
120
    }
121
122
123
    public function getFolderArray(array $data) :array
124
    {
125
        // check folders
126
        $folderArray = [];
127
        foreach($data as $dataInner) {
128
            $folder = $dataInner['folder'] ?? '';
129
            if($folder) {
130
                $folderArray[$folder] = [];
131
                $classes = $dataInner['used_by'] ?? [];
132
                if(! empty($classes)) {
133
                    if(is_array($classes)) {
134
                        foreach($classes as $classAndMethodList) {
135
                            $folderArray[$folder][$classAndMethodList] = $classAndMethodList;
136
                        }
137
                    } else {
138
                        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

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