Completed
Pull Request — master (#327)
by Damian
01:53
created

AssetAdmin::apiReadFolder()   F

Complexity

Conditions 28
Paths 768

Size

Total Lines 102
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
c 6
b 0
f 0
dl 0
loc 102
rs 2.1804
cc 28
eloc 64
nc 768
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\Forms\UploadField;
10
use SilverStripe\AssetAdmin\Forms\FileFormFactory;
11
use SilverStripe\AssetAdmin\Forms\FolderFormFactory;
12
use SilverStripe\AssetAdmin\Forms\FileHistoryFormFactory;
13
use SilverStripe\AssetAdmin\Forms\ImageFormFactory;
14
use SilverStripe\Assets\File;
15
use SilverStripe\Assets\Folder;
16
use SilverStripe\Assets\Image;
17
use SilverStripe\Assets\Storage\AssetNameGenerator;
18
use SilverStripe\Assets\Upload;
19
use SilverStripe\Control\Controller;
20
use SilverStripe\Control\HTTPRequest;
21
use SilverStripe\Control\HTTPResponse;
22
use SilverStripe\Core\Injector\Injector;
23
use SilverStripe\Forms\CheckboxField;
24
use SilverStripe\Forms\DateField;
25
use SilverStripe\Forms\DropdownField;
26
use SilverStripe\Forms\FieldGroup;
27
use SilverStripe\Forms\Form;
28
use SilverStripe\Forms\FormFactory;
29
use SilverStripe\Forms\HeaderField;
30
use SilverStripe\ORM\ArrayList;
31
use SilverStripe\ORM\DataList;
32
use SilverStripe\ORM\DataObject;
33
use SilverStripe\ORM\FieldType\DBHTMLText;
34
use SilverStripe\ORM\Search\SearchContext;
35
use SilverStripe\ORM\ValidationResult;
36
use SilverStripe\Security\Member;
37
use SilverStripe\Security\PermissionProvider;
38
use SilverStripe\Security\SecurityToken;
39
use SilverStripe\View\Requirements;
40
use SilverStripe\ORM\Versioning\Versioned;
41
42
/**
43
 * AssetAdmin is the 'file store' section of the CMS.
44
 * It provides an interface for manipulating the File and Folder objects in the system.
45
 */
46
class AssetAdmin extends LeftAndMain implements PermissionProvider
47
{
48
    private static $url_segment = 'assets';
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
49
50
    private static $url_rule = '/$Action/$ID';
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
51
52
    private static $menu_title = 'Files';
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
53
54
    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...
55
56
    private static $url_handlers = [
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
57
        // Legacy redirect for SS3-style detail view
58
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
59
        // Pass all URLs to the index, for React to unpack
60
        'show/$FolderID/edit/$FileID' => 'index',
61
        // API access points with structured data
62
        'POST api/createFolder' => 'apiCreateFolder',
63
        'POST api/createFile' => 'apiCreateFile',
64
        'GET api/readFolder' => 'apiReadFolder',
65
        'PUT api/updateFolder' => 'apiUpdateFolder',
66
        'DELETE api/delete' => 'apiDelete',
67
        'GET api/search' => 'apiSearch',
68
        'GET api/history' => 'apiHistory'
69
    ];
70
71
    /**
72
     * Amount of results showing on a single page.
73
     *
74
     * @config
75
     * @var int
76
     */
77
    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...
78
79
    /**
80
     * @config
81
     * @see Upload->allowedMaxFileSize
82
     * @var int
83
     */
84
    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...
85
86
    /**
87
     * @config
88
     *
89
     * @var int
90
     */
91
    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...
92
93
    /**
94
     * @var array
95
     */
96
    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...
97
        'legacyRedirectForEditView',
98
        'apiCreateFolder',
99
        'apiCreateFile',
100
        'apiReadFolder',
101
        'apiUpdateFolder',
102
        'apiHistory',
103
        'apiDelete',
104
        'apiSearch',
105
        'fileEditForm',
106
        'fileHistoryForm',
107
        'addToCampaignForm',
108
        'fileInsertForm',
109
        'schema',
110
    );
111
112
    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...
113
114
    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...
115
116
    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...
117
118
    /**
119
     * Set up the controller
120
     */
121
    public function init()
122
    {
123
        parent::init();
124
125
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
126
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
127
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
128
129
        CMSBatchActionHandler::register(
130
            'delete',
131
            'SilverStripe\AssetAdmin\BatchAction\DeleteAssets',
132
            'SilverStripe\\Assets\\Folder'
133
        );
134
    }
135
136
    public function getClientConfig()
137
    {
138
        $baseLink = $this->Link();
139
        return array_merge(parent::getClientConfig(), [
140
            'reactRouter' => true,
141
            'createFileEndpoint' => [
142
                'url' => Controller::join_links($baseLink, 'api/createFile'),
143
                'method' => 'post',
144
                'payloadFormat' => 'urlencoded',
145
            ],
146
            'createFolderEndpoint' => [
147
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
148
                'method' => 'post',
149
                'payloadFormat' => 'urlencoded',
150
            ],
151
            'readFolderEndpoint' => [
152
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
153
                'method' => 'get',
154
                'responseFormat' => 'json',
155
            ],
156
            'searchEndpoint' => [
157
                'url' => Controller::join_links($baseLink, 'api/search'),
158
                'method' => 'get',
159
                'responseFormat' => 'json',
160
            ],
161
            'updateFolderEndpoint' => [
162
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
163
                'method' => 'put',
164
                'payloadFormat' => 'urlencoded',
165
            ],
166
            'deleteEndpoint' => [
167
                'url' => Controller::join_links($baseLink, 'api/delete'),
168
                'method' => 'delete',
169
                'payloadFormat' => 'urlencoded',
170
            ],
171
            'historyEndpoint' => [
172
                'url' => Controller::join_links($baseLink, 'api/history'),
173
                'method' => 'get',
174
                'responseFormat' => 'json'
175
            ],
176
            'limit' => $this->config()->page_length,
177
            'form' => [
178
                'fileEditForm' => [
179
                    'schemaUrl' => $this->Link('schema/fileEditForm')
180
                ],
181
                'fileInsertForm' => [
182
                    'schemaUrl' => $this->Link('schema/fileInsertForm')
183
                ],
184
                'addToCampaignForm' => [
185
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
186
                ],
187
                'fileHistoryForm' => [
188
                    'schemaUrl' => $this->Link('schema/fileHistoryForm')
189
                ]
190
            ],
191
        ]);
192
    }
193
194
    /**
195
     * Fetches a collection of files by ParentID.
196
     *
197
     * @param HTTPRequest $request
198
     * @return HTTPResponse
199
     */
200
    public function apiReadFolder(HTTPRequest $request)
201
    {
202
        $params = $request->requestVars();
203
        $items = array();
204
        $parentId = null;
0 ignored issues
show
Unused Code introduced by
$parentId 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...
205
        $folderID = null;
0 ignored issues
show
Unused Code introduced by
$folderID 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...
206
207
        if (!isset($params['id']) && !strlen($params['id'])) {
208
            $this->httpError(400);
209
        }
210
211
        $folderID = (int)$params['id'];
212
        /** @var Folder $folder */
213
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
214
215
        if (!$folder) {
216
            $this->httpError(400);
217
        }
218
219
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
220
        $files = $this->getList()->filter('ParentID', $folderID);
221
222
        if ($files) {
223
            /** @var File $file */
224
            foreach ($files as $file) {
225
                if (!$file->canView()) {
226
                    continue;
227
                }
228
229
                $items[] = $this->getObjectFromData($file);
230
            }
231
        }
232
233
        // Build parents (for breadcrumbs)
234
        $parents = [];
235
        $next = $folder->Parent();
236
        while ($next && $next->exists()) {
237
            array_unshift($parents, [
238
                'id' => $next->ID,
239
                'title' => $next->getTitle(),
240
                'filename' => $next->getFilename(),
241
            ]);
242
            if ($next->ParentID) {
243
                $next = $next->Parent();
244
            } else {
245
                break;
246
            }
247
        }
248
249
        $column = 'title';
250
        $direction = 'asc';
251
        if (isset($params['sort'])) {
252
            list($column, $direction) = explode(',', $params['sort']);
253
        }
254
        $multiplier = ($direction === 'asc') ? 1 : -1;
255
256
        usort($items, function ($a, $b) use ($column, $multiplier) {
257
            if (!isset($a[$column]) || !isset($b[$column])) {
258
                return 0;
259
            }
260
            if ($a['type'] === 'folder' && $b['type'] !== 'folder') {
261
                return -1;
262
            }
263
            if ($b['type'] === 'folder' && $a['type'] !== 'folder') {
264
                return 1;
265
            }
266
            $numeric = (is_numeric($a[$column]) && is_numeric($b[$column]));
267
            $fieldA = ($numeric) ? floatval($a[$column]) : strtolower($a[$column]);
268
            $fieldB = ($numeric) ? floatval($b[$column]) : strtolower($b[$column]);
269
270
            if ($fieldA < $fieldB) {
271
                return $multiplier * -1;
272
            }
273
274
            if ($fieldA > $fieldB) {
275
                return $multiplier;
276
            }
277
278
            return 0;
279
        });
280
281
        $page = (isset($params['page'])) ? $params['page'] : 0;
282
        $limit = (isset($params['limit'])) ? $params['limit'] : $this->config()->page_length;
283
        $filteredItems = array_slice($items, $page * $limit, $limit);
284
285
        // Build response
286
        $response = new HTTPResponse();
287
        $response->addHeader('Content-Type', 'application/json');
288
        $response->setBody(json_encode([
289
            'files' => $filteredItems,
290
            'title' => $folder->getTitle(),
291
            'count' => count($items),
292
            'parents' => $parents,
293
            'parent' => $parents ? $parents[count($parents) - 1] : null,
294
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
295
            'folderID' => $folderID,
296
            'canEdit' => $folder->canEdit(),
297
            'canDelete' => $folder->canArchive(),
298
        ]));
299
300
        return $response;
301
    }
302
303
    /**
304
     * @param HTTPRequest $request
305
     *
306
     * @return HTTPResponse
307
     */
308
    public function apiSearch(HTTPRequest $request)
309
    {
310
        $params = $request->getVars();
311
        $list = $this->getList($params);
312
313
        $response = new HTTPResponse();
314
        $response->addHeader('Content-Type', 'application/json');
315
        $response->setBody(json_encode([
316
            // Serialisation
317
            "files" => array_map(function ($file) {
318
                return $this->getObjectFromData($file);
319
            }, $list->toArray()),
320
            "count" => $list->count(),
321
        ]));
322
323
        return $response;
324
    }
325
326
    /**
327
     * @param HTTPRequest $request
328
     *
329
     * @return HTTPResponse
330
     */
331
    public function apiDelete(HTTPRequest $request)
332
    {
333
        parse_str($request->getBody(), $vars);
334
335
        // CSRF check
336
        $token = SecurityToken::inst();
337 View Code Duplication
        if (empty($vars[$token->getName()]) || !$token->check($vars[$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...
338
            return new HTTPResponse(null, 400);
339
        }
340
341 View Code Duplication
        if (!isset($vars['ids']) || !$vars['ids']) {
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...
342
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
343
                ->addHeader('Content-Type', 'application/json');
344
        }
345
346
        $fileIds = $vars['ids'];
347
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
348
349 View Code Duplication
        if (!count($files)) {
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...
350
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
351
                ->addHeader('Content-Type', 'application/json');
352
        }
353
354
        if (!min(array_map(function (File $file) {
355
            return $file->canArchive();
356
        }, $files))) {
357
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
358
                ->addHeader('Content-Type', 'application/json');
359
        }
360
361
        /** @var File $file */
362
        foreach ($files as $file) {
363
            $file->doArchive();
364
        }
365
366
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
367
            ->addHeader('Content-Type', 'application/json');
368
    }
369
370
    /**
371
     * Creates a single file based on a form-urlencoded upload.
372
     *
373
     * @param HTTPRequest $request
374
     * @return HTTPRequest|HTTPResponse
375
     */
376
    public function apiCreateFile(HTTPRequest $request)
377
    {
378
        $data = $request->postVars();
379
        $upload = $this->getUpload();
380
381
        // CSRF check
382
        $token = SecurityToken::inst();
383 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...
384
            return new HTTPResponse(null, 400);
385
        }
386
387
        // Check parent record
388
        /** @var Folder $parentRecord */
389
        $parentRecord = null;
390
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
391
            $parentRecord = Folder::get()->byID($data['ParentID']);
392
        }
393
        $data['Parent'] = $parentRecord;
394
395
        $tmpFile = $request->postVar('Upload');
396
        if (!$upload->validate($tmpFile)) {
397
            $result = ['message' => null];
398
            $errors = $upload->getErrors();
399
            if ($message = array_shift($errors)) {
400
                $result['message'] = [
401
                    'type' => 'error',
402
                    'value' => $message,
403
                ];
404
            }
405
            return (new HTTPResponse(json_encode($result), 400))
406
                ->addHeader('Content-Type', 'application/json');
407
        }
408
409
        // TODO Allow batch uploads
410
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
411
        /** @var File $file */
412
        $file = Injector::inst()->create($fileClass);
413
414
        // check canCreate permissions
415 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...
416
            $result = ['message' => [
417
                'type' => 'error',
418
                'value' => _t(
419
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
420
                    'You do not have permission to add files'
421
                )
422
            ]];
423
            return (new HTTPResponse(json_encode($result), 403))
424
                ->addHeader('Content-Type', 'application/json');
425
        }
426
427
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
428 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...
429
            $result = ['message' => [
430
                'type' => 'error',
431
                'value' => _t(
432
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
433
                    'Failed to load file'
434
                )
435
            ]];
436
            return (new HTTPResponse(json_encode($result), 400))
437
                ->addHeader('Content-Type', 'application/json');
438
        }
