Completed
Push — master ( 4d797b...29f0f9 )
by Ingo
11s
created

AssetAdmin::apiUploadFile()   D

Complexity

Conditions 15
Paths 43

Size

Total Lines 81
Code Lines 50

Duplication

Lines 15
Ratio 18.52 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 15
loc 81
rs 4.9974
cc 15
eloc 50
nc 43
nop 1

How to fix   Long Method    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 SilverStripe\AssetAdmin\Controller;
4
5
use InvalidArgumentException;
6
use SilverStripe\Admin\AddToCampaignHandler;
7
use SilverStripe\Admin\CMSBatchActionHandler;
8
use SilverStripe\Admin\LeftAndMain;
9
use SilverStripe\AssetAdmin\BatchAction\DeleteAssets;
10
use SilverStripe\AssetAdmin\Forms\AssetFormFactory;
11
use SilverStripe\AssetAdmin\Forms\FileSearchFormFactory;
12
use SilverStripe\AssetAdmin\Forms\UploadField;
13
use SilverStripe\AssetAdmin\Forms\FileFormFactory;
14
use SilverStripe\AssetAdmin\Forms\FolderFormFactory;
15
use SilverStripe\AssetAdmin\Forms\FileHistoryFormFactory;
16
use SilverStripe\AssetAdmin\Forms\ImageFormFactory;
17
use SilverStripe\Assets\File;
18
use SilverStripe\Assets\Folder;
19
use SilverStripe\Assets\Image;
20
use SilverStripe\Assets\Storage\AssetNameGenerator;
21
use SilverStripe\Assets\Upload;
22
use SilverStripe\Control\Controller;
23
use SilverStripe\Control\HTTPRequest;
24
use SilverStripe\Control\HTTPResponse;
25
use SilverStripe\Core\Injector\Injector;
26
use SilverStripe\Forms\DateField;
27
use SilverStripe\Forms\Form;
28
use SilverStripe\Forms\FormFactory;
29
use SilverStripe\ORM\ArrayList;
30
use SilverStripe\ORM\DataList;
31
use SilverStripe\ORM\DataObject;
32
use SilverStripe\ORM\FieldType\DBHTMLText;
33
use SilverStripe\ORM\ValidationResult;
34
use SilverStripe\Security\Member;
35
use SilverStripe\Security\PermissionProvider;
36
use SilverStripe\Security\SecurityToken;
37
use SilverStripe\View\Requirements;
38
use SilverStripe\ORM\Versioning\Versioned;
39
use Exception;
40
41
/**
42
 * AssetAdmin is the 'file store' section of the CMS.
43
 * It provides an interface for manipulating the File and Folder objects in the system.
44
 */
45
class AssetAdmin extends LeftAndMain implements PermissionProvider
46
{
47
    private static $url_segment = 'assets';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_segment is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
48
49
    private static $url_rule = '/$Action/$ID';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_rule is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
50
51
    private static $menu_title = 'Files';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $menu_title is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
52
53
    private static $menu_icon_class = 'font-icon-image';
0 ignored issues
show
Unused Code introduced by
The property $menu_icon_class is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
54
55
    private static $tree_class = 'SilverStripe\\Assets\\Folder';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $tree_class is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
56
57
    private static $url_handlers = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_handlers is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
58
        // Legacy redirect for SS3-style detail view
59
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
60
        // Pass all URLs to the index, for React to unpack
61
        'show/$FolderID/edit/$FileID' => 'index',
62
        // API access points with structured data
63
        'POST api/createFile' => 'apiCreateFile',
64
        'POST api/uploadFile' => 'apiUploadFile',
65
        'GET api/history' => 'apiHistory'
66
    ];
67
68
    /**
69
     * Amount of results showing on a single page.
70
     *
71
     * @config
72
     * @var int
73
     */
74
    private static $page_length = 15;
0 ignored issues
show
Unused Code introduced by
The property $page_length is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
75
76
    /**
77
     * @config
78
     * @see Upload->allowedMaxFileSize
79
     * @var int
80
     */
81
    private static $allowed_max_file_size;
0 ignored issues
show
Unused Code introduced by
The property $allowed_max_file_size is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
82
83
    /**
84
     * @config
85
     *
86
     * @var int
87
     */
88
    private static $max_history_entries = 100;
