Completed
Pull Request — master (#288)
by Damian
02:08
created

AssetAdmin::getFileEditActions()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 64
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 64
rs 7.2058
cc 7
eloc 38
nc 9
nop 1

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\AssetAdmin\Forms\AssetFormBuilder;
9
use SilverStripe\AssetAdmin\Forms\FileFormBuilder;
10
use SilverStripe\AssetAdmin\Forms\FolderFormBuilder;
11
use SilverStripe\Forms\DefaultFormBuilder;
12
use SilverStripe\AssetAdmin\Forms\ImageFormBuilder;
13
use SilverStripe\Assets\File;
14
use SilverStripe\Assets\Folder;
15
use SilverStripe\Assets\Image;
16
use SilverStripe\Assets\Storage\AssetNameGenerator;
17
use SilverStripe\Assets\Upload;
18
use SilverStripe\Control\Controller;
19
use SilverStripe\Control\HTTPRequest;
20
use SilverStripe\Control\HTTPResponse;
21
use SilverStripe\Core\Config\Config;
22
use SilverStripe\Core\Convert;
23
use SilverStripe\Core\Injector\Injector;
24
use SilverStripe\Forms\CheckboxField;
25
use SilverStripe\Forms\DateField;
26
use SilverStripe\Forms\DropdownField;
27
use SilverStripe\Forms\FieldGroup;
28
use SilverStripe\Forms\Form;
29
use SilverStripe\Forms\FormBuilder;
30
use SilverStripe\Forms\HeaderField;
31
use SilverStripe\ORM\ArrayList;
32
use SilverStripe\ORM\DataList;
33
use SilverStripe\ORM\DataObject;
34
use SilverStripe\ORM\FieldType\DBHTMLText;
35
use SilverStripe\ORM\Search\SearchContext;
36
use SilverStripe\Security\Member;
37
use SilverStripe\Security\PermissionProvider;
38
use SilverStripe\Security\SecurityToken;
39
use SilverStripe\View\Requirements;
40
41
/**
42
 * AssetAdmin is the 'file store' section of the CMS.
43
 * It provides an interface for manipulating the File and Folder objects in the system.
44
 */
45
class AssetAdmin extends LeftAndMain implements PermissionProvider
46
{
47
    private static $url_segment = 'assets';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_segment is not used and could be removed.

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

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

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

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

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

Loading history...
52
53
    private static $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...
54
55
    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...
56
        // Legacy redirect for SS3-style detail view
57
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
58
        // Pass all URLs to the index, for React to unpack
59
        'show/$FolderID/edit/$FileID' => 'index',
60
        // API access points with structured data
61
        'POST api/createFolder' => 'apiCreateFolder',
62
        'POST api/createFile' => 'apiCreateFile',
63
        'GET api/readFolder' => 'apiReadFolder',
64
        'PUT api/updateFolder' => 'apiUpdateFolder',
65
        'DELETE api/delete' => 'apiDelete',
66
        'GET api/search' => 'apiSearch',
67
    ];
68
69
    /**
70
     * Amount of results showing on a single page.
71
     *
72
     * @config
73
     * @var int
74
     */
75
    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...
76
77
    /**
78
     * @config
79
     * @see Upload->allowedMaxFileSize
80
     * @var int
81
     */
82
    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...
83
84
    /**
85
     * @var array
86
     */
87
    private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

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

Loading history...
88
        'legacyRedirectForEditView',
89
        'apiCreateFolder',
90
        'apiCreateFile',
91
        'apiReadFolder',
92
        'apiUpdateFolder',
93
        'apiDelete',
94
        'apiSearch',
95
        'FileEditForm',
96
        'AddToCampaignForm',
97
    );
98
99
    private static $required_permission_codes = 'CMS_ACCESS_AssetAdmin';
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
100
101
    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...
102
103
    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...
104
105
    /**
106
     * Set up the controller
107
     */
108
    public function init()