439
440
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
441
        $file->write();
442
443
        $result = [$this->getObjectFromData($file)];
444
445
        return (new HTTPResponse(json_encode($result)))
446
            ->addHeader('Content-Type', 'application/json');
447
    }
448
449
    /**
450
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
451
     *
452
     * @param HTTPRequest $request
453
     * @return HTTPResponse
454
     */
455
    public function apiHistory(HTTPRequest $request)
456
    {
457
        // CSRF check not required as the GET request has no side effects.
458
        $fileId = $request->getVar('fileId');
459
460
        if (!$fileId || !is_numeric($fileId)) {
461
            return new HTTPResponse(null, 400);
462
        }
463
464
        $class = File::class;
465
        $file = DataObject::get($class)->byID($fileId);
466
467
        if (!$file) {
468
            return new HTTPResponse(null, 404);
469
        }
470
471
        if (!$file->canView()) {
472
            return new HTTPResponse(null, 403);
473
        }
474
475
        $versions = Versioned::get_all_versions($class, $fileId)
476
            ->limit($this->config()->max_history_entries)
477
            ->sort('Version', 'DESC');
478
479
        $output = array();
480
        $next = array();
481
        $prev = null;
482
483
        // swap the order so we can get the version number to compare against.
484
        // i.e version 3 needs to know version 2 is the previous version
485
        $copy = $versions->map('Version', 'Version')->toArray();
486
        foreach (array_reverse($copy) as $k => $v) {
487
            if ($prev) {
488
                $next[$v] = $prev;
489
            }
490
491
            $prev = $v;
492
        }
493
494
        $_cachedMembers = array();
495
496
        /** @var File $version */
497
        foreach ($versions as $version) {
498
            $author = null;
499
500
            if ($version->AuthorID) {
501
                if (!isset($_cachedMembers[$version->AuthorID])) {
502
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
503
                        ->byID($version->AuthorID);
504
                }
505
506
                $author = $_cachedMembers[$version->AuthorID];
507
            }
508
509
            if ($version->canView()) {
510
                if (isset($next[$version->Version])) {
511
                    $summary = $version->humanizedChanges(
512
                        $version->Version,
513
                        $next[$version->Version]
514
                    );
515
516
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
517
                    // generic message
518
                    if (!$summary) {
519
                        $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.SAVEDFILE', "Saved file");
520
                    }
521
                } else {
522
                    $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UPLOADEDFILE', "Uploaded file");
523
                }
524
525
                $output[] = array(
526
                    'versionid' => $version->Version,
527
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
528
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
529
                    'status' => ($version->WasPublished) ? _t('File.PUBLISHED', 'Published') : '',
530
                    'author' => ($author)
531
                        ? $author->Name
532
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNKNOWN', "Unknown"),
533
                    'summary' => ($summary)
534
                        ? $summary
535
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NOSUMMARY', "No summary available")
536
                );
537
            }
538
        }
539
540
        return
541
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
542
    }
