Completed
Push — master ( b77e8f...d6f753 )
by Damian
26:54
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 View Code Duplication
        if(!$upload->validate($tmpFile)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
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()]
586
            ));
587
            return null;
588
        }
589
590
        $fields = $file->getCMSFields();
591
592
        $actions = $this->getFileEditActions($file);
593
594
        $form = Form::create(
595
            $this,
596
            'FileEditForm',
597
            $fields,
598
            $actions
599
        );
600
601
        // Load into form
602
        if($id && $file) {
603
            $form->loadDataFrom($file);
604
        }
605
606
        // Configure form to respond to validation errors with form schema
607
        // if requested via react.
608
        $form->setValidationResponseCallback(function() use ($form) {
609
            return $this->getSchemaResponse($form);
610
        });
611
612
        return $form;
613
    }
614
615
    /**
616
     * Get file edit form
617
     *
618
     * @return Form
619
     */
620
    public function FileEditForm()
621
    {
622
        // Get ID either from posted back value, or url parameter
623
        $request = $this->getRequest();
624
        $id = $request->param('ID') ?: $request->postVar('ID');
625
        return $this->getFileEditForm($id);
626
    }
627
628
    /**
629
     * @param array $data
630
     * @param Form $form
631
     * @return HTTPResponse
632
     */
633
    public function save($data, $form)
634
    {
635
        return $this->saveOrPublish($data, $form, false);
636
    }
637
638
639
    /**
640
     * @param array $data
641
     * @param Form $form
642
     * @return HTTPResponse
643
     */
644
    public function publish($data, $form) {
645
        return $this->saveOrPublish($data, $form, true);
646
    }
647
648
    /**
649
     * Update thisrecord
650
     *
651
     * @param array $data
652
     * @param Form $form
653
     * @param bool $doPublish
654
     * @return HTTPResponse
655
     */
656
    protected function saveOrPublish($data, $form, $doPublish = false) {
657 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...
658
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
659
                ->addHeader('Content-Type', 'application/json');
660
        }
661
662
        $id = (int) $data['ID'];
663
        /** @var File $record */
664
        $record = $this->getList()->filter('ID', $id)->first();
665
666 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...
667
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
668
                ->addHeader('Content-Type', 'application/json');
669
        }
670
671 View Code Duplication
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
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...
672
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
673
                ->addHeader('Content-Type', 'application/json');
674
        }
675
676
        $form->saveInto($record);
677
        $record->write();
678
679
        // Publish this record and owned objects
680
        if ($doPublish) {
681
            $record->publishRecursive();
682
        }
683
684
        // Return the record data in the same response as the schema to save a postback
685
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
0 ignored issues
show
Bug introduced by
It seems like $this->getFileEditForm($id) can be null; however, getSchemaForForm() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
686
        $schemaData['record'] = $this->getObjectFromData($record);
687
        $response = new HTTPResponse(Convert::raw2json($schemaData));
688
        $response->addHeader('Content-Type', 'application/json');
689
        return $response;
690
    }
691
692
    public function unpublish($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...
693 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...
694
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
695
                ->addHeader('Content-Type', 'application/json');
696
        }
697
698
        $id = (int) $data['ID'];
699
        /** @var File $record */
700
        $record = $this->getList()->filter('ID', $id)->first();
701
702
        if (!$record) {
703
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
704
                ->addHeader('Content-Type', 'application/json');
705
        }
706
707
        if (!$record->canUnpublish()) {
708
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
709
                ->addHeader('Content-Type', 'application/json');
710
        }
711
712
        $record->doUnpublish();
713
714
        // Return the record data in the same response as the schema to save a postback
715
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
0 ignored issues
show
Bug introduced by
It seems like $this->getFileEditForm($id) can be null; however, getSchemaForForm() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
716
        $schemaData['record'] = $this->getObjectFromData($record);
717
        $response = new HTTPResponse(Convert::raw2json($schemaData));
718
        $response->addHeader('Content-Type', 'application/json');
719
        return $response;
720
    }
721
722
    /**
723
     * @param File $file
724
     *
725
     * @return array
726
     */
727
    protected function getObjectFromData(File $file)