0 ignored issues
show
Unused Code introduced by
The property $max_history_entries is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
89
90
    /**
91
     * @var array
92
     */
93
    private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
94
        'legacyRedirectForEditView',
95
        'apiCreateFile',
96
        'apiUploadFile',
97
        'apiHistory',
98
        'fileEditForm',
99
        'fileHistoryForm',
100
        'addToCampaignForm',
101
        'fileInsertForm',
102
        'schema',
103
        'fileSelectForm',
104
        'fileSearchForm',
105
    );
106
107
    private static $required_permission_codes = 'CMS_ACCESS_AssetAdmin';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $required_permission_codes is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
108
109
    private static $thumbnail_width = 400;
0 ignored issues
show
Unused Code introduced by
The property $thumbnail_width is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
110
111
    private static $thumbnail_height = 300;
0 ignored issues
show
Unused Code introduced by
The property $thumbnail_height is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
112
113
    /**
114
     * Set up the controller
115
     */
116
    public function init()
117
    {
118
        parent::init();
119
120
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
121
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
122
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
123
124
        CMSBatchActionHandler::register('delete', DeleteAssets::class, Folder::class);
125
    }
126
127
    public function getClientConfig()
128
    {
129
        $baseLink = $this->Link();
130
        return array_merge(parent::getClientConfig(), [
131
            'reactRouter' => true,
132
            'createFileEndpoint' => [
133
                'url' => Controller::join_links($baseLink, 'api/createFile'),
134
                'method' => 'post',
135
                'payloadFormat' => 'urlencoded',
136
            ],
137
            'uploadFileEndpoint' => [
138
                'url' => Controller::join_links($baseLink, 'api/uploadFile'),
139
                'method' => 'post',
140
                'payloadFormat' => 'urlencoded',
141
            ],
142
            'historyEndpoint' => [
143
                'url' => Controller::join_links($baseLink, 'api/history'),
144
                'method' => 'get',
145
                'responseFormat' => 'json',
146
            ],
147
            'limit' => $this->config()->page_length,
148
            'form' => [
149
                'fileEditForm' => [
150
                    'schemaUrl' => $this->Link('schema/fileEditForm')
151
                ],
152
                'fileInsertForm' => [
153
                    'schemaUrl' => $this->Link('schema/fileInsertForm')
154
                ],
155
                'fileSelectForm' => [
156
                    'schemaUrl' => $this->Link('schema/fileSelectForm')
157
                ],
158
                'addToCampaignForm' => [
159
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
160
                ],
161
                'fileHistoryForm' => [
162
                    'schemaUrl' => $this->Link('schema/fileHistoryForm')
163
                ],
164
                'fileSearchForm' => [
165
                    'schemaUrl' => $this->Link('schema/fileSearchForm')
166
                ],
167
            ],
168
        ]);
169
    }
170
171
    /**
172
     * Creates a single file based on a form-urlencoded upload.
173
     *
174
     * @param HTTPRequest $request
175
     * @return HTTPRequest|HTTPResponse
176
     */
177
    public function apiCreateFile(HTTPRequest $request)
178
    {
179
        $data = $request->postVars();
180
181
        // When creating new files, rename on conflict
182
        $upload = $this->getUpload();
183
        $upload->setReplaceFile(false);
184
185
        // CSRF check
186
        $token = SecurityToken::inst();
187 View Code Duplication
        if (empty($data[$token->getName()]) || !$token->check($data[$token->getName()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
188
            return new HTTPResponse(null, 400);
189
        }
190
191
        // Check parent record
192
        /** @var Folder $parentRecord */
193
        $parentRecord = null;
194
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
195
            $parentRecord = Folder::get()->byID($data['ParentID']);
196
        }
197
        $data['Parent'] = $parentRecord;
198
199
        $tmpFile = $request->postVar('Upload');
200 View Code Duplication
        if (!$upload->validate($tmpFile)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
201
            $result = ['message' => null];
202
            $errors = $upload->getErrors();
203
            if ($message = array_shift($errors)) {
204
                $result['message'] = [
205
                    'type' => 'error',
206
                    'value' => $message,
207
                ];
208
            }
209
            return (new HTTPResponse(json_encode($result), 400))
210
                ->addHeader('Content-Type', 'application/json');
211
        }
212
213
        // TODO Allow batch uploads
214
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
215
        /** @var File $file */
216
        $file = Injector::inst()->create($fileClass);
217
218
        // check canCreate permissions
219 View Code Duplication
        if (!$file->canCreate(null, $data)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
220
            $result = ['message' => [
221
                'type' => 'error',
222
                'value' => _t(
223
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
224
                    'You do not have permission to add files'
225
                )
226
            ]];
227
            return (new HTTPResponse(json_encode($result), 403))
228
                ->addHeader('Content-Type', 'application/json');
229
        }
230
231
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
232 View Code Duplication
        if (!$uploadResult) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
233
            $result = ['message' => [
234
                'type' => 'error',
235
                'value' => _t(
236
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
237
                    'Failed to load file'
238
                )
239
            ]];
240
            return (new HTTPResponse(json_encode($result), 400))
241
                ->addHeader('Content-Type', 'application/json');
242
        }
243
244
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
245
        $file->write();
246
247
        $result = [$this->getObjectFromData($file)];
248
249
        return (new HTTPResponse(json_encode($result)))
250
            ->addHeader('Content-Type', 'application/json');
251
    }