543
544
545
    /**
546
     * Creates a single folder, within an optional parent folder.
547
     *
548
     * @param HTTPRequest $request
549
     * @return HTTPRequest|HTTPResponse
550
     */
551
    public function apiCreateFolder(HTTPRequest $request)
552
    {
553
        $data = $request->postVars();
554
555
        $class = 'SilverStripe\\Assets\\Folder';
556
557
        // CSRF check
558
        $token = SecurityToken::inst();
559 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...
560
            return new HTTPResponse(null, 400);
561
        }
562
563
        // check addchildren permissions
564
        /** @var Folder $parentRecord */
565
        $parentRecord = null;
566
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
567
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
568
        }
569
        $data['Parent'] = $parentRecord;
570
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
571
572
        // Build filename
573
        $baseFilename = isset($data['Name'])
574
            ? basename($data['Name'])
575
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
576
577
        if ($parentRecord && $parentRecord->ID) {
578
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
579
        }
580
581
        // Ensure name is unique
582
        $nameGenerator = $this->getNameGenerator($baseFilename);
583
        $filename = null;
584
        foreach ($nameGenerator as $filename) {
585
            if (! File::find($filename)) {
586
                break;
587
            }
588
        }
589
        $data['Name'] = basename($filename);
590
591
        // Create record