109
    {
110
        parent::init();
111
112
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
113
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
114
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
115
116
        CMSBatchActionHandler::register(
117
            'delete',
118
            'SilverStripe\AssetAdmin\BatchAction\DeleteAssets',
119
            'SilverStripe\\Assets\\Folder'
120
        );
121
    }
122
123
    public function getClientConfig()
124
    {
125
        $baseLink = $this->Link();
126
        return array_merge(parent::getClientConfig(), [
127
            'reactRouter' => true,
128
            'createFileEndpoint' => [
129
                'url' => Controller::join_links($baseLink, 'api/createFile'),
130
                'method' => 'post',
131
                'payloadFormat' => 'urlencoded',
132
            ],
133
            'createFolderEndpoint' => [
134
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
135
                'method' => 'post',
136
                'payloadFormat' => 'urlencoded',
137
            ],
138
            'readFolderEndpoint' => [
139
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
140
                'method' => 'get',
141
                'responseFormat' => 'json',
142
            ],
143
            'searchEndpoint' => [
144
                'url' => Controller::join_links($baseLink, 'api/search'),
145
                'method' => 'get',
146
                'responseFormat' => 'json',
147
            ],
148
            'updateFolderEndpoint' => [
149
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
150
                'method' => 'put',
151
                'payloadFormat' => 'urlencoded',
152
            ],
153
            'deleteEndpoint' => [
154
                'url' => Controller::join_links($baseLink, 'api/delete'),
155
                'method' => 'delete',
156
                'payloadFormat' => 'urlencoded',
157
            ],
158
            'limit' => $this->config()->page_length,
159
            'form' => [
160
                'FileEditForm' => [
161
                    'schemaUrl' => $this->Link('schema/FileEditForm')
162
                ],
163
                'AddToCampaignForm' => [
164
                    'schemaUrl' => $this->Link('schema/AddToCampaignForm')
165
                ],
166
            ],
167
        ]);
168
    }
169
170
    /**
171
     * Fetches a collection of files by ParentID.
172
     *
173
     * @param HTTPRequest $request
174
     * @return HTTPResponse
175
     */
176
    public function apiReadFolder(HTTPRequest $request)
177
    {
178
        $params = $request->requestVars();
179
        $items = array();
180
        $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...
181
        $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...
182
183
        if (!isset($params['id']) && !strlen($params['id'])) {
184
            $this->httpError(400);
185
        }
186
187
        $folderID = (int)$params['id'];
188
        /** @var Folder $folder */
189
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
190
191
        if (!$folder) {
192
            $this->httpError(400);
193
        }
194
195
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
196
        $files = $this->getList()->filter('ParentID', $folderID);
197
198
        if ($files) {
199
            /** @var File $file */
200
            foreach ($files as $file) {
201
                if (!$file->canView()) {
202
                    continue;
203
                }
204
205
                $items[] = $this->getObjectFromData($file);
206
            }
207
        }
208
209
        // Build parents (for breadcrumbs)
210
        $parents = [];
211
        $next = $folder->Parent();
212
        while ($next && $next->exists()) {
213
            array_unshift($parents, [
214
                'id' => $next->ID,
215
                'title' => $next->getTitle(),
216
                'filename' => $next->getFilename(),
217
            ]);
218
            if ($next->ParentID) {
219
                $next = $next->Parent();
220
            } else {
221
                break;
222
            }
223
        }
224
225
        // Build response
226
        $response = new HTTPResponse();
227
        $response->addHeader('Content-Type', 'application/json');
228
        $response->setBody(json_encode([
229
            'files' => $items,
230
            'title' => $folder->getTitle(),
231
            'count' => count($items),
232
            'parents' => $parents,
233
            'parent' => $parents ? $parents[count($parents) - 1] : null,
234
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
235
            'folderID' => $folderID,
236
            'canEdit' => $folder->canEdit(),
237
            'canDelete' => $folder->canDelete(),
238
        ]));
239
240
        return $response;
241
    }
