Completed
Pull Request — master (#284)
by
unknown
02:00
created

AssetAdmin::getSearchContext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 52
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 52
rs 9.4929
cc 1
eloc 36
nc 1
nop 0

How to fix   Long Method   

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 SilverStripe\Admin\AddToCampaignHandler;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\LeftAndMain;
8
use SilverStripe\Assets\File;
9
use SilverStripe\Assets\Folder;
10
use SilverStripe\Assets\Storage\AssetNameGenerator;
11
use SilverStripe\Assets\Upload;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\HTTPRequest;
14
use SilverStripe\Control\HTTPResponse;
15
use SilverStripe\Core\Config\Config;
16
use SilverStripe\Core\Convert;
17
use SilverStripe\Core\Injector\Injector;
18
use SilverStripe\Forms\CheckboxField;
19
use SilverStripe\Forms\DateField;
20
use SilverStripe\Forms\DropdownField;
21
use SilverStripe\Forms\FieldGroup;
22
use SilverStripe\Forms\FieldList;
23
use SilverStripe\Forms\Form;
24
use SilverStripe\Forms\FormAction;
25
use SilverStripe\Forms\HeaderField;
26
use SilverStripe\Forms\PopoverField;
27
use SilverStripe\ORM\ArrayList;
28
use SilverStripe\ORM\DataList;
29
use SilverStripe\ORM\DataObject;
30
use SilverStripe\ORM\FieldType\DBHTMLText;
31
use SilverStripe\ORM\Search\SearchContext;
32
use SilverStripe\Security\Member;
33
use SilverStripe\Security\PermissionProvider;
34
use SilverStripe\Security\SecurityToken;
35
use SilverStripe\View\Requirements;
36
37
/**
38
 * AssetAdmin is the 'file store' section of the CMS.
39
 * It provides an interface for manipulating the File and Folder objects in the system.
40
 */
41
class AssetAdmin extends LeftAndMain implements PermissionProvider
42
{
43
    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...
44
45
    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...
46
47
    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...
48
49
    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...
50
51
    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...
52
        // Legacy redirect for SS3-style detail view
53
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
54
        // Pass all URLs to the index, for React to unpack
55
        'show/$FolderID/edit/$FileID' => 'index',
56
        // API access points with structured data
57
        'POST api/createFolder' => 'apiCreateFolder',
58
        'POST api/createFile' => 'apiCreateFile',
59
        'GET api/readFolder' => 'apiReadFolder',
60
        'PUT api/updateFolder' => 'apiUpdateFolder',
61
        'DELETE api/delete' => 'apiDelete',
62
        'GET api/search' => 'apiSearch',
63
    ];
64
65
    /**
66
     * Amount of results showing on a single page.
67
     *
68
     * @config
69
     * @var int
70
     */
71
    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...
72
73
    /**
74
     * @config
75
     * @see Upload->allowedMaxFileSize
76
     * @var int
77
     */
78
    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...
79
80
    /**
81
     * @var array
82
     */
83
    private static $allowed_actions = array(
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
84
        'legacyRedirectForEditView',
85
        'apiCreateFolder',
86
        'apiCreateFile',
87
        'apiReadFolder',
88
        'apiUpdateFolder',
89
        'apiDelete',
90
        'apiSearch',
91
        'FileEditForm',
92
        'AddToCampaignForm',
93
    );
94
95
    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...
96
97
    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...
98
99
    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...
100
101
    /**
102
     * Set up the controller
103
     */
104
    public function init()
105
    {
106
        parent::init();
107
108
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
109
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
110
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
111
112
        CMSBatchActionHandler::register(
113
            'delete',
114
            'SilverStripe\AssetAdmin\BatchAction\DeleteAssets',
115
            'SilverStripe\\Assets\\Folder'
116
        );
117
    }
118
119
    public function getClientConfig()
120
    {
121
        $baseLink = $this->Link();
122
        return array_merge( parent::getClientConfig(), [
123
            'reactRouter' => true,
124
            'createFileEndpoint' => [
125
                'url' => Controller::join_links($baseLink, 'api/createFile'),
126
                'method' => 'post',
127
                'payloadFormat' => 'urlencoded',
128
            ],
129
            'createFolderEndpoint' => [
130
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
131
                'method' => 'post',
132
                'payloadFormat' => 'urlencoded',
133
            ],
134
            'readFolderEndpoint' => [
135
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
136
                'method' => 'get',
137
                'responseFormat' => 'json',
138
            ],
139
            'searchEndpoint' => [
140
                'url' => Controller::join_links($baseLink, 'api/search'),
141
                'method' => 'get',
142
                'responseFormat' => 'json',
143
            ],
144
            'updateFolderEndpoint' => [
145
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
146
                'method' => 'put',
147
                'payloadFormat' => 'urlencoded',
148
            ],
149
            'deleteEndpoint' => [
150
                'url' => Controller::join_links($baseLink, 'api/delete'),
151
                'method' => 'delete',
152
                'payloadFormat' => 'urlencoded',
153
            ],
154
            'limit' => $this->config()->page_length,
155
            'form' => [
156
                'FileEditForm' => [
157
                    'schemaUrl' => $this->Link('schema/FileEditForm')
158
                ],
159
                'AddToCampaignForm' => [
160
                    'schemaUrl' => $this->Link('schema/AddToCampaignForm')
161
                ],
162
            ],
163
        ]);
164
    }
165
166
    /**
167
     * Fetches a collection of files by ParentID.
168
     *
169
     * @param HTTPRequest $request
170
     * @return HTTPResponse
171
     */
172
    public function apiReadFolder(HTTPRequest $request)
173
    {
174
        $params = $request->requestVars();
175
        $items = array();
176
        $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...
177
        $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...
178
179
        if (!isset($params['id']) && !strlen($params['id'])) {
180
            $this->httpError(400);
181
        }
182
183
        $folderID = (int)$params['id'];
184
        /** @var Folder $folder */
185
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
186
187
        if (!$folder) {
188
            $this->httpError(400);
189
        }
190
191
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
192
        $files = $this->getList()->filter('ParentID', $folderID);
193
194
        if ($files) {
195
            /** @var File $file */
196
            foreach ($files as $file) {
197
                if (!$file->canView()) {
198
                    continue;
199
                }
200
201
                $items[] = $this->getObjectFromData($file);
202
            }
203
        }
204
205
        // Build parents (for breadcrumbs)
206
        $parents = [];
207
        $next = $folder->Parent();
208
        while($next && $next->exists()) {
209
            array_unshift($parents, [
210
                'id' => $next->ID,
211
                'title' => $next->getTitle(),
212
                'filename' => $next->getFilename(),
213
            ]);
214
            if($next->ParentID) {
215
                $next = $next->Parent();
216
            } else {
217
                break;
218
            }
219
        }
220
221
        // Build response
222
        $response = new HTTPResponse();
223
        $response->addHeader('Content-Type', 'application/json');
224
        $response->setBody(json_encode([
225
            'files' => $items,
226
            'title' => $folder->getTitle(),
227
            'count' => count($items),
228
            'parents' => $parents,
229
            'parent' => $parents ? $parents[count($parents) - 1] : null,
230
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
231
            'folderID' => $folderID,
232
            'canEdit' => $folder->canEdit(),
233
            'canDelete' => $folder->canDelete(),
234
        ]));
235
236
        return $response;
237
    }
238
239
    /**
240
     * @param HTTPRequest $request
241
     *
242
     * @return HTTPResponse
243
     */
244
    public function apiSearch(HTTPRequest $request)
245
    {
246
        $params = $request->getVars();
247
        $list = $this->getList($params);
248
249
        $response = new HTTPResponse();
250
        $response->addHeader('Content-Type', 'application/json');
251
        $response->setBody(json_encode([
252
            // Serialisation
253
            "files" => array_map(function($file) {
254
                return $this->getObjectFromData($file);
255
            }, $list->toArray()),
256
            "count" => $list->count(),
257
        ]));
258
259
        return $response;
260
    }
261
262
    /**
263
     * @param HTTPRequest $request
264
     *
265
     * @return HTTPResponse
266
     */
267
    public function apiDelete(HTTPRequest $request)
268
    {
269
        parse_str($request->getBody(), $vars);
270
271
        // CSRF check
272
        $token = SecurityToken::inst();
273 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...
274
            return new HTTPResponse(null, 400);
275
        }
276
277 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...
278
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
279
                ->addHeader('Content-Type', 'application/json');
280
        }