592
        /** @var Folder $record */
593
        $record = Injector::inst()->create($class);
594
595
        // check create permissions
596
        if (!$record->canCreate(null, $data)) {
597
            return (new HTTPResponse(null, 403))
598
                ->addHeader('Content-Type', 'application/json');
599
        }
600
601
        $record->ParentID = $data['ParentID'];
602
        $record->Name = $record->Title = basename($data['Name']);
603
        $record->write();
604
605
        $result = $this->getObjectFromData($record);
606
607
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
608
    }
609
610
    /**
611
     * Redirects 3.x style detail links to new 4.x style routing.
612
     *
613
     * @param HTTPRequest $request
614
     */
615
    public function legacyRedirectForEditView($request)
616
    {
617
        $fileID = $request->param('FileID');
618
        /** @var File $file */
619
        $file = File::get()->byID($fileID);
620
        $link = $this->getFileEditLink($file) ?: $this->Link();
621
        $this->redirect($link);
622
    }
623
624
    /**
625
     * Given a file return the CMS link to edit it
626
     *
627
     * @param File $file
628
     * @return string
629
     */
630
    public function getFileEditLink($file)
631
    {
632
        if (!$file || !$file->isInDB()) {
633
            return null;
634
        }
635
636
        return Controller::join_links(
637
            $this->Link('show'),
638
            $file->ParentID,
639
            'edit',
640
            $file->ID
641
        );
642
    }
643
644
    /**
645
     * Get the search context from {@link File}, used to create the search form
646
     * as well as power the /search API endpoint.
647
     *
648
     * @return SearchContext
649
     */