252
253
    /**
254
     * Upload a new asset for a pre-existing record. Returns the asset tuple.
255
     *
256
     * Note that conflict resolution is as follows:
257
     *  - If uploading a file with the same extension, we simply keep the same filename,
258
     *    and overwrite any existing files (same name + sha = don't duplicate).
259
     *  - If uploading a new file with a different extension, then the filename will
260
     *    be replaced, and will be checked for uniqueness against other File dataobjects.
261
     *
262
     * @param HTTPRequest $request Request containing vars 'ID' of parent record ID,
263
     * and 'Name' as form filename value
264
     * @return HTTPRequest|HTTPResponse
265
     */
266
    public function apiUploadFile(HTTPRequest $request)
267
    {
268
        $data = $request->postVars();
269
270
        // When updating files, replace on conflict
271
        $upload = $this->getUpload();
272
        $upload->setReplaceFile(true);
273
274
        // CSRF check
275
        $token = SecurityToken::inst();
276 View Code Duplication
        if (empty($data[$token->getName()]) || !$token->check($data[$token->getName()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
277
            return new HTTPResponse(null, 400);
278
        }
279
        $tmpFile = $data['Upload'];
280
        if (empty($data['ID']) || empty($tmpFile['name']) || !array_key_exists('Name', $data)) {
281
            return new HTTPResponse('Invalid request', 400);
282
        }
283
284
        // Check parent record
285
        /** @var File $file */
286
        $file = File::get()->byID($data['ID']);
287
        if (!$file) {
288
            return new HTTPResponse('File not found', 404);
289
        }
290
        $folder = $file->ParentID ? $file->Parent()->getFilename() : '/';
291
292
        // If extension is the same, attempt to re-use existing name
293
        if (File::get_file_extension($tmpFile['name']) === File::get_file_extension($data['Name'])) {
294
            $tmpFile['name'] = $data['Name'];
295
        } else {
296
            // If we are allowing this upload to rename the underlying record,
297
            // do a uniqueness check.
298
            $renamer = $this->getNameGenerator($tmpFile['name']);
299
            foreach ($renamer as $name) {
300
                $filename = File::join_paths($folder, $name);
301
                if (!File::find($filename)) {
302
                    $tmpFile['name'] = $name;
303
                    break;
304
                }
305
            }
306
        }
307
308 View Code Duplication
        if (!$upload->validate($tmpFile)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
309
            $result = ['message' => null];
310
            $errors = $upload->getErrors();
311
            if ($message = array_shift($errors)) {
312
                $result['message'] = [
313
                    'type' => 'error',
314
                    'value' => $message,
315
                ];
316
            }
317
            return (new HTTPResponse(json_encode($result), 400))
318
                ->addHeader('Content-Type', 'application/json');
319
        }
320
321
        try {
322
            $tuple = $upload->load($tmpFile, $folder);
323
        } catch (Exception $e) {
324
            $result = [
325
                'message' => [
326
                    'type' => 'error',
327
                    'value' => $e->getMessage(),
328
                ]
329
            ];
330
            return (new HTTPResponse(json_encode($result), 400))
331
                ->addHeader('Content-Type', 'application/json');
332
        }
333
334
        if ($upload->isError()) {
335
            $result['message'] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
336
                'type' => 'error',
337
                'value' => implode(' ' . PHP_EOL, $upload->getErrors()),
338
            ];
339
            return (new HTTPResponse(json_encode($result), 400))
340
                ->addHeader('Content-Type', 'application/json');
341
        }
342
343
        $tuple['Name'] = basename($tuple['Filename']);
344
        return (new HTTPResponse(json_encode($tuple)))
345
            ->addHeader('Content-Type', 'application/json');
346
    }
347
348
    /**
349
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
350
     *
351
     * @param HTTPRequest $request
352
     * @return HTTPResponse
353
     */
354
    public function apiHistory(HTTPRequest $request)
355
    {
356
        // CSRF check not required as the GET request has no side effects.
357
        $fileId = $request->getVar('fileId');
358
359
        if (!$fileId || !is_numeric($fileId)) {
360
            return new HTTPResponse(null, 400);
361
        }
362
363
        $class = File::class;
364
        $file = DataObject::get($class)->byID($fileId);
365
366
        if (!$file) {
367
            return new HTTPResponse(null, 404);
368
        }
369
370
        if (!$file->canView()) {
371
            return new HTTPResponse(null, 403);
372
        }
373
374
        $versions = Versioned::get_all_versions($class, $fileId)
375
            ->limit($this->config()->max_history_entries)
376
            ->sort('Version', 'DESC');
377
378
        $output = array();
379
        $next = array();
380
        $prev = null;
381
382
        // swap the order so we can get the version number to compare against.
383
        // i.e version 3 needs to know version 2 is the previous version
384
        $copy = $versions->map('Version', 'Version')->toArray();
385
        foreach (array_reverse($copy) as $k => $v) {
386
            if ($prev) {
387
                $next[$v] = $prev;
388
            }
389
390
            $prev = $v;
391
        }
392
393
        $_cachedMembers = array();
394
395
        /** @var File $version */
396
        foreach ($versions as $version) {
397
            $author = null;
398
399
            if ($version->AuthorID) {
400
                if (!isset($_cachedMembers[$version->AuthorID])) {
401
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
402
                        ->byID($version->AuthorID);
403
                }
404
405
                $author = $_cachedMembers[$version->AuthorID];
406
            }
407
408
            if ($version->canView()) {
409
                if (isset($next[$version->Version])) {
410
                    $summary = $version->humanizedChanges(
411
                        $version->Version,
412
                        $next[$version->Version]
413
                    );
414
415
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
416
                    // generic message
417
                    if (!$summary) {
418
                        $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.SAVEDFILE', "Saved file");
419
                    }
420
                } else {
421
                    $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UPLOADEDFILE', "Uploaded file");
422
                }
423
424
                $output[] = array(
425
                    'versionid' => $version->Version,
426
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
427
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
428
                    'status' => ($version->WasPublished) ? _t('File.PUBLISHED', 'Published') : '',
429
                    'author' => ($author)
430
                        ? $author->Name
431
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNKNOWN', "Unknown"),
432
                    'summary' => ($summary)
433
                        ? $summary
434
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NOSUMMARY', "No summary available")
435
                );
436
            }
437
        }
438
439
        return
440
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
441
    }
442
443
    /**
444
     * Redirects 3.x style detail links to new 4.x style routing.
445
     *
446
     * @param HTTPRequest $request
447
     */
448
    public function legacyRedirectForEditView($request)
449
    {
450
        $fileID = $request->param('FileID');
451
        /** @var File $file */
452
        $file = File::get()->byID($fileID);
453
        $link = $this->getFileEditLink($file) ?: $this->Link();
454
        $this->redirect($link);
455
    }
456
457
    /**
458
     * Given a file return the CMS link to edit it
459
     *
460
     * @param File $file
461
     * @return string
462
     */
463
    public function getFileEditLink($file)
464
    {
465
        if (!$file || !$file->isInDB()) {
466
            return null;
467
        }
468
469
        return Controller::join_links(
470
            $this->Link('show'),
471
            $file->ParentID,
472
            'edit',
473
            $file->ID
474
        );
475
    }
476
477
    /**
478
     * Get an asset renamer for the given filename.
479
     *
480
     * @param  string             $filename Path name
481
     * @return AssetNameGenerator
482
     */
483
    protected function getNameGenerator($filename)
484
    {
485
        return Injector::inst()
486
            ->createWithArgs('AssetNameGenerator', array($filename));
487
    }
488
489
    /**
490
     * @todo Implement on client
491
     *
492
     * @param bool $unlinked
493
     * @return ArrayList
494
     */
495
    public function breadcrumbs($unlinked = false)
496
    {
497
        return null;
498
    }
499
500
501
    /**
502
     * Don't include class namespace in auto-generated CSS class
503
     */
504
    public function baseCSSClasses()
505
    {
506
        return 'AssetAdmin LeftAndMain';
507
    }
508
509
    public function providePermissions()
510
    {
511
        return array(
512
            "CMS_ACCESS_AssetAdmin" => array(
513
                'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array(
0 ignored issues
show
Documentation introduced by
array('title' => static::menu_title()) is of type array<string,string,{"title":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
514
                    'title' => static::menu_title()
515
                )),
516
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
517
            )
518
        );
519
    }