281
282
        $fileIds = $vars['ids'];
283
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
284
285 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...
286
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
287
                ->addHeader('Content-Type', 'application/json');
288
        }
289
290
        if (!min(array_map(function (File $file) {
291
            return $file->canDelete();
292
        }, $files))) {
293
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
294
                ->addHeader('Content-Type', 'application/json');
295
        }
296
297
        /** @var File $file */
298
        foreach ($files as $file) {
299
            $file->delete();
300
        }
301
302
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
303
            ->addHeader('Content-Type', 'application/json');
304
    }
305
306
    /**
307
     * Creates a single file based on a form-urlencoded upload.
308
     *
309
     * @param HTTPRequest $request
310
     * @return HTTPRequest|HTTPResponse
311
     */
312
    public function apiCreateFile(HTTPRequest $request)
313
    {
314
        $data = $request->postVars();
315
        $upload = $this->getUpload();
316
317
        // CSRF check
318
        $token = SecurityToken::inst();
319 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...
320
            return new HTTPResponse(null, 400);
321
        }
322
323
        // Check parent record
324
        /** @var Folder $parentRecord */
325
        $parentRecord = null;
326
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
327
            $parentRecord = Folder::get()->byID($data['ParentID']);
328
        }
329
        $data['Parent'] = $parentRecord;
