Completed
Pull Request — master (#327)
by
unknown
01:44
created

AssetAdmin::apiReadFolder()   F

Complexity

Conditions 28
Paths 768

Size

Total Lines 102
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

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

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...
859
        });
860
861
        return $form;
862
    }
863
864
    /**
865
     * Get form for selecting a file
866
     *
867
     * @return Form
868
     */
869
    public function fileSelectForm()
870
    {
871
        // Get ID either from posted back value, or url parameter
872
        $request = $this->getRequest();
873
        $id = $request->param('ID') ?: $request->postVar('ID');
874
        return $this->getFileSelectForm($id);
875
    }
876
877
    /**
878
     * Get form for selecting a file
879
     *
880
     * @param int $id ID of the record being selected
881
     * @return Form
882
     */
883
    public function getFileSelectForm($id)
884
    {
885
        return $this->getAbstractFileForm($id, 'fileSelectForm', [ 'Type' => AssetFormFactory::TYPE_SELECT ]);
886
    }
887
888
    /**
889
     * @param array $context
890
     * @return Form
891
     * @throws InvalidArgumentException
892
     */
893
    public function getFileHistoryForm($context)
894
    {
895
        // Check context
896
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
897
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
898
        }
899
        $id = $context['RecordID'];
900
        $versionId = $context['RecordVersion'];
901
        if (!$id || !$versionId) {
902
            return $this->httpError(404);
903
        }
904
905
        /** @var File $file */
906
        $file = Versioned::get_version(File::class, $id, $versionId);
907
        if (!$file) {
908
            return $this->httpError(404);
909
        }
910
911 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...
912
            $this->httpError(403, _t(
913
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
914
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
915
                '',
916
                ['ObjectTitle' => $file->i18n_singular_name()]
917
            ));
918
            return null;
919
        }
920
921
        $effectiveContext = array_merge($context, ['Record' => $file]);
922
        /** @var FormFactory $scaffolder */
923
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
924
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
925
926
        // Configure form to respond to validation errors with form schema
927
        // if requested via react.
928 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...
929
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
930
            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...
931
        });
932
933
        return $form;
934
    }
935
936
    /**
937
     * Gets a JSON schema representing the current edit form.
938
     *
939
     * WARNING: Experimental API.
940
     *
941
     * @param HTTPRequest $request
942
     * @return HTTPResponse
943
     */
944
    public function schema($request)
945
    {
946
        $formName = $request->param('FormName');
947
        if ($formName !== 'fileHistoryForm') {
948
            return parent::schema($request);
949
        }
950
951
        // Get schema for history form
952
        // @todo Eventually all form scaffolding will be based on context rather than record ID
953
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
954
        $itemID = $request->param('ItemID');
955
        $version = $request->param('OtherItemID');
956
        $form = $this->getFileHistoryForm([
957
            'RecordID' => $itemID,
958
            'RecordVersion' => $version,
959
        ]);
960
961
        // Respond with this schema
962
        $response = $this->getResponse();
963
        $response->addHeader('Content-Type', 'application/json');
964
        $schemaID = $this->getRequest()->getURL();
965
        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 956 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...
966
    }
967
968
    /**
969
     * Get file history form
970
     *
971
     * @return Form
972
     */
973
    public function fileHistoryForm()
974
    {
975
        $request = $this->getRequest();
976
        $id = $request->param('ID') ?: $request->postVar('ID');
977
        $version = $request->param('OtherID') ?: $request->postVar('Version');
978
        $form = $this->getFileHistoryForm([
979
            'RecordID' => $id,
980
            'RecordVersion' => $version,
981
        ]);
982
        return $form;
983
    }
984
985
    /**
986
     * @param array $data
987
     * @param Form $form
988
     * @return HTTPResponse
989
     */
990
    public function save($data, $form)
991
    {
992
        return $this->saveOrPublish($data, $form, false);
993
    }
994
995
    /**
996
     * @param array $data
997
     * @param Form $form
998
     * @return HTTPResponse
999
     */
1000
    public function publish($data, $form)
1001
    {
1002
        return $this->saveOrPublish($data, $form, true);
1003
    }
1004
1005
    /**
1006
     * Update thisrecord
1007
     *
1008
     * @param array $data
1009
     * @param Form $form
1010
     * @param bool $doPublish
1011
     * @return HTTPResponse
1012
     */
1013
    protected function saveOrPublish($data, $form, $doPublish = false)
1014
    {
1015 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...
1016
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1017
                ->addHeader('Content-Type', 'application/json');
1018
        }