242
243
    /**
244
     * @param HTTPRequest $request
245
     *
246
     * @return HTTPResponse
247
     */
248
    public function apiSearch(HTTPRequest $request)
249
    {
250
        $params = $request->getVars();
251
        $list = $this->getList($params);
252
253
        $response = new HTTPResponse();
254
        $response->addHeader('Content-Type', 'application/json');
255
        $response->setBody(json_encode([
256
            // Serialisation
257
            "files" => array_map(function ($file) {
258
                return $this->getObjectFromData($file);
259
            }, $list->toArray()),
260
            "count" => $list->count(),
261
        ]));
262
263
        return $response;
264
    }
265
266
    /**
267
     * @param HTTPRequest $request
268
     *
269
     * @return HTTPResponse
270
     */
271
    public function apiDelete(HTTPRequest $request)
272
    {
273
        parse_str($request->getBody(), $vars);
274
275
        // CSRF check
276
        $token = SecurityToken::inst();
277 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...
278
            return new HTTPResponse(null, 400);
279
        }
280
281 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...
282
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
283
                ->addHeader('Content-Type', 'application/json');
284
        }
285
286
        $fileIds = $vars['ids'];
287
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
288
289 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...
290
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
291
                ->addHeader('Content-Type', 'application/json');
292
        }
293
294
        if (!min(array_map(function (File $file) {
295
            return $file->canDelete();
296
        }, $files))) {
297
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
298
                ->addHeader('Content-Type', 'application/json');
299
        }
300
301
        /** @var File $file */
302
        foreach ($files as $file) {
303
            $file->delete();
304
        }
305
306
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
307
            ->addHeader('Content-Type', 'application/json');
308
    }
309
310
    /**
311
     * Creates a single file based on a form-urlencoded upload.
312
     *
313
     * @param HTTPRequest $request
314
     * @return HTTPRequest|HTTPResponse
315
     */
316
    public function apiCreateFile(HTTPRequest $request)
317
    {
318
        $data = $request->postVars();
319
        $upload = $this->getUpload();
320
321
        // CSRF check
322
        $token = SecurityToken::inst();
323 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...
324
            return new HTTPResponse(null, 400);
325
        }
326
327
        // Check parent record
328
        /** @var Folder $parentRecord */
329
        $parentRecord = null;
330
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
331
            $parentRecord = Folder::get()->byID($data['ParentID']);
332
        }
333
        $data['Parent'] = $parentRecord;
334
335
        $tmpFile = $request->postVar('Upload');
336 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...
337
            $result = ['error' => $upload->getErrors()];
338
            return (new HTTPResponse(json_encode($result), 400))
339
                ->addHeader('Content-Type', 'application/json');
340
        }
341
342
        // TODO Allow batch uploads
343
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
344
        /** @var File $file */
345
        $file = Injector::inst()->create($fileClass);
346
347
        // check canCreate permissions
348 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...
349
            return (new HTTPResponse(json_encode(['status' => 'error']), 403))
350
                ->addHeader('Content-Type', 'application/json');
351
        }
352
353
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
354 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...
355
            $result = ['error' => 'unknown'];
356
            return (new HTTPResponse(json_encode($result), 400))
357
                ->addHeader('Content-Type', 'application/json');
358
        }
359
360
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
361
        $file->write();
362
363
        $result = [$this->getObjectFromData($file)];
364
365
        return (new HTTPResponse(json_encode($result)))
366
            ->addHeader('Content-Type', 'application/json');
367
    }
368
369
    /**
370
     * Creates a single folder, within an optional parent folder.
371
     *
372
     * @param HTTPRequest $request
373
     * @return HTTPRequest|HTTPResponse
374
     */
375
    public function apiCreateFolder(HTTPRequest $request)