650
    public function getSearchContext()
651
    {
652
        $context = File::singleton()->getDefaultSearchContext();
653
654
        // Customize fields
655
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
656
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
657
        ->setConfig('showcalendar', true);
658
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
659
        ->setConfig('showcalendar', true);
660
        $dateGroup = FieldGroup::create(
661
            $dateHeader,
662
            $dateFrom,
663
            $dateTo
664
        );
665
        $context->addField($dateGroup);
666
        /** @skipUpgrade */
667
        $appCategories = array(
668
            'archive' => _t(
669
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
670
                'Archive'
671
            ),
672
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
673
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
674
            'flash' => _t(
675
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
676
                'Flash',
677
                'The fileformat'
678
            ),
679
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
680
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
681
        );
682
        $context->addField(
683
            $typeDropdown = new DropdownField(
684
                'AppCategory',
685
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
686
                $appCategories
687
            )
688
        );
689
690
        $typeDropdown->setEmptyString(' ');
691
692
        $currentfolderLabel = _t(
693
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
694
            'Limit to current folder?'
695
        );
696
        $context->addField(
697
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
698
        );
699
        $context->getFields()->removeByName('Title');
700
701
        return $context;
702
    }
703
704
    /**
705
     * Get an asset renamer for the given filename.
706
     *
707
     * @param  string             $filename Path name
708
     * @return AssetNameGenerator
709
     */
710
    protected function getNameGenerator($filename)
711
    {
712
        return Injector::inst()
713
            ->createWithArgs('AssetNameGenerator', array($filename));
714
    }
715
716
    /**
717
     * @todo Implement on client
718
     *
719
     * @param bool $unlinked
720
     * @return ArrayList
721
     */
722
    public function breadcrumbs($unlinked = false)
723
    {
724
        return null;
725
    }
726
727
728
    /**
729
     * Don't include class namespace in auto-generated CSS class
730
     */
731
    public function baseCSSClasses()
732
    {
733
        return 'AssetAdmin LeftAndMain';
734
    }
735
736
    public function providePermissions()
737
    {
738
        return array(
739
            "CMS_ACCESS_AssetAdmin" => array(
740
                '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...
741
                    'title' => static::menu_title()
742
                )),
743
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
744
            )
745
        );
746
    }
747
748
    /**
749
     * Build a form scaffolder for this model
750
     *
751
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
752
     *
753
     * @param File $file
754
     * @return FormFactory
755
     */
756
    public function getFormFactory(File $file)
757
    {
758
        // Get service name based on file class
759
        $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...
760
        if ($file instanceof Folder) {
761
            $name = FolderFormFactory::class;
762
        } elseif ($file instanceof Image) {
763
            $name = ImageFormFactory::class;
764
        } else {
765
            $name = FileFormFactory::class;
766
        }
767
        return Injector::inst()->get($name);
768
    }
769
770
    /**
771
     * The form is used to generate a form schema,
772
     * as well as an intermediary object to process data through API endpoints.
773
     * Since it's used directly on API endpoints, it does not have any form actions.
774
     * It handles both {@link File} and {@link Folder} records.
775
     *
776
     * @param int $id
777
     * @return Form
778
     */
779
    public function getFileEditForm($id)
780
    {
781
        /** @var File $file */
782
        $file = $this->getList()->byID($id);
783
784
        if (!$file) {
785
            $this->httpError(404);
786
            return null;
787
        }
788
789 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...
790
            $this->httpError(403, _t(
791
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
792
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
793
                '',
794
                ['ObjectTitle' => $file->i18n_singular_name()]
795
            ));
796
            return null;
797
        }
798
799
        $scaffolder = $this->getFormFactory($file);
800
        $form = $scaffolder->getForm($this, 'fileEditForm', [
801
            'Record' => $file
802
        ]);
803
804
        // Configure form to respond to validation errors with form schema
805
        // if requested via react.
806 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...
807
            $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $id);
808
            return $this->getSchemaResponse($schemaId, $form, $errors);
0 ignored issues
show
Documentation introduced by
$schemaId is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with $errors.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
809
        });
810
811
        return $form;
812
    }
813
814
    /**
815
     * Get file edit form
816
     *
817
     * @return Form
818
     */
819 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...
820
    {
821
        // Get ID either from posted back value, or url parameter
822
        $request = $this->getRequest();
823
        $id = $request->param('ID') ?: $request->postVar('ID');
824
        return $this->getFileEditForm($id);
825
    }
826
827
    /**
828
     * The form is used to generate a form schema,
829
     * as well as an intermediary object to process data through API endpoints.
830
     * Since it's used directly on API endpoints, it does not have any form actions.
831
     * It handles both {@link File} and {@link Folder} records.
832
     *
833
     * @param int $id
834
     * @return Form
835
     */
836
    public function getFileInsertForm($id)