330
331
        $tmpFile = $request->postVar('Upload');
332
        if(!$upload->validate($tmpFile)) {
333
            $result = ['error' => $upload->getErrors()];
334
            return (new HTTPResponse(json_encode($result), 400))
335
                ->addHeader('Content-Type', 'application/json');
336
        }
337
338
        // TODO Allow batch uploads
339
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
340
        /** @var File $file */
341
        $file = Injector::inst()->create($fileClass);
342
343
        // check canCreate permissions
344 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...
345
            return (new HTTPResponse(json_encode(['status' => 'error']), 403))
346
                ->addHeader('Content-Type', 'application/json');
347
        }
348
349
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
350 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...
351
            $result = ['error' => 'unknown'];
352
            return (new HTTPResponse(json_encode($result), 400))
353
                ->addHeader('Content-Type', 'application/json');
354
        }
355
356
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
357
        $file->write();
358
359
        $result = [$this->getObjectFromData($file)];
360
361
        return (new HTTPResponse(json_encode($result)))
362
            ->addHeader('Content-Type', 'application/json');
363
    }
364
365
    /**
366
     * Creates a single folder, within an optional parent folder.
367
     *
368
     * @param HTTPRequest $request
369
     * @return HTTPRequest|HTTPResponse
370
     */
371
    public function apiCreateFolder(HTTPRequest $request)
372
    {
373
        $data = $request->postVars();
374
375
        $class = 'SilverStripe\\Assets\\Folder';
376
377
        // CSRF check
378
        $token = SecurityToken::inst();
379 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...
380
            return new HTTPResponse(null, 400);
381
        }
382
383
        // check addchildren permissions
384
        /** @var Folder $parentRecord */
385
        $parentRecord = null;
386
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
387
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
388
        }
389
        $data['Parent'] = $parentRecord;
390
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
391
392
        // Build filename
393
        $baseFilename = isset($data['Name'])
394
            ? basename($data['Name'])
395
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
396
397
        if ($parentRecord && $parentRecord->ID) {
398
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
399
        }
400
401
        // Ensure name is unique
402
        $nameGenerator = $this->getNameGenerator($baseFilename);
403
        $filename = null;
404
        foreach ($nameGenerator as $filename) {
405
            if (! File::find($filename)) {
406
                break;
407
            }
408
        }
409
        $data['Name'] = basename($filename);
410
411
        // Create record
412
        /** @var Folder $record */
413
        $record = Injector::inst()->create($class);
414
415
        // check create permissions
416
        if (!$record->canCreate(null, $data)) {
417
            return (new HTTPResponse(null, 403))
418
                ->addHeader('Content-Type', 'application/json');
419
        }
420
421
        $record->ParentID = $data['ParentID'];
422
        $record->Name = $record->Title = basename($data['Name']);
423
        $record->write();
424
425
        $result = $this->getObjectFromData($record);
426
427
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
428
    }
429
430
    /**
431
     * Redirects 3.x style detail links to new 4.x style routing.
432
     *
433
     * @param HTTPRequest $request
434
     */
435
    public function legacyRedirectForEditView($request)
436
    {
437
        $fileID = $request->param('FileID');
438
        /** @var File $file */
439
        $file = File::get()->byID($fileID);
440
        $link = $this->getFileEditLink($file) ?: $this->Link();
441
        $this->redirect($link);
442
    }
443
444
    /**
445
     * Given a file return the CMS link to edit it
446
     *
447
     * @param File $file
448
     * @return string
449
     */