376
    {
377
        $data = $request->postVars();
378
379
        $class = 'SilverStripe\\Assets\\Folder';
380
381
        // CSRF check
382
        $token = SecurityToken::inst();
383 View Code Duplication
        if (empty($data[$token->getName()]) || !$token->check($data[$token->getName()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
384
            return new HTTPResponse(null, 400);
385
        }
386
387
        // check addchildren permissions
388
        /** @var Folder $parentRecord */
389
        $parentRecord = null;
390
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
391
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
392
        }
393
        $data['Parent'] = $parentRecord;
394
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
395
396
        // Build filename
397
        $baseFilename = isset($data['Name'])
398
            ? basename($data['Name'])
399
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
400
401
        if ($parentRecord && $parentRecord->ID) {
402
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
403
        }
404
405
        // Ensure name is unique
406
        $nameGenerator = $this->getNameGenerator($baseFilename);
407
        $filename = null;
408
        foreach ($nameGenerator as $filename) {
409
            if (! File::find($filename)) {
410
                break;
411
            }
412
        }
413
        $data['Name'] = basename($filename);
414
415
        // Create record
416
        /** @var Folder $record */
417
        $record = Injector::inst()->create($class);
418
419
        // check create permissions
420
        if (!$record->canCreate(null, $data)) {
421
            return (new HTTPResponse(null, 403))
422
                ->addHeader('Content-Type', 'application/json');
423
        }
424
425
        $record->ParentID = $data['ParentID'];
426
        $record->Name = $record->Title = basename($data['Name']);
427
        $record->write();
428
429
        $result = $this->getObjectFromData($record);
430
431
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
432
    }
433
434
    /**
435
     * Redirects 3.x style detail links to new 4.x style routing.
436
     *
437
     * @param HTTPRequest $request
438
     */
439
    public function legacyRedirectForEditView($request)
440
    {
441
        $fileID = $request->param('FileID');
442
        /** @var File $file */
443
        $file = File::get()->byID($fileID);
444
        $link = $this->getFileEditLink($file) ?: $this->Link();
445
        $this->redirect($link);
446
    }
447
448
    /**
449
     * Given a file return the CMS link to edit it
450
     *
451
     * @param File $file
452
     * @return string
453
     */
454
    public function getFileEditLink($file)
455
    {
456
        if (!$file || !$file->isInDB()) {
457
            return null;
458
        }
459
460
        return Controller::join_links(
461
            $this->Link('show'),
462
            $file->ParentID,
463
            'edit',
464
            $file->ID
465
        );
466
    }
467
468
    /**
469
     * Get the search context from {@link File}, used to create the search form
470
     * as well as power the /search API endpoint.
471
     *
472
     * @return SearchContext
473
     */
474
    public function getSearchContext()
475
    {
476
        $context = File::singleton()->getDefaultSearchContext();
477
478
        // Customize fields
479
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
480
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
481
        ->setConfig('showcalendar', true);
482
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
483
        ->setConfig('showcalendar', true);
484
        $dateGroup = FieldGroup::create(
485
            $dateHeader,
486
            $dateFrom,
487
            $dateTo
488
        );
489
        $context->addField($dateGroup);
490
        /** @skipUpgrade */
491
        $appCategories = array(
492
            'archive' => _t(
493
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
494
                'Archive'
495
            ),
496
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
497
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
498
            'flash' => _t(
499
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
500
                'Flash',
501
                'The fileformat'
502
            ),
503
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
504
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
505
        );
506
        $context->addField(
507
            $typeDropdown = new DropdownField(
508
                'AppCategory',
509
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
510
                $appCategories
511
            )
512
        );
513
514
        $typeDropdown->setEmptyString(' ');
515
516
        $currentfolderLabel = _t(
517
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
518
            'Limit to current folder?'
519
        );
520
        $context->addField(
521
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
522
        );
523
        $context->getFields()->removeByName('Title');
524
525
        return $context;
526
    }
527
528
    /**
529
     * Get an asset renamer for the given filename.
530
     *
531
     * @param  string             $filename Path name
532
     * @return AssetNameGenerator
533
     */
534
    protected function getNameGenerator($filename)
535
    {
536
        return Injector::inst()
537
            ->createWithArgs('AssetNameGenerator', array($filename));
538
    }
539
540
    /**
541
     * @todo Implement on client
542
     *
543
     * @param bool $unlinked
544
     * @return ArrayList
545
     */
546
    public function breadcrumbs($unlinked = false)
547
    {
548
        return null;
549
    }
550
551
552
    /**
553
     * Don't include class namespace in auto-generated CSS class
554
     */
555
    public function baseCSSClasses()
556
    {
557
        return 'AssetAdmin LeftAndMain';
558
    }
559
560
    public function providePermissions()
561
    {
562
        return array(
563
            "CMS_ACCESS_AssetAdmin" => array(
564
                '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...
565
                    'title' => static::menu_title()
566
                )),
567
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
568
            )
569
        );