837
    {
838
        /** @var File $file */
839
        $file = $this->getList()->byID($id);
840
841 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...
842
            $this->httpError(403, _t(
843
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
844
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
845
                '',
846
                ['ObjectTitle' => $file->i18n_singular_name()]
847
            ));
848
            return null;
849
        }
850
851
        $scaffolder = $this->getFormFactory($file);
852
        $form = $scaffolder->getForm($this, 'fileInsertForm', [
853
            'Record' => $file,
854
            'Type' => 'insert',
855
        ]);
856
857
        return $form;
858
    }
859
860
    /**
861
     * Get file insert form
862
     *
863
     * @return Form
864
     */
865 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...
866
    {
867
        // Get ID either from posted back value, or url parameter
868
        $request = $this->getRequest();
869
        $id = $request->param('ID') ?: $request->postVar('ID');
870
        return $this->getFileInsertForm($id);
871
    }
872
873
    /**
874
     * @param array $context
875
     * @return Form
876
     * @throws InvalidArgumentException
877
     */
878
    public function getFileHistoryForm($context)
879
    {
880
        // Check context
881
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
882
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
883
        }
884
        $id = $context['RecordID'];
885
        $versionId = $context['RecordVersion'];
886
        if (!$id || !$versionId) {
887
            return $this->httpError(404);
888
        }
889
890
        /** @var File $file */
891
        $file = Versioned::get_version(File::class, $id, $versionId);
892
        if (!$file) {
893
            return $this->httpError(404);
894
        }
895
896 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...
897
            $this->httpError(403, _t(
898
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
899
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
900
                '',
901
                ['ObjectTitle' => $file->i18n_singular_name()]
902
            ));
903
            return null;
904
        }
905
906
        $effectiveContext = array_merge($context, ['Record' => $file]);
907
        /** @var FormFactory $scaffolder */
908
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
909
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
910
911
        // Configure form to respond to validation errors with form schema
912
        // if requested via react.
913 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...
914
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
915
            return $this->getSchemaResponse($schemaId, $form, $errors);
0 ignored issues
show
Documentation introduced by
$schemaId is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with $errors.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
916
        });
917
918
        return $form;
919
    }
920
921
    /**
922
     * Gets a JSON schema representing the current edit form.
923
     *
924
     * WARNING: Experimental API.
925
     *
926
     * @param HTTPRequest $request
927
     * @return HTTPResponse
928
     */
929
    public function schema($request)
930
    {
931
        $formName = $request->param('FormName');
932
        if ($formName !== 'fileHistoryForm') {
933
            return parent::schema($request);
934
        }
935
936
        // Get schema for history form
937
        // @todo Eventually all form scaffolding will be based on context rather than record ID
938
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
939
        $itemID = $request->param('ItemID');
940
        $version = $request->param('OtherItemID');
941
        $form = $this->getFileHistoryForm([
942
            'RecordID' => $itemID,
943
            'RecordVersion' => $version,
944
        ]);
945
946
        // Respond with this schema
947
        $response = $this->getResponse();
948
        $response->addHeader('Content-Type', 'application/json');
949
        $schemaID = $this->getRequest()->getURL();
950
        return $this->getSchemaResponse($schemaID, $form);
0 ignored issues
show
Documentation introduced by
$schemaID is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Bug introduced by
It seems like $form defined by $this->getFileHistoryFor...dVersion' => $version)) on line 941 can also be of type object<SilverStripe\Forms\Form>; however, SilverStripe\Admin\LeftA...in::getSchemaResponse() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
951
    }
952
953
    /**
954
     * Get file history form
955
     *
956
     * @return Form
957
     */
958
    public function fileHistoryForm()
959
    {
960
        $request = $this->getRequest();
961
        $id = $request->param('ID') ?: $request->postVar('ID');
962
        $version = $request->param('OtherID') ?: $request->postVar('Version');
963
        $form = $this->getFileHistoryForm([
964
            'RecordID' => $id,
965
            'RecordVersion' => $version,
966
        ]);
967
        return $form;
968
    }
969
970
    /**
971
     * @param array $data
972
     * @param Form $form
973
     * @return HTTPResponse
974
     */
975
    public function save($data, $form)
976
    {
977
        return $this->saveOrPublish($data, $form, false);
978
    }
979
980
    /**
981
     * @param array $data
982
     * @param Form $form
983
     * @return HTTPResponse
984
     */
985
    public function publish($data, $form)
986
    {
987
        return $this->saveOrPublish($data, $form, true);
988
    }
989
990
    /**
991
     * Update thisrecord
992
     *
993
     * @param array $data
994
     * @param Form $form
995
     * @param bool $doPublish
996
     * @return HTTPResponse
997
     */
998
    protected function saveOrPublish($data, $form, $doPublish = false)
999
    {
1000 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...
1001
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1002
                ->addHeader('Content-Type', 'application/json');
1003
        }
1004
1005
        $id = (int) $data['ID'];
1006
        /** @var File $record */
1007
        $record = $this->getList()->filter('ID', $id)->first();