520
521
    /**
522
     * Build a form scaffolder for this model
523
     *
524
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
525
     *
526
     * @param File $file
527
     * @return FormFactory
528
     */
529
    public function getFormFactory(File $file)
530
    {
531
        // Get service name based on file class
532
        $name = null;
0 ignored issues
show
Unused Code introduced by
$name is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
533
        if ($file instanceof Folder) {
534
            $name = FolderFormFactory::class;
535
        } elseif ($file instanceof Image) {
536
            $name = ImageFormFactory::class;
537
        } else {
538
            $name = FileFormFactory::class;
539
        }
540
        return Injector::inst()->get($name);
541
    }
542
543
    /**
544
     * The form is used to generate a form schema,
545
     * as well as an intermediary object to process data through API endpoints.
546
     * Since it's used directly on API endpoints, it does not have any form actions.
547
     * It handles both {@link File} and {@link Folder} records.
548
     *
549
     * @param int $id
550
     * @return Form
551
     */
552
    public function getFileEditForm($id)
553
    {
554
        return $this->getAbstractFileForm($id, 'fileEditForm');
555
    }
556
557
    /**
558
     * Get file edit form
559
     *
560
     * @return Form
561
     */
562 View Code Duplication
    public function fileEditForm()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
563
    {
564
        // Get ID either from posted back value, or url parameter
565
        $request = $this->getRequest();
566
        $id = $request->param('ID') ?: $request->postVar('ID');
567
        return $this->getFileEditForm($id);
568
    }