570
    }
571
572
    /**
573
     * Build a form scaffolder for this model
574
     *
575
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
576
     *
577
     * @param File $file
578
     * @return FormBuilder
579
     */
580
    public function getFormBuilder(File $file)
581
    {
582
        if ($file instanceof Folder) {
583
            return Injector::inst()->create(FolderFormBuilder::class, $this, $file);
584
        }
585
        if ($file instanceof Image) {
586
            return Injector::inst()->create(ImageFormBuilder::class, $this, $file);
587
        }
588
        return Injector::inst()->create(FileFormBuilder::class, $this, $file);
589
    }
590
591
    /**
592
     * The form is used to generate a form schema,
593
     * as well as an intermediary object to process data through API endpoints.
594
     * Since it's used directly on API endpoints, it does not have any form actions.
595
     * It handles both {@link File} and {@link Folder} records.
596
     *
597
     * @param int $id
598
     * @return Form
599
     */
600
    public function getFileEditForm($id)
601
    {
602
        /** @var File $file */
603
        $file = $this->getList()->byID($id);
604
605 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...
606
            $this->httpError(403, _t(
607
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
608
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
609
                '',
610
                ['ObjectTitle' => $file->i18n_singular_name()]
611
            ));
612
            return null;
613
        }
614
615
        $scaffolder = $this->getFormBuilder($file);
616
        $form = $scaffolder->getForm('FileEditForm');
617
618
        // Configure form to respond to validation errors with form schema
619
        // if requested via react.
620
        $form->setValidationResponseCallback(function () use ($form) {
621
            return $this->getSchemaResponse($form);
622
        });
623
624
        return $form;
625
    }
626
627
    /**
628
     * Get file edit form
629
     *
630
     * @return Form
631
     */
632
    public function FileEditForm()
633
    {
634
        // Get ID either from posted back value, or url parameter
635
        $request = $this->getRequest();
636
        $id = $request->param('ID') ?: $request->postVar('ID');
637
        return $this->getFileEditForm($id);
638
    }
639
640
    /**
641
     * @param array $data
642
     * @param Form $form
643
     * @return HTTPResponse
644
     */
645
    public function save($data, $form)
646
    {
647
        return $this->saveOrPublish($data, $form, false);
648
    }
649
650
651
    /**
652
     * @param array $data
653
     * @param Form $form
654
     * @return HTTPResponse
655
     */
656
    public function publish($data, $form)
657
    {
658
        return $this->saveOrPublish($data, $form, true);
659
    }
660
661
    /**
662
     * Update thisrecord
663
     *
664
     * @param array $data
665
     * @param Form $form
666
     * @param bool $doPublish
667
     * @return HTTPResponse
668
     */
669
    protected function saveOrPublish($data, $form, $doPublish = false)
670
    {
671 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...
672
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
673
                ->addHeader('Content-Type', 'application/json');
674
        }
675
676
        $id = (int) $data['ID'];
677
        /** @var File $record */
678
        $record = $this->getList()->filter('ID', $id)->first();
679
680 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...
681
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
682
                ->addHeader('Content-Type', 'application/json');
683
        }
684
685 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...
686
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
687
                ->addHeader('Content-Type', 'application/json');
688
        }