1008
1009 View Code Duplication
        if (!$record) {
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...
1010
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1011
                ->addHeader('Content-Type', 'application/json');
1012
        }
1013
1014
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
1015
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1016
                ->addHeader('Content-Type', 'application/json');
1017
        }
1018
1019
        $form->saveInto($record);
1020
        $record->write();
1021
1022
        // Publish this record and owned objects
1023
        if ($doPublish) {
1024
            $record->publishRecursive();
1025
        }
1026
1027
        // Note: Force return of schema / state in success result
1028
        return $this->getRecordUpdatedResponse($record, $form);
1029
    }
1030
1031
    public function unpublish($data, $form)
1032
    {
1033 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...
1034
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1035
                ->addHeader('Content-Type', 'application/json');
1036
        }
1037
1038
        $id = (int) $data['ID'];
1039
        /** @var File $record */
1040
        $record = $this->getList()->filter('ID', $id)->first();
1041
1042
        if (!$record) {
1043
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1044
                ->addHeader('Content-Type', 'application/json');
1045
        }
1046
1047
        if (!$record->canUnpublish()) {
1048
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1049
                ->addHeader('Content-Type', 'application/json');
1050
        }
1051
1052
        $record->doUnpublish();
1053
        return $this->getRecordUpdatedResponse($record, $form);
1054
    }
1055
1056
    /**
1057
     * @param File $file
1058
     *
1059
     * @return array
1060
     */
1061
    public function getObjectFromData(File $file)
1062
    {
1063
        $object = array(
1064
            'id' => $file->ID,
1065
            'created' => $file->Created,
1066
            'lastUpdated' => $file->LastEdited,
1067
            'owner' => null,
1068
            'parent' => null,
1069
            'title' => $file->Title,
1070
            'exists' => $file->exists(), // Broken file check
1071
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1072
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
1073
            'name' => $file->Name,
1074
            'filename' => $file->Filename,
1075
            'extension' => $file->Extension,
0 ignored issues
show
Bug introduced by
The property Extension does not seem to exist. Did you mean allowed_extensions?

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...
1076
            'size' => $file->AbsoluteSize,
1077
            'url' => $file->AbsoluteURL,
1078
            'published' => $file->isPublished(),
1079
            'modified' => $file->isModifiedOnDraft(),
1080
            'draft' => $file->isOnDraftOnly(),
1081
            'canEdit' => $file->canEdit(),
1082
            'canDelete' => $file->canArchive(),
1083
        );
1084
1085
        /** @var Member $owner */
1086
        $owner = $file->Owner();
1087
1088
        if ($owner) {
1089
            $object['owner'] = array(
1090
                'id' => $owner->ID,
1091
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1092
            );
1093
        }
1094
1095
        /** @var Folder $parent */
1096
        $parent = $file->Parent();
1097
1098
        if ($parent) {
1099
            $object['parent'] = array(
1100
                'id' => $parent->ID,
1101
                'title' => $parent->Title,
1102
                'filename' => $parent->Filename,
1103
            );
1104
        }
1105
1106
        /** @var File $file */
1107
        if ($file->getIsImage()) {
1108
            // Small thumbnail
1109
            $smallWidth = UploadField::config()->get('thumbnail_width');
1110
            $smallHeight = UploadField::config()->get('thumbnail_height');
1111
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1112
            if ($smallThumbnail && $smallThumbnail->exists()) {
1113
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1114
            }
1115
1116
            // Large thumbnail
1117
            $width = $this->config()->get('thumbnail_width');
1118
            $height = $this->config()->get('thumbnail_height');
1119
            $thumbnail = $file->FitMax($width, $height);
1120
            if ($thumbnail && $thumbnail->exists()) {
1121
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1122
            }
1123
            $object['dimensions']['width'] = $file->Width;
1124
            $object['dimensions']['height'] = $file->Height;
0 ignored issues
show
Bug introduced by
The property Height does not seem to exist. Did you mean asset_preview_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...
1125
        } else {
1126
            $object['thumbnail'] = $file->PreviewLink();
1127
        }
1128
1129
        return $object;
1130
    }
1131
1132
    /**
1133
     * Returns the files and subfolders contained in the currently selected folder,
1134
     * defaulting to the root node. Doubles as search results, if any search parameters
1135
     * are set through {@link SearchForm()}.
1136
     *
1137
     * @param array $params Unsanitised request parameters
1138
     * @return DataList
1139
     */
1140
    protected function getList($params = array())
1141
    {
1142
        $context = $this->getSearchContext();
1143
1144
        // Overwrite name filter to search both Name and Title attributes
1145
        $context->removeFilterByName('Name');
1146
1147
        // Lazy loaded list. Allows adding new filters through SearchContext.
1148
        /** @var DataList $list */
1149
        $list = $context->getResults($params);
1150
1151
        // Re-add previously removed "Name" filter as combined filter
1152
        // TODO Replace with composite SearchFilter once that API exists
1153
        if (!empty($params['Name'])) {
1154
            $list = $list->filterAny(array(
1155
                'Name:PartialMatch' => $params['Name'],
1156
                'Title:PartialMatch' => $params['Name']
1157
            ));
1158
        }
1159
1160
        // Optionally limit search to a folder (non-recursive)
1161
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
1162
            $list = $list->filter('ParentID', $params['ParentID']);
1163
        }