569
570
    /**
571
     * The form is used to generate a form schema,
572
     * as well as an intermediary object to process data through API endpoints.
573
     * Since it's used directly on API endpoints, it does not have any form actions.
574
     * It handles both {@link File} and {@link Folder} records.
575
     *
576
     * @param int $id
577
     * @return Form
578
     */
579
    public function getFileInsertForm($id)
580
    {
581
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT ]);
582
    }
583
584
    /**
585
     * Get file insert form
586
     *
587
     * @return Form
588
     */
589 View Code Duplication
    public function fileInsertForm()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
590
    {
591
        // Get ID either from posted back value, or url parameter
592
        $request = $this->getRequest();
593
        $id = $request->param('ID') ?: $request->postVar('ID');
594
        return $this->getFileInsertForm($id);
595
    }
596
597
    /**
598
     * Abstract method for generating a form for a file
599
     *
600
     * @param int $id Record ID
601
     * @param string $name Form name
602
     * @param array $context Form context
603
     * @return Form
604
     */
605
    protected function getAbstractFileForm($id, $name, $context = [])
606
    {
607
        /** @var File $file */
608
        $file = File::get()->byID($id);
609
610 View Code Duplication
        if (!$file->canView()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
611
            $this->httpError(403, _t(
612
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
613
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
614
                '',
615
                ['ObjectTitle' => $file->i18n_singular_name()]
616
            ));
617
            return null;
618
        }
619
620
        // Pass to form factory
621
        $augmentedContext = array_merge($context, ['Record' => $file]);
622
        $scaffolder = $this->getFormFactory($file);
623
        $form = $scaffolder->getForm($this, $name, $augmentedContext);
624
625
        // Configure form to respond to validation errors with form schema
626
        // if requested via react.
627
        $form->setValidationResponseCallback(function (ValidationResult $error) use ($form, $id, $name) {
628
            $schemaId = Controller::join_links($this->Link('schema'), $name, $id);
629
            return $this->getSchemaResponse($schemaId, $form, $error);
630
        });
631
632
        return $form;
633
    }