689
690
        $form->saveInto($record);
691
        $record->write();
692
693
        // Publish this record and owned objects
694
        if ($doPublish) {
695
            $record->publishRecursive();
696
        }
697
698
        // Return the record data in the same response as the schema to save a postback
699
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
700
        $schemaData['record'] = $this->getObjectFromData($record);
701
        $response = new HTTPResponse(Convert::raw2json($schemaData));
702
        $response->addHeader('Content-Type', 'application/json');
703
        return $response;
704
    }
705
706
    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...
707
    {
708 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...
709
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
710
                ->addHeader('Content-Type', 'application/json');
711
        }
712
713
        $id = (int) $data['ID'];
714
        /** @var File $record */
715
        $record = $this->getList()->filter('ID', $id)->first();
716
717
        if (!$record) {
718
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
719
                ->addHeader('Content-Type', 'application/json');
720
        }
721
722
        if (!$record->canUnpublish()) {
723
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
724
                ->addHeader('Content-Type', 'application/json');
725
        }
726
727
        $record->doUnpublish();
728
729
        // Return the record data in the same response as the schema to save a postback
730
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
731
        $schemaData['record'] = $this->getObjectFromData($record);
732
        $response = new HTTPResponse(Convert::raw2json($schemaData));
733
        $response->addHeader('Content-Type', 'application/json');
734
        return $response;
735
    }
736
737
    /**
738
     * @param File $file
739
     *
740
     * @return array
741
     */
742
    protected function getObjectFromData(File $file)
743
    {
744
        $object = array(
745
            'id' => $file->ID,
746
            'created' => $file->Created,
747
            'lastUpdated' => $file->LastEdited,
748
            'owner' => null,
749
            'parent' => null,
750
            'title' => $file->Title,
751
            'exists' => $file->exists(), // Broken file check
752
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
753
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
754
            'name' => $file->Name,
755
            'filename' => $file->Filename,
756
            '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...
757
            'size' => $file->Size,
758
            'url' => $file->AbsoluteURL,
759
            'published' => $file->isPublished(),
760
            'modified' => $file->isModifiedOnDraft(),
761
            'draft' => $file->isOnDraftOnly(),
762
            'canEdit' => $file->canEdit(),
763
            'canDelete' => $file->canDelete(),
764
        );
765
766
        /** @var Member $owner */
767
        $owner = $file->Owner();
768
769
        if ($owner) {
770
            $object['owner'] = array(
771
                'id' => $owner->ID,
772
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
773
            );
774
        }
775
776
        /** @var Folder $parent */
777
        $parent = $file->Parent();
778
779
        if ($parent) {
780
            $object['parent'] = array(
781
                'id' => $parent->ID,
782
                'title' => $parent->Title,
783
                'filename' => $parent->Filename,
784
            );
785
        }
786
787
        /** @var File $file */
788
        if ($file->getIsImage()) {
789
            $width = (int)Config::inst()->get(self::class, 'thumbnail_width');
790
            $height = (int)Config::inst()->get(self::class, 'thumbnail_height');
791
792
            $thumbnail = $file->FitMax($width, $height);
793
            if ($thumbnail && $thumbnail->exists()) {
794
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
795
            }
796
            $object['dimensions']['width'] = $file->Width;
797
            $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...
798
        }
799
800
        return $object;
801
    }
802
803
804
    /**
805
     * Returns the files and subfolders contained in the currently selected folder,
806
     * defaulting to the root node. Doubles as search results, if any search parameters
807
     * are set through {@link SearchForm()}.
808
     *
809
     * @param array $params Unsanitised request parameters
810
     * @return DataList
811
     */
812
    protected function getList($params = array())