1164
1165
        // Date filtering
1166 View Code Duplication
        if (!empty($params['CreatedFrom'])) {
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...
1167
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
1168
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
1169
        }
1170 View Code Duplication
        if (!empty($params['CreatedTo'])) {
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...
1171
            $toDate = new DateField(null, null, $params['CreatedTo']);
1172
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
1173
        }
1174
1175
        // Categories
1176
        if (!empty($filters['AppCategory']) && !empty(File::config()->app_categories[$filters['AppCategory']])) {
0 ignored issues
show
Bug introduced by
The variable $filters seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
1177
            $extensions = File::config()->app_categories[$filters['AppCategory']];
1178
            $list = $list->filter('Name:PartialMatch', $extensions);
1179
        }
1180
1181
        // Sort folders first
1182
        $list = $list->sort(
1183
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
1184
        );
1185
1186
        // Pagination
1187
        if (isset($filters['page']) && isset($filters['limit'])) {
1188
            $page = $filters['page'];
1189
            $limit = $filters['limit'];
1190
            $offset = ($page - 1) * $limit;
1191
            $list = $list->limit($limit, $offset);
1192
        }
1193
1194
        // Access checks
1195
        $list = $list->filterByCallback(function (File $file) {
1196
            return $file->canView();
1197
        });
1198
1199
        return $list;
1200
    }
1201
1202
    /**
1203
     * Action handler for adding pages to a campaign
1204
     *
1205
     * @param array $data
1206
     * @param Form $form
1207
     * @return DBHTMLText|HTTPResponse
1208
     */
1209
    public function addtocampaign($data, $form)
1210
    {
1211
        $id = $data['ID'];
1212
        $record = $this->getList()->byID($id);
1213
1214
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1215
        $results = $handler->addToCampaign($record, $data['Campaign']);
1216
        if (!isset($results)) {
1217
            return null;
1218
        }
1219
1220
        // Send extra "message" data with schema response
1221
        $extraData = ['message' => $results];
1222
        $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1223
        return $this->getSchemaResponse($schemaId, $form, null, $extraData);
0 ignored issues
show
Documentation introduced by
$schemaId is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with null.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1224
    }
1225
1226
    /**
1227
     * Url handler for add to campaign form
1228
     *
1229
     * @param HTTPRequest $request
1230
     * @return Form
1231
     */
1232
    public function addToCampaignForm($request)
1233
    {
1234
        // Get ID either from posted back value, or url parameter
1235
        $id = $request->param('ID') ?: $request->postVar('ID');
1236
        return $this->getAddToCampaignForm($id);
1237
    }
1238
1239
    /**
1240
     * @param int $id
1241
     * @return Form
1242
     */
1243
    public function getAddToCampaignForm($id)
1244
    {
1245
        // Get record-specific fields
1246
        $record = $this->getList()->byID($id);
1247
1248
        if (!$record) {
1249
            $this->httpError(404, _t(
1250
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1251
                'That {Type} couldn\'t be found',
1252
                '',
1253
                ['Type' => File::singleton()->i18n_singular_name()]
1254
            ));
1255
            return null;
1256
        }
1257 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...
1258
            $this->httpError(403, _t(
1259
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1260
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1261
                '',
1262
                ['ObjectTitle' => $record->i18n_singular_name()]
1263
            ));
1264
            return null;
1265
        }
1266
1267
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1268
        $form = $handler->Form($record);
1269
1270 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...
1271
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1272
            return $this->getSchemaResponse($schemaId, $form, $errors);
0 ignored issues
show
Documentation introduced by
$schemaId is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with $errors.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1273
        });
1274
1275
        return $form;
1276
    }
1277
1278
    /**
1279
     * @return Upload
1280
     */
1281
    protected function getUpload()
1282
    {
1283
        $upload = Upload::create();
1284
        $upload->getValidator()->setAllowedExtensions(
1285
            // filter out '' since this would be a regex problem on JS end
1286
            array_filter(File::config()->get('allowed_extensions'))
1287
        );
1288
1289
        return $upload;
1290
    }
1291
1292
    /**
1293
     * Get response for successfully updated record
1294
     *
1295
     * @param File $record
1296
     * @param Form $form
1297
     * @return HTTPResponse
1298
     */
1299
    protected function getRecordUpdatedResponse($record, $form)
1300
    {
1301
        // Return the record data in the same response as the schema to save a postback
1302
        $schemaData = ['record' => $this->getObjectFromData($record)];
1303
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1304
        return $this->getSchemaResponse($schemaId, $form, null, $schemaData);
0 ignored issues
show
Documentation introduced by
$schemaId is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with null.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1305
    }
1306
}
1307