728
    {
729
        $object = array(
730
            'id' => $file->ID,
731
            'created' => $file->Created,
732
            'lastUpdated' => $file->LastEdited,
733
            'owner' => null,
734
            'parent' => null,
735
            'title' => $file->Title,
736
            'exists' => $file->exists(), // Broken file check
737
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
738
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
739
            'name' => $file->Name,
740
            'filename' => $file->Filename,
741
            '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...
742
            'size' => $file->Size,
743
            'url' => $file->AbsoluteURL,
744
            'published' => $file->isPublished(),
745
            'modified' => $file->isModifiedOnDraft(),
746
            'draft' => $file->isOnDraftOnly(),
747
            'canEdit' => $file->canEdit(),
748
            'canDelete' => $file->canDelete(),
749
        );
750
751
        /** @var Member $owner */
752
        $owner = $file->Owner();
753
754
        if ($owner) {
755
            $object['owner'] = array(
756
                'id' => $owner->ID,
757
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
758
            );
759
        }
760
761
        /** @var Folder $parent */
762
        $parent = $file->Parent();
763
764
        if ($parent) {
765
            $object['parent'] = array(
766
                'id' => $parent->ID,
767
                'title' => $parent->Title,
768
                'filename' => $parent->Filename,
769
            );
770
        }
771
772
        /** @var File $file */
773
        if ($file->getIsImage()) {
774
            $width = (int)Config::inst()->get(self::class, 'thumbnail_width');
775
            $height = (int)Config::inst()->get(self::class, 'thumbnail_height');
776
777
            $thumbnail = $file->FitMax($width, $height);
778
            if ($thumbnail && $thumbnail->exists()) {
779
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
780
            }
781
            $object['dimensions']['width'] = $file->Width;
782
            $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...
783
        }
784
785
        return $object;
786
    }
787
788
789
    /**
790
     * Returns the files and subfolders contained in the currently selected folder,
791
     * defaulting to the root node. Doubles as search results, if any search parameters
792
     * are set through {@link SearchForm()}.
793
     *
794
     * @param array $params Unsanitised request parameters
795
     * @return DataList
796
     */
797
    protected function getList($params = array())
798
    {
799
        $context = $this->getSearchContext();
800
801
        // Overwrite name filter to search both Name and Title attributes
802
        $context->removeFilterByName('Name');
803
804
        // Lazy loaded list. Allows adding new filters through SearchContext.
805
        /** @var DataList $list */
806
        $list = $context->getResults($params);
807
808
        // Re-add previously removed "Name" filter as combined filter
809
        // TODO Replace with composite SearchFilter once that API exists
810
        if(!empty($params['Name'])) {
811
            $list = $list->filterAny(array(
812
                'Name:PartialMatch' => $params['Name'],
813
                'Title:PartialMatch' => $params['Name']
814
            ));
815
        }
816
817
        // Optionally limit search to a folder (non-recursive)
818
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
819
            $list = $list->filter('ParentID', $params['ParentID']);
820
        }
821
822
        // Date filtering
823 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...
824
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
825
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
826
        }
827 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...
828
            $toDate = new DateField(null, null, $params['CreatedTo']);
829
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
830
        }
831
832
        // Categories
833
        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...
834
            $extensions = File::config()->app_categories[$filters['AppCategory']];
835
            $list = $list->filter('Name:PartialMatch', $extensions);
836
        }
837
838
        // Sort folders first
839
        $list = $list->sort(
840
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
841
        );
842
843
        // Pagination
844
        if (isset($filters['page']) && isset($filters['limit'])) {
845
            $page = $filters['page'];
846
            $limit = $filters['limit'];
847
            $offset = ($page - 1) * $limit;
848
            $list = $list->limit($limit, $offset);
849
        }
850
851
        // Access checks
852
        $list = $list->filterByCallback(function(File $file) {
853
            return $file->canView();
854
        });
855
856
        return $list;
857
    }
858
859
    /**
860
     * Action handler for adding pages to a campaign
861
     *
862
     * @param array $data
863
     * @param Form $form
864
     * @return DBHTMLText|HTTPResponse
865
     */
866
    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...
867
    {
868
        $id = $data['ID'];
869
        $record = $this->getList()->byID($id);
870
871
        $handler = AddToCampaignHandler::create($this, $record);
872
        $results = $handler->addToCampaign($record, $data['Campaign']);
873
        if (!is_null($results)) {
874
            $request = $this->getRequest();
875
            if($request->getHeader('X-Formschema-Request')) {
876
                $data = $this->getSchemaForForm($handler->Form($record));
877
                $data['message'] = $results;
878
879
                $response = new HTTPResponse(Convert::raw2json($data));
880
                $response->addHeader('Content-Type', 'application/json');
881
                return $response;
882
            }
883
            return $results;
884
        }
885
    }