450
    public function getFileEditLink($file) {
451
        if(!$file || !$file->isInDB()) {
452
            return null;
453
        }
454
455
        return Controller::join_links(
456
            $this->Link('show'),
457
            $file->ParentID,
458
            'edit',
459
            $file->ID
460
        );
461
    }
462
463
    /**
464
     * Get the search context from {@link File}, used to create the search form
465
     * as well as power the /search API endpoint.
466
     *
467
     * @return SearchContext
468
     */
469
    public function getSearchContext()
470
    {
471
        $context = File::singleton()->getDefaultSearchContext();
472
473
        // Customize fields
474
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
475
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
476
        ->setConfig('showcalendar', true);
477
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
478
        ->setConfig('showcalendar', true);
479
        $dateGroup = FieldGroup::create(
480
            $dateHeader,
481
            $dateFrom,
482
            $dateTo
483
        );
484
        $context->addField($dateGroup);
485
        /** @skipUpgrade */
486
        $appCategories = array(
487
            'archive' => _t(
488
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
489
                'Archive'
490
            ),
491
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
492
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
493
            'flash' => _t(
494
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash', 'Flash',
495
                'The fileformat'
496
            ),
497
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
498
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
499
        );
500
        $context->addField(
501
            $typeDropdown = new DropdownField(
502
                'AppCategory',
503
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
504
                $appCategories
505
            )
506
        );
507
508
        $typeDropdown->setEmptyString(' ');
509
510
        $currentfolderLabel = _t(
511
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
512
            'Limit to current folder?'
513
        );
514
        $context->addField(
515
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
516
        );
517
        $context->getFields()->removeByName('Title');
518
519
        return $context;
520
    }
521
522
    /**
523
     * Get an asset renamer for the given filename.
524
     *
525
     * @param  string             $filename Path name
526
     * @return AssetNameGenerator
527
     */
528
    protected function getNameGenerator($filename)
529
    {
530
        return Injector::inst()
531
            ->createWithArgs('AssetNameGenerator', array($filename));
532
    }
533
534
    /**
535
     * @todo Implement on client
536
     *
537
     * @param bool $unlinked
538
     * @return ArrayList
539
     */
540
    public function breadcrumbs($unlinked = false)
541
    {
542
        return null;
543
    }
544
545
546
    /**
547
     * Don't include class namespace in auto-generated CSS class
548
     */
549
    public function baseCSSClasses()
550
    {
551
        return 'AssetAdmin LeftAndMain';
552
    }
553
554
    public function providePermissions()
555
    {
556
        return array(
557
            "CMS_ACCESS_AssetAdmin" => array(
558
                '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...
559
                    'title' => static::menu_title()
560
                )),
561
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
562
            )
563
        );
564
    }
565
566
    /**
567
     * The form is used to generate a form schema,
568
     * as well as an intermediary object to process data through API endpoints.
569
     * Since it's used directly on API endpoints, it does not have any form actions.
570
     * It handles both {@link File} and {@link Folder} records.
571
     *
572
     * @param int $id
573
     * @return Form
574
     */
575
    public function getFileEditForm($id)
576
    {
577
        /** @var File $file */
578
        $file = $this->getList()->byID($id);
579
580 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...
581
            $this->httpError(403, _t(
582
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
583
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
584
                '',
585
                ['ObjectTitle' => $file->i18n_singular_name()]
0 ignored issues
show
Documentation introduced by
array('ObjectTitle' => $...->i18n_singular_name()) is of type array<string,?,{"ObjectTitle":"?"}>, 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...
586
            ));
587
            return null;
588
        }
589
590
        $fields = $file->getCMSFields();
591
592
        $actions = FieldList::create([
593
            FormAction::create('save', _t('CMSMain.SAVE', 'Save'))
594
                ->setIcon('save')
595
        ]);
596
597
        // Delete action
598
        $actions->push(
599
            FormAction::create(
600
                'delete',
601
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.DELETE_BUTTON', 'Delete')
602
            )
603
                ->setIcon('trash-bin')
604
        );
605
606
        // Add to campaign action
607
        if (!$file instanceof Folder) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
608
            $actions->push(PopoverField::create([
609
                FormAction::create(
610
                    'addtocampaign',
611
                    _t(
612
                        'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ADDTOCAMPAIGN',
613
                        'Add to campaign'
614
                    )
615
                ),
616
            ])
617
                ->setPlacement('top')
618
            );
619
        }