634
635
    /**
636
     * Get form for selecting a file
637
     *
638
     * @return Form
639
     */
640
    public function fileSelectForm()
641
    {
642
        // Get ID either from posted back value, or url parameter
643
        $request = $this->getRequest();
644
        $id = $request->param('ID') ?: $request->postVar('ID');
645
        return $this->getFileSelectForm($id);
646
    }
647
648
    /**
649
     * Get form for selecting a file
650
     *
651
     * @param int $id ID of the record being selected
652
     * @return Form
653
     */
654
    public function getFileSelectForm($id)
655
    {
656
        return $this->getAbstractFileForm($id, 'fileSelectForm', [ 'Type' => AssetFormFactory::TYPE_SELECT ]);
657
    }
658
659
    /**
660
     * @param array $context
661
     * @return Form
662
     * @throws InvalidArgumentException
663
     */
664
    public function getFileHistoryForm($context)
665
    {
666
        // Check context
667
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
668
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
669
        }
670
        $id = $context['RecordID'];
671
        $versionId = $context['RecordVersion'];
672
        if (!$id || !$versionId) {
673
            return $this->httpError(404);
674
        }
675
676
        /** @var File $file */
677
        $file = Versioned::get_version(File::class, $id, $versionId);
678
        if (!$file) {
679
            return $this->httpError(404);
680
        }
681
682 View Code Duplication
        if (!$file->canView()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
683
            $this->httpError(403, _t(
684
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
685
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
686
                '',
687
                ['ObjectTitle' => $file->i18n_singular_name()]
688
            ));
689
            return null;
690
        }
691
692
        $effectiveContext = array_merge($context, ['Record' => $file]);
693
        /** @var FormFactory $scaffolder */
694
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
695
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
696
697
        // Configure form to respond to validation errors with form schema
698
        // if requested via react.
699 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id, $versionId) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
700
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
701
            return $this->getSchemaResponse($schemaId, $form, $errors);
702
        });
703
704
        return $form;
705
    }
706
707
    /**
708
     * Gets a JSON schema representing the current edit form.
709
     *
710
     * WARNING: Experimental API.
711
     *
712
     * @param HTTPRequest $request
713
     * @return HTTPResponse
714
     */
715
    public function schema($request)
716
    {
717
        $formName = $request->param('FormName');
718
        if ($formName !== 'fileHistoryForm') {
719
            return parent::schema($request);
720
        }
721
722
        // Get schema for history form
723
        // @todo Eventually all form scaffolding will be based on context rather than record ID
724
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
725
        $itemID = $request->param('ItemID');
726
        $version = $request->param('OtherItemID');
727
        $form = $this->getFileHistoryForm([
728
            'RecordID' => $itemID,
729
            'RecordVersion' => $version,
730
        ]);
731
732
        // Respond with this schema
733
        $response = $this->getResponse();
734
        $response->addHeader('Content-Type', 'application/json');
735
        $schemaID = $this->getRequest()->getURL();
736
        return $this->getSchemaResponse($schemaID, $form);
737
    }
738
739
    /**
740
     * Get file history form
741
     *
742
     * @return Form
743
     */
744
    public function fileHistoryForm()
745
    {
746
        $request = $this->getRequest();
747
        $id = $request->param('ID') ?: $request->postVar('ID');
748
        $version = $request->param('OtherID') ?: $request->postVar('Version');
749
        $form = $this->getFileHistoryForm([
750
            'RecordID' => $id,
751
            'RecordVersion' => $version,
752
        ]);
753
        return $form;
754
    }
755
756
    /**
757
     * @param array $data
758
     * @param Form $form
759
     * @return HTTPResponse
760
     */
761
    public function save($data, $form)
762
    {
763
        return $this->saveOrPublish($data, $form, false);
764
    }
765
766
    /**
767
     * @param array $data
768
     * @param Form $form
769
     * @return HTTPResponse
770
     */
771
    public function publish($data, $form)
772
    {
773
        return $this->saveOrPublish($data, $form, true);
774
    }
775
776
    /**
777
     * Update thisrecord
778
     *
779
     * @param array $data
780
     * @param Form $form
781
     * @param bool $doPublish
782
     * @return HTTPResponse
783
     */
784
    protected function saveOrPublish($data, $form, $doPublish = false)