886
887
    /**
888
     * Url handler for add to campaign form
889
     *
890
     * @param HTTPRequest $request
891
     * @return Form
892
     */
893
    public function AddToCampaignForm($request)
894
    {
895
        // Get ID either from posted back value, or url parameter
896
        $id = $request->param('ID') ?: $request->postVar('ID');
897
        return $this->getAddToCampaignForm($id);
898
    }
899
900
    /**
901
     * @param int $id
902
     * @return Form
903
     */
904
    public function getAddToCampaignForm($id)
905
    {
906
        // Get record-specific fields
907
        $record = $this->getList()->byID($id);
908
909
        if (!$record) {
910
            $this->httpError(404, _t(
911
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
912
                'That {Type} couldn\'t be found',
913
                '',
914
                ['Type' => File::singleton()->i18n_singular_name()]
915
            ));
916
            return null;
917
        }
918 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...
919
            $this->httpError(403, _t(
920
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
921
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
922
                '',
923
                ['ObjectTitle' => $record->i18n_singular_name()]
924
            ));
925
            return null;
926
        }
927
928
        $handler = AddToCampaignHandler::create($this, $record);
929
        return $handler->Form($record);
930
    }
931
932
    /**
933
     * @return Upload
934
     */
935
    protected function getUpload()
936
    {
937
        $upload = Upload::create();
938
        $upload->getValidator()->setAllowedExtensions(
939
            // filter out '' since this would be a regex problem on JS end
940
            array_filter(File::config()->get('allowed_extensions'))
941
        );
942
943
        return $upload;
944
    }
945
946
    /**
947
     * Get actions for file edit
948
     *
949
     * @param File $file
950
     * @return FieldList
951
     */
952
    protected function getFileEditActions($file)
953
    {
954
        $actions = FieldList::create();
955
956
        // Save and/or publish
957
        if ($file->canEdit()) {
958
            // Create save button
959
            $saveAction = FormAction::create('save', _t('CMSMain.SAVE', 'Save'));
960
            $saveAction->setIcon('save');
961
            $actions->push($saveAction);
962
963
            // Folders are automatically published
964
            if ($file->canPublish() && (!$file instanceof Folder)) {
965
                $publishText = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.PUBLISH_BUTTON', 'Publish');
966
                $publishAction = FormAction::create('publish', $publishText);
967
                $publishAction->setIcon('rocket');
968
                $publishAction->setSchemaData(['data' => ['buttonStyle' => 'primary']]);
969
                $actions->push($publishAction);
970
            }
971
        }
972
973
        // Delete action
974
        $deleteText = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.DELETE_BUTTON', 'Delete');
975
        $deleteAction = FormAction::create('delete', $deleteText);
976
        //$deleteAction->setSchemaData(['data' => ['buttonStyle' => 'danger']]);
0 ignored issues
show
Unused Code Comprehensibility introduced by
74% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
977
        $deleteAction->setIcon('trash-bin');
978
979
        // Add file-specific actions
980
        if (!$file instanceof Folder) {
981
            // Add to campaign action
982
            $addToCampaignAction = FormAction::create(
983
                'addtocampaign',
984
                _t(
985
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ADDTOCAMPAIGN',
986
                    'Add to campaign'
987
                )
988
            );
989
            $popoverActions = [
990
                $addToCampaignAction
991
            ];
992
            // Add unpublish if available
993
            if ($file->isPublished() && $file->canUnpublish()) {
994
                $unpublishText = _t(
995
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNPUBLISH_BUTTON',
996
                    'Unpublish'
997
                );
998
                $unpublishAction = FormAction::create('unpublish', $unpublishText);
999
                $unpublishAction->setIcon('cancel-circled');
1000
                $popoverActions[] = $unpublishAction;
1001
            }
1002
            // Delete
1003
            $popoverActions[] = $deleteAction;
1004
1005
            // Build popover menu
1006
            $popoverField = PopoverField::create($popoverActions);
1007
            $popoverField->setPlacement('top');
1008
            $actions->push($popoverField);
1009
        } else {
1010
            $actions->push($deleteAction);
1011
        }
1012
1013
        $this->extend('updateFileEditActions', $actions);
1014
        return $actions;
1015
    }
1016
}
1017