620
621
        $form = Form::create(
622
            $this,
623
            'FileEditForm',
624
            $fields,
625
            $actions
626
        );
627
628
        // Load into form
629
        if($id && $file) {
630
            $form->loadDataFrom($file);
631
        }
632
633
        // Configure form to respond to validation errors with form schema
634
        // if requested via react.
635
        $form->setValidationResponseCallback(function() use ($form) {
636
            return $this->getSchemaResponse($form);
637
        });
638
639
        return $form;
640
    }
641
642
    /**
643
     * Get file edit form
644
     *
645
     * @return Form
646
     */
647
    public function FileEditForm()
648
    {
649
        // Get ID either from posted back value, or url parameter
650
        $request = $this->getRequest();
651
        $id = $request->param('ID') ?: $request->postVar('ID');
652
        return $this->getFileEditForm($id);
653
    }
654
655
    /**
656
     * @param array $data
657
     * @param Form $form
658
     * @return HTTPResponse
659
     */
660
    public function save($data, $form)
661
    {
662 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...
663
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
664
                ->addHeader('Content-Type', 'application/json');
665
        }
666
667
        $id = (int) $data['ID'];
668
        $record = $this->getList()->filter('ID', $id)->first();
669
670 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...
671
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
672
                ->addHeader('Content-Type', 'application/json');
673
        }
674
675 View Code Duplication
        if (!$record->canEdit()) {
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...
676
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
677
                ->addHeader('Content-Type', 'application/json');
678
        }
679
680
        $form->saveInto($record);
681
        $record->write();
682
683
        // Return the record data in the same response as the schema to save a postback
684
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
685
        $schemaData['record'] = $this->getObjectFromData($record);
686
        $response = new HTTPResponse(Convert::raw2json($schemaData));
687
        $response->addHeader('Content-Type', 'application/json');
688
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $response; (SilverStripe\Control\HTTPResponse) is incompatible with the return type of the parent method SilverStripe\Admin\LeftAndMain::save of type SS_HTTPResponse|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
689
    }
690
691
    /**
692
     * @param File $file
693
     *
694
     * @return array
695
     */
696
    protected function getObjectFromData(File $file)
697
    {
698
        $object = array(
699
            'id' => $file->ID,
700
            'created' => $file->Created,
701
            'lastUpdated' => $file->LastEdited,
702
            'owner' => null,
703
            'parent' => null,
704
            'title' => $file->Title,
705
            'exists' => $file->exists(), // Broken file check
706
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
707
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
708
            'name' => $file->Name,
709
            'filename' => $file->Filename,
710
            'extension' => $file->Extension,
711
            'size' => $file->Size,
712
            'url' => $file->AbsoluteURL,
713
            'published' => $file->isPublished(),
714
            'modified' => $file->isModifiedOnDraft(),
715
            'draft' => $file->isOnDraftOnly(),
716
            'canEdit' => $file->canEdit(),
717
            'canDelete' => $file->canDelete(),
718
        );
719
720
        /** @var Member $owner */
721
        $owner = $file->Owner();
722
723
        if ($owner) {
724
            $object['owner'] = array(
725
                'id' => $owner->ID,
726
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
727
            );
728
        }
729
730
        /** @var Folder $parent */
731
        $parent = $file->Parent();
732
733
        if ($parent) {
734
            $object['parent'] = array(
735
                'id' => $parent->ID,
736
                'title' => $parent->Title,
737
                'filename' => $parent->Filename,
738
            );
739
        }
740
741
        /** @var File $file */
742
        if ($file->getIsImage()) {
743
            $width = (int)Config::inst()->get(self::class, 'thumbnail_width');
744
            $height = (int)Config::inst()->get(self::class, 'thumbnail_height');
745
746
            $thumbnail = $file->FitMax($width, $height);
747
            if ($thumbnail && $thumbnail->exists()) {
748
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
749
            }
750
            $object['dimensions']['width'] = $file->Width;
751
            $object['dimensions']['height'] = $file->Height;
752
        }
753
754
        return $object;
755
    }
756
757
758
    /**
759
     * Returns the files and subfolders contained in the currently selected folder,
760
     * defaulting to the root node. Doubles as search results, if any search parameters
761
     * are set through {@link SearchForm()}.
762
     *
763
     * @param array $params Unsanitised request parameters
764
     * @return DataList
765
     */
766
    protected function getList($params = array())