813
    {
814
        $context = $this->getSearchContext();
815
816
        // Overwrite name filter to search both Name and Title attributes
817
        $context->removeFilterByName('Name');
818
819
        // Lazy loaded list. Allows adding new filters through SearchContext.
820
        /** @var DataList $list */
821
        $list = $context->getResults($params);
822
823
        // Re-add previously removed "Name" filter as combined filter
824
        // TODO Replace with composite SearchFilter once that API exists
825
        if (!empty($params['Name'])) {
826
            $list = $list->filterAny(array(
827
                'Name:PartialMatch' => $params['Name'],
828
                'Title:PartialMatch' => $params['Name']
829
            ));
830
        }
831
832
        // Optionally limit search to a folder (non-recursive)
833
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
834
            $list = $list->filter('ParentID', $params['ParentID']);
835
        }
836
837
        // Date filtering
838 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...
839
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
840
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
841
        }
842 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...
843
            $toDate = new DateField(null, null, $params['CreatedTo']);
844
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
845
        }
846
847
        // Categories
848
        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...
849
            $extensions = File::config()->app_categories[$filters['AppCategory']];
850
            $list = $list->filter('Name:PartialMatch', $extensions);
851
        }
852
853
        // Sort folders first
854
        $list = $list->sort(
855
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
856
        );
857
858
        // Pagination
859
        if (isset($filters['page']) && isset($filters['limit'])) {
860
            $page = $filters['page'];
861
            $limit = $filters['limit'];
862
            $offset = ($page - 1) * $limit;
863
            $list = $list->limit($limit, $offset);
864
        }
865
866
        // Access checks
867
        $list = $list->filterByCallback(function (File $file) {
868
            return $file->canView();
869
        });
870
871
        return $list;
872
    }
873
874
    /**
875
     * Action handler for adding pages to a campaign
876
     *
877
     * @param array $data
878
     * @param Form $form
879
     * @return DBHTMLText|HTTPResponse
880
     */
881
    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...
882
    {
883
        $id = $data['ID'];
884
        $record = $this->getList()->byID($id);
885
886
        $handler = AddToCampaignHandler::create($this, $record);
887
        $results = $handler->addToCampaign($record, $data['Campaign']);
888
        if (!is_null($results)) {
889
            $request = $this->getRequest();
890
            if ($request->getHeader('X-Formschema-Request')) {
891
                $data = $this->getSchemaForForm($handler->Form($record));
892
                $data['message'] = $results;
893
894
                $response = new HTTPResponse(Convert::raw2json($data));
895
                $response->addHeader('Content-Type', 'application/json');
896
                return $response;
897
            }
898
            return $results;
899
        }
900
    }
901
902
    /**
903
     * Url handler for add to campaign form
904
     *
905
     * @param HTTPRequest $request
906
     * @return Form
907
     */
908
    public function AddToCampaignForm($request)
909
    {
910
        // Get ID either from posted back value, or url parameter
911
        $id = $request->param('ID') ?: $request->postVar('ID');
912
        return $this->getAddToCampaignForm($id);
913
    }
914
915
    /**
916
     * @param int $id
917
     * @return Form
918
     */
919
    public function getAddToCampaignForm($id)
920
    {
921
        // Get record-specific fields
922
        $record = $this->getList()->byID($id);
923
924
        if (!$record) {
925
            $this->httpError(404, _t(
926
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
927
                'That {Type} couldn\'t be found',
928
                '',
929
                ['Type' => File::singleton()->i18n_singular_name()]
930
            ));
931
            return null;
932
        }
933 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...
934
            $this->httpError(403, _t(
935
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
936
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
937
                '',
938
                ['ObjectTitle' => $record->i18n_singular_name()]
939
            ));
940
            return null;
941
        }
942
943
        $handler = AddToCampaignHandler::create($this, $record);
944
        return $handler->Form($record);
945
    }
946
947
    /**
948
     * @return Upload
949
     */
950
    protected function getUpload()
951
    {
952
        $upload = Upload::create();
953
        $upload->getValidator()->setAllowedExtensions(
954
            // filter out '' since this would be a regex problem on JS end
955
            array_filter(File::config()->get('allowed_extensions'))
956
        );
957
958
        return $upload;
959
    }
960
}
961