785
    {
786 View Code Duplication
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
787
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
788
                ->addHeader('Content-Type', 'application/json');
789
        }
790
791
        $id = (int) $data['ID'];
792
        /** @var File $record */
793
        $record = DataObject::get_by_id(File::class, $id);
794
795
        if (!$record) {
796
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
797
                ->addHeader('Content-Type', 'application/json');
798
        }
799
800
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
801
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
802
                ->addHeader('Content-Type', 'application/json');
803
        }
804
805
        // check File extension
806
        if (!empty($data['FileFilename'])) {
807
            $extension = File::get_file_extension($data['FileFilename']);
808
            $newClass = File::get_class_for_file_extension($extension);
809
810
            // if the class has changed, cast it to the proper class
811
            if ($record->getClassName() !== $newClass) {
812
                $record = $record->newClassInstance($newClass);
813
814
                // update the allowed category for the new file extension
815
                $category = File::get_app_category($extension);
816
                $record->File->setAllowedCategories($category);
817
            }
818
        }
819
820
        $form->saveInto($record);
821
        $record->write();
822
823
        // Publish this record and owned objects
824
        if ($doPublish) {
825
            $record->publishRecursive();
826
        }
827
828
        // Note: Force return of schema / state in success result
829
        return $this->getRecordUpdatedResponse($record, $form);
0 ignored issues
show
Compatibility introduced by
$record of type object<SilverStripe\ORM\DataObject> is not a sub-type of object<SilverStripe\Assets\File>. It seems like you assume a child class of the class SilverStripe\ORM\DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
830
    }
831
832
    public function unpublish($data, $form)
833
    {
834 View Code Duplication
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
835
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
836
                ->addHeader('Content-Type', 'application/json');
837
        }
838
839
        $id = (int) $data['ID'];
840
        /** @var File $record */
841
        $record = DataObject::get_by_id(File::class, $id);
842
843
        if (!$record) {
844
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
845
                ->addHeader('Content-Type', 'application/json');
846
        }
847
848
        if (!$record->canUnpublish()) {
849
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
850
                ->addHeader('Content-Type', 'application/json');
851
        }
852
853
        $record->doUnpublish();
854
        return $this->getRecordUpdatedResponse($record, $form);
855
    }
856
857
    /**
858
     * @param File $file
859
     *
860
     * @return array
861
     */
862
    public function getObjectFromData(File $file)
863
    {
864
        $object = array(
865
            'id' => $file->ID,
866
            'created' => $file->Created,
867
            'lastUpdated' => $file->LastEdited,
868
            'owner' => null,
869
            'parent' => null,
870
            'title' => $file->Title,
871
            'exists' => $file->exists(), // Broken file check
872
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
873
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
874
            'name' => $file->Name,
875
            'filename' => $file->Filename,
876
            'extension' => $file->Extension,
0 ignored issues
show
Bug introduced by
The property Extension does not seem to exist. Did you mean extension_instances?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
877
            'size' => $file->AbsoluteSize,
878
            'url' => $file->AbsoluteURL,
879
            'published' => $file->isPublished(),
880
            'modified' => $file->isModifiedOnDraft(),
881
            'draft' => $file->isOnDraftOnly(),
882
            'canEdit' => $file->canEdit(),
883
            'canDelete' => $file->canArchive(),
884
        );
885
886
        /** @var Member $owner */
887
        $owner = $file->Owner();
888
889
        if ($owner) {
890
            $object['owner'] = array(
891
                'id' => $owner->ID,
892
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
893
            );
894
        }
895
896
        /** @var Folder $parent */
897
        $parent = $file->Parent();
898
899
        if ($parent) {
900
            $object['parent'] = array(
901
                'id' => $parent->ID,
902
                'title' => $parent->Title,
903
                'filename' => $parent->Filename,
904
            );
905
        }
906
907
        /** @var File $file */
908
        if ($file->getIsImage()) {
909
            // Small thumbnail
910
            $smallWidth = UploadField::config()->get('thumbnail_width');
911
            $smallHeight = UploadField::config()->get('thumbnail_height');
912
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
913
            if ($smallThumbnail && $smallThumbnail->exists()) {
914
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
915
            }
916
917
            // Large thumbnail
918
            $width = $this->config()->get('thumbnail_width');
919
            $height = $this->config()->get('thumbnail_height');
920
            $thumbnail = $file->FitMax($width, $height);
921
            if ($thumbnail && $thumbnail->exists()) {
922
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
923
            }
924
            $object['width'] = $file->Width;
925
            $object['height'] = $file->Height;
0 ignored issues
show
Bug introduced by
The property Height does not seem to exist. Did you mean strip_thumbnail_height?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
926
        } else {
927
            $object['thumbnail'] = $file->PreviewLink();
928
        }
929
930
        return $object;
931
    }