1019
1020
        $id = (int) $data['ID'];
1021
        /** @var File $record */
1022
        $record = $this->getList()->filter('ID', $id)->first();
1023
1024 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...
1025
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1026
                ->addHeader('Content-Type', 'application/json');
1027
        }
1028
1029
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
1030
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1031
                ->addHeader('Content-Type', 'application/json');
1032
        }
1033
1034
        $form->saveInto($record);
1035
        $record->write();
1036
1037
        // Publish this record and owned objects
1038
        if ($doPublish) {
1039
            $record->publishRecursive();
1040
        }
1041
1042
        // Note: Force return of schema / state in success result
1043
        return $this->getRecordUpdatedResponse($record, $form);
1044
    }
1045
1046
    public function unpublish($data, $form)
1047
    {
1048 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...
1049
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1050
                ->addHeader('Content-Type', 'application/json');
1051
        }
1052
1053
        $id = (int) $data['ID'];
1054
        /** @var File $record */
1055
        $record = $this->getList()->filter('ID', $id)->first();
1056
1057
        if (!$record) {
1058
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1059
                ->addHeader('Content-Type', 'application/json');
1060
        }
1061
1062
        if (!$record->canUnpublish()) {
1063
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1064
                ->addHeader('Content-Type', 'application/json');
1065
        }
1066
1067
        $record->doUnpublish();
1068
        return $this->getRecordUpdatedResponse($record, $form);
1069
    }
1070
1071
    /**
1072
     * @param File $file
1073
     *
1074
     * @return array
1075
     */
1076
    public function getObjectFromData(File $file)
1077
    {
1078
        $object = array(
1079
            'id' => $file->ID,
1080
            'created' => $file->Created,
1081
            'lastUpdated' => $file->LastEdited,
1082
            'owner' => null,
1083
            'parent' => null,
1084
            'title' => $file->Title,
1085
            'exists' => $file->exists(), // Broken file check
1086
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1087
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
1088
            'name' => $file->Name,
1089
            'filename' => $file->Filename,
1090
            '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...
1091
            'size' => $file->AbsoluteSize,
1092
            'url' => $file->AbsoluteURL,
1093
            'published' => $file->isPublished(),
1094
            'modified' => $file->isModifiedOnDraft(),
1095
            'draft' => $file->isOnDraftOnly(),
1096
            'canEdit' => $file->canEdit(),
1097
            'canDelete' => $file->canArchive(),
1098
        );
1099
1100
        /** @var Member $owner */
1101
        $owner = $file->Owner();
1102
1103
        if ($owner) {
1104
            $object['owner'] = array(
1105
                'id' => $owner->ID,
1106
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1107
            );
1108
        }
1109
1110
        /** @var Folder $parent */
1111
        $parent = $file->Parent();
1112
1113
        if ($parent) {
1114
            $object['parent'] = array(
1115
                'id' => $parent->ID,
1116
                'title' => $parent->Title,
1117
                'filename' => $parent->Filename,
1118
            );
1119
        }
1120
1121
        /** @var File $file */
1122
        if ($file->getIsImage()) {
1123
            // Small thumbnail
1124
            $smallWidth = UploadField::config()->get('thumbnail_width');
1125
            $smallHeight = UploadField::config()->get('thumbnail_height');
1126
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1127
            if ($smallThumbnail && $smallThumbnail->exists()) {
1128
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1129
            }
1130
1131
            // Large thumbnail
1132
            $width = $this->config()->get('thumbnail_width');
1133
            $height = $this->config()->get('thumbnail_height');
1134
            $thumbnail = $file->FitMax($width, $height);
1135
            if ($thumbnail && $thumbnail->exists()) {
1136
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1137
            }
1138
            $object['dimensions']['width'] = $file->Width;
1139
            $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...
1140
        } else {
1141
            $object['thumbnail'] = $file->PreviewLink();
1142
        }
1143
1144
        return $object;
1145
    }
1146
1147
    /**
1148
     * Returns the files and subfolders contained in the currently selected folder,
1149
     * defaulting to the root node. Doubles as search results, if any search parameters
1150
     * are set through {@link SearchForm()}.
1151
     *
1152
     * @param array $params Unsanitised request parameters
1153
     * @return DataList
1154
     */
1155
    protected function getList($params = array())