767
    {
768
        $context = $this->getSearchContext();
769
770
        // Overwrite name filter to search both Name and Title attributes
771
        $context->removeFilterByName('Name');
772
773
        // Lazy loaded list. Allows adding new filters through SearchContext.
774
        /** @var DataList $list */
775
        $list = $context->getResults($params);
776
777
        // Re-add previously removed "Name" filter as combined filter
778
        // TODO Replace with composite SearchFilter once that API exists
779
        if(!empty($params['Name'])) {
780
            $list = $list->filterAny(array(
781
                'Name:PartialMatch' => $params['Name'],
782
                'Title:PartialMatch' => $params['Name']
783
            ));
784
        }
785
786
        // Optionally limit search to a folder (non-recursive)
787
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
788
            $list = $list->filter('ParentID', $params['ParentID']);
789
        }
790
791
        // Date filtering
792 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...
793
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
794
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
795
        }
796 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...
797
            $toDate = new DateField(null, null, $params['CreatedTo']);
798
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
799
        }
800
801
        // Categories
802
        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...
803
            $extensions = File::config()->app_categories[$filters['AppCategory']];
804
            $list = $list->filter('Name:PartialMatch', $extensions);
805
        }
806
807
        // Sort folders first
808
        $list = $list->sort(
809
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
810
        );
811
812
        // Pagination
813
        if (isset($filters['page']) && isset($filters['limit'])) {
814
            $page = $filters['page'];
815
            $limit = $filters['limit'];
816
            $offset = ($page - 1) * $limit;
817
            $list = $list->limit($limit, $offset);
818
        }
819
820
        // Access checks
821
        $list = $list->filterByCallback(function(File $file) {
822
            return $file->canView();
823
        });
824
825
        return $list;
826
    }
827
828
    /**
829
     * Action handler for adding pages to a campaign
830
     *
831
     * @param array $data
832
     * @param Form $form
833
     * @return DBHTMLText|HTTPResponse
834
     */
835
    public function addtocampaign($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
836
    {
837
        $id = $data['ID'];
838
        $record = $this->getList()->byID($id);
839
840
        $handler = AddToCampaignHandler::create($this, $record);
841
        $results = $handler->addToCampaign($record, $data['Campaign']);
842
        if (!is_null($results)) {
843
            $request = $this->getRequest();
844
            if($request->getHeader('X-Formschema-Request')) {
845
                $data = $this->getSchemaForForm($handler->Form($record));
846
                $data['message'] = $results;
847
848
                $response = new HTTPResponse(Convert::raw2json($data));
849
                $response->addHeader('Content-Type', 'application/json');
850
                return $response;
851
            }
852
            return $results;
853
        }
854
    }
855
856
    /**
857
     * Url handler for add to campaign form
858
     *
859
     * @param HTTPRequest $request
860
     * @return Form
861
     */
862
    public function AddToCampaignForm($request)
863
    {
864
        // Get ID either from posted back value, or url parameter
865
        $id = $request->param('ID') ?: $request->postVar('ID');
866
        return $this->getAddToCampaignForm($id);
867
    }
868
869
    /**
870
     * @param int $id
871
     * @return Form
872
     */
873
    public function getAddToCampaignForm($id)
874
    {
875
        // Get record-specific fields
876
        $record = $this->getList()->byID($id);
877
878
        if (!$record) {
879
            $this->httpError(404, _t(
880
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
881
                'That {Type} couldn\'t be found',
882
                '',
883
                ['Type' => File::singleton()->i18n_singular_name()]
0 ignored issues
show
Documentation introduced by
array('Type' => \SilverS...->i18n_singular_name()) is of type array<string,?,{"Type":"?"}>, 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...
884
            ));
885
            return null;
886
        }
887 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...
888
            $this->httpError(403, _t(
889
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
890
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
891
                '',
892
                ['ObjectTitle' => $record->i18n_singular_name()]
0 ignored issues
show
Documentation introduced by
array('ObjectTitle' => $...->i18n_singular_name()) is of type array<string,?,{"ObjectTitle":"?"}>, 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...
893
            ));
894
            return null;
895
        }
896
897
        $handler = AddToCampaignHandler::create($this, $record);
898
        return $handler->Form($record);
899
    }
900
901
    /**
902
     * @return Upload
903
     */
904
    protected function getUpload()
905
    {
906
        $upload = Upload::create();
907
        $upload->getValidator()->setAllowedExtensions(
908
            // filter out '' since this would be a regex problem on JS end
909
            array_filter(File::config()->get('allowed_extensions'))
910
        );
911
912
        return $upload;
913
    }
914
}
915