932
933
    /**
934
     * Action handler for adding pages to a campaign
935
     *
936
     * @param array $data
937
     * @param Form $form
938
     * @return DBHTMLText|HTTPResponse
939
     */
940
    public function addtocampaign($data, $form)
941
    {
942
        $id = $data['ID'];
943
        $record = File::get()->byID($id);
944
945
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
946
        $results = $handler->addToCampaign($record, $data['Campaign']);
0 ignored issues
show
Bug introduced by
It seems like $record defined by \SilverStripe\Assets\File::get()->byID($id) on line 943 can be null; however, SilverStripe\Admin\AddTo...andler::addToCampaign() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
947
        if (!isset($results)) {
948
            return null;
949
        }
950
951
        // Send extra "message" data with schema response
952
        $extraData = ['message' => $results];
953
        $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
954
        return $this->getSchemaResponse($schemaId, $form, null, $extraData);
955
    }
956
957
    /**
958
     * Url handler for add to campaign form
959
     *
960
     * @param HTTPRequest $request
961
     * @return Form
962
     */
963
    public function addToCampaignForm($request)
964
    {
965
        // Get ID either from posted back value, or url parameter
966
        $id = $request->param('ID') ?: $request->postVar('ID');
967
        return $this->getAddToCampaignForm($id);
968
    }
969
970
    /**
971
     * @param int $id
972
     * @return Form
973
     */
974
    public function getAddToCampaignForm($id)
975
    {
976
        // Get record-specific fields
977
        $record = File::get()->byID($id);
978
979
        if (!$record) {
980
            $this->httpError(404, _t(
981
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
982
                'That {Type} couldn\'t be found',
983
                '',
984
                ['Type' => File::singleton()->i18n_singular_name()]
985
            ));
986
            return null;
987
        }
988 View Code Duplication
        if (!$record->canView()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
989
            $this->httpError(403, _t(
990
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
991
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
992
                '',
993
                ['ObjectTitle' => $record->i18n_singular_name()]
994
            ));
995
            return null;
996
        }
997
998
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
999
        $form = $handler->Form($record);
1000
1001 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1002
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1003
            return $this->getSchemaResponse($schemaId, $form, $errors);
1004
        });
1005
1006
        return $form;
1007
    }
1008
1009
    /**
1010
     * @return Upload
1011
     */
1012
    protected function getUpload()
1013
    {
1014
        $upload = Upload::create();
1015
        $upload->getValidator()->setAllowedExtensions(
1016
            // filter out '' since this would be a regex problem on JS end
1017
            array_filter(File::config()->get('allowed_extensions'))
1018
        );
1019
1020
        return $upload;
1021
    }
1022
1023
    /**
1024
     * Get response for successfully updated record
1025
     *
1026
     * @param File $record
1027
     * @param Form $form
1028
     * @return HTTPResponse
1029
     */
1030
    protected function getRecordUpdatedResponse($record, $form)
1031
    {
1032
        // Return the record data in the same response as the schema to save a postback
1033
        $schemaData = ['record' => $this->getObjectFromData($record)];
1034
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1035
        return $this->getSchemaResponse($schemaId, $form, null, $schemaData);
1036
    }
1037
1038
    /**
1039
     * Scaffold a search form.
1040
     * Note: This form does not submit to itself, but rather uses the apiReadFolder endpoint
1041
     * (to be replaced with graphql)
1042
     *
1043
     * @return Form
1044
     */
1045
    public function fileSearchForm()
1046
    {
1047
        $scaffolder = FileSearchFormFactory::singleton();
1048
        return $scaffolder->getForm($this, 'fileSearchForm', []);
1049
    }
1050
1051
    /**
1052
     * Allow search form to be accessible to schema
1053
     *
1054
     * @return Form
1055
     */
1056
    public function getFileSearchform()
1057
    {
1058
        return $this->fileSearchForm();
1059
    }
1060
}
1061