1156
    {
1157
        $context = $this->getSearchContext();
1158
1159
        // Overwrite name filter to search both Name and Title attributes
1160
        $context->removeFilterByName('Name');
1161
1162
        // Lazy loaded list. Allows adding new filters through SearchContext.
1163
        /** @var DataList $list */
1164
        $list = $context->getResults($params);
1165
1166
        // Re-add previously removed "Name" filter as combined filter
1167
        // TODO Replace with composite SearchFilter once that API exists
1168
        if (!empty($params['Name'])) {
1169
            $list = $list->filterAny(array(
1170
                'Name:PartialMatch' => $params['Name'],
1171
                'Title:PartialMatch' => $params['Name']
1172
            ));
1173
        }
1174
1175
        // Optionally limit search to a folder (non-recursive)
1176
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
1177
            $list = $list->filter('ParentID', $params['ParentID']);
1178
        }
1179
1180
        // Date filtering
1181 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...
1182
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
1183
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
1184
        }
1185 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...
1186
            $toDate = new DateField(null, null, $params['CreatedTo']);
1187
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
1188
        }
1189
1190
        // Categories
1191
        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...
1192
            $extensions = File::config()->app_categories[$filters['AppCategory']];
1193
            $list = $list->filter('Name:PartialMatch', $extensions);
1194
        }
1195
1196
        // Sort folders first
1197
        $list = $list->sort(
1198
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
1199
        );
1200
1201
        // Pagination
1202
        if (isset($filters['page']) && isset($filters['limit'])) {
1203
            $page = $filters['page'];
1204
            $limit = $filters['limit'];
1205
            $offset = ($page - 1) * $limit;
1206
            $list = $list->limit($limit, $offset);
1207
        }
1208
1209
        // Access checks
1210
        $list = $list->filterByCallback(function (File $file) {
1211
            return $file->canView();
1212
        });
1213
1214
        return $list;
1215
    }
1216
1217
    /**
1218
     * Action handler for adding pages to a campaign
1219
     *
1220
     * @param array $data
1221
     * @param Form $form
1222
     * @return DBHTMLText|HTTPResponse
1223
     */
1224
    public function addtocampaign($data, $form)
1225
    {
1226
        $id = $data['ID'];
1227
        $record = $this->getList()->byID($id);
1228
1229
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1230
        $results = $handler->addToCampaign($record, $data['Campaign']);
1231
        if (!isset($results)) {
1232
            return null;
1233
        }
1234
1235
        // Send extra "message" data with schema response
1236
        $extraData = ['message' => $results];
1237
        $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1238
        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...
1239
    }
1240
1241
    /**
1242
     * Url handler for add to campaign form
1243
     *
1244
     * @param HTTPRequest $request
1245
     * @return Form
1246
     */
1247
    public function addToCampaignForm($request)
1248
    {
1249
        // Get ID either from posted back value, or url parameter
1250
        $id = $request->param('ID') ?: $request->postVar('ID');
1251
        return $this->getAddToCampaignForm($id);
1252
    }
1253
1254
    /**
1255
     * @param int $id
1256
     * @return Form
1257
     */
1258
    public function getAddToCampaignForm($id)
1259
    {
1260
        // Get record-specific fields
1261
        $record = $this->getList()->byID($id);
1262
1263
        if (!$record) {
1264
            $this->httpError(404, _t(
1265
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1266
                'That {Type} couldn\'t be found',
1267
                '',
1268
                ['Type' => File::singleton()->i18n_singular_name()]
1269
            ));
1270
            return null;
1271
        }
1272 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...
1273
            $this->httpError(403, _t(
1274
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1275
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1276
                '',
1277
                ['ObjectTitle' => $record->i18n_singular_name()]
1278
            ));
1279
            return null;
1280
        }
1281
1282
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1283
        $form = $handler->Form($record);
1284
1285 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...
1286
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1287
            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...
1288
        });
1289
1290
        return $form;
1291
    }
1292
1293
    /**
1294
     * @return Upload
1295
     */
1296
    protected function getUpload()
1297
    {
1298
        $upload = Upload::create();
1299
        $upload->getValidator()->setAllowedExtensions(
1300
            // filter out '' since this would be a regex problem on JS end
1301
            array_filter(File::config()->get('allowed_extensions'))
1302
        );
1303
1304
        return $upload;
1305
    }
1306
1307
    /**
1308
     * Get response for successfully updated record
1309
     *
1310
     * @param File $record
1311
     * @param Form $form
1312
     * @return HTTPResponse
1313
     */
1314
    protected function getRecordUpdatedResponse($record, $form)
1315
    {
1316
        // Return the record data in the same response as the schema to save a postback
1317
        $schemaData = ['record' => $this->getObjectFromData($record)];
1318
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1319
        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...
1320
    }
1321
}
1322