Completed
Pull Request — master (#312)
by Damian
01:48
created

AssetAdmin::getObjectFromData()   C

Complexity

Conditions 10
Paths 80

Size

Total Lines 68
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 68
rs 6.0995
cc 10
eloc 46
nc 80
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
602
        if ($file instanceof Folder) {
603
            $name = FolderFormFactory::class;
604
        } elseif ($file instanceof Image) {
605
            $name = ImageFormFactory::class;
606
        } else {
607
            $name = FileFormFactory::class;
608
        }
609
        return Injector::inst()->get($name);
610
    }
611
612
    /**
613
     * The form is used to generate a form schema,
614
     * as well as an intermediary object to process data through API endpoints.
615
     * Since it's used directly on API endpoints, it does not have any form actions.
616
     * It handles both {@link File} and {@link Folder} records.
617
     *
618
     * @param int $id
619
     * @return Form
620
     */
621
    public function getFileEditForm($id)
622
    {
623
        /** @var File $file */
624
        $file = $this->getList()->byID($id);
625
626 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...
627
            $this->httpError(403, _t(
628
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
629
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
630
                '',
631
                ['ObjectTitle' => $file->i18n_singular_name()]
632
            ));
633
            return null;
634
        }
635
636
        $scaffolder = $this->getFormFactory($file);
637
        $form = $scaffolder->getForm($this, 'FileEditForm', [
638
            'Record' => $file
639
        ]);
640
641
        // Configure form to respond to validation errors with form schema
642
        // if requested via react.
643 View Code Duplication
        $form->setValidationResponseCallback(function() use ($form, $file) {
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...
644
            $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $file->exists() ? $file->ID : '');
645
            return $this->getSchemaResponse($form, $schemaId);
646
        });
647
648
        return $form;
649
    }
650
651
    /**
652
     * Get file edit form
653
     *
654
     * @return Form
655
     */
656
    public function FileEditForm()
657
    {
658
        // Get ID either from posted back value, or url parameter
659
        $request = $this->getRequest();
660
        $id = $request->param('ID') ?: $request->postVar('ID');
661
        return $this->getFileEditForm($id);
662
    }
663
664
    /**
665
     * @param array $data
666
     * @param Form $form
667
     * @return HTTPResponse
668
     */
669
    public function save($data, $form)
670
    {
671
        return $this->saveOrPublish($data, $form, false);
672
    }
673
674
675
    /**
676
     * @param array $data
677
     * @param Form $form
678
     * @return HTTPResponse
679
     */
680
    public function publish($data, $form)
681
    {
682
        return $this->saveOrPublish($data, $form, true);
683
    }
684
685
    /**
686
     * Update thisrecord
687
     *
688
     * @param array $data
689
     * @param Form $form
690
     * @param bool $doPublish
691
     * @return HTTPResponse
692
     */
693
    protected function saveOrPublish($data, $form, $doPublish = false)
694
    {
695 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...
696
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
697
                ->addHeader('Content-Type', 'application/json');
698
        }
699
700
        $id = (int) $data['ID'];
701
        /** @var File $record */
702
        $record = $this->getList()->filter('ID', $id)->first();
703
704 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...
705
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
706
                ->addHeader('Content-Type', 'application/json');
707
        }
708
709
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
710
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
711
                ->addHeader('Content-Type', 'application/json');
712
        }
713
714
        $form->saveInto($record);
715
        $record->write();
716
717
        // Publish this record and owned objects
718
        if ($doPublish) {
719
            $record->publishRecursive();
720
        }
721
722
        // Return the record data in the same response as the schema to save a postback
723
        $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $record->exists() ? $record->ID : '');
724
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id), $schemaId);
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...
725
        $schemaData['record'] = $this->getObjectFromData($record);
726
        $response = new HTTPResponse(Convert::raw2json($schemaData));
727
        $response->addHeader('Content-Type', 'application/json');
728
        return $response;
729
    }
730
731
    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...
732
    {
733 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...
734
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
735
                ->addHeader('Content-Type', 'application/json');
736
        }
737
738
        $id = (int) $data['ID'];
739
        /** @var File $record */
740
        $record = $this->getList()->filter('ID', $id)->first();
741
742
        if (!$record) {
743
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
744
                ->addHeader('Content-Type', 'application/json');
745
        }
746
747
        if (!$record->canUnpublish()) {
748
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
749
                ->addHeader('Content-Type', 'application/json');
750
        }
751
752
        $record->doUnpublish();
753
754
        // Return the record data in the same response as the schema to save a postback
755
        $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $record->exists() ? $record->ID : '');
756
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id), $schemaId);
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...
757
        $schemaData['record'] = $this->getObjectFromData($record);
758
        $response = new HTTPResponse(Convert::raw2json($schemaData));
759
        $response->addHeader('Content-Type', 'application/json');
760
        return $response;
761
    }
762
763
    /**
764
     * @param File $file
765
     *
766
     * @return array
767
     */
768
    public function getObjectFromData(File $file)
769
    {
770
        $object = array(
771
            'id' => $file->ID,
772
            'created' => $file->Created,
773
            'lastUpdated' => $file->LastEdited,
774
            'owner' => null,
775
            'parent' => null,
776
            'title' => $file->Title,
777
            'exists' => $file->exists(), // Broken file check
778
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
779
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
780
            'name' => $file->Name,
781
            'filename' => $file->Filename,
782
            '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...
783
            'size' => $file->Size,
784
            'url' => $file->AbsoluteURL,
785
            'published' => $file->isPublished(),
786
            'modified' => $file->isModifiedOnDraft(),
787
            'draft' => $file->isOnDraftOnly(),
788
            'canEdit' => $file->canEdit(),
789
            'canDelete' => $file->canArchive(),
790
        );
791
792
        /** @var Member $owner */
793
        $owner = $file->Owner();
794
795
        if ($owner) {
796
            $object['owner'] = array(
797
                'id' => $owner->ID,
798
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
799
            );
800
        }
801
802
        /** @var Folder $parent */
803
        $parent = $file->Parent();
804
805
        if ($parent) {
806
            $object['parent'] = array(
807
                'id' => $parent->ID,
808
                'title' => $parent->Title,
809
                'filename' => $parent->Filename,
810
            );
811
        }
812
813
        /** @var File $file */
814
        if ($file->getIsImage()) {
815
            // Small thumbnail
816
            $smallWidth = FileField::config()->get('thumbnail_width');
817
            $smallHeight = FileField::config()->get('thumbnail_height');
818
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
819
            if ($smallThumbnail && $smallThumbnail->exists()) {
820
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
821
            }
822
823
            // Large thumbnail
824
            $width = $this->config()->get('thumbnail_width');
825
            $height = $this->config()->get('thumbnail_height');
826
            $thumbnail = $file->FitMax($width, $height);
827
            if ($thumbnail && $thumbnail->exists()) {
828
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
829
            }
830
            $object['dimensions']['width'] = $file->Width;
831
            $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...
832
        }
833
834
        return $object;
835
    }
836
837
838
    /**
839
     * Returns the files and subfolders contained in the currently selected folder,
840
     * defaulting to the root node. Doubles as search results, if any search parameters
841
     * are set through {@link SearchForm()}.
842
     *
843
     * @param array $params Unsanitised request parameters
844
     * @return DataList
845
     */
846
    protected function getList($params = array())
847
    {
848
        $context = $this->getSearchContext();
849
850
        // Overwrite name filter to search both Name and Title attributes
851
        $context->removeFilterByName('Name');
852
853
        // Lazy loaded list. Allows adding new filters through SearchContext.
854
        /** @var DataList $list */
855
        $list = $context->getResults($params);
856
857
        // Re-add previously removed "Name" filter as combined filter
858
        // TODO Replace with composite SearchFilter once that API exists
859
        if(!empty($params['Name'])) {
860
            $list = $list->filterAny(array(
861
                'Name:PartialMatch' => $params['Name'],
862
                'Title:PartialMatch' => $params['Name']
863
            ));
864
        }
865
866
        // Optionally limit search to a folder (non-recursive)
867
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
868
            $list = $list->filter('ParentID', $params['ParentID']);
869
        }
870
871
        // Date filtering
872 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...
873
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
874
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
875
        }
876 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...
877
            $toDate = new DateField(null, null, $params['CreatedTo']);
878
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
879
        }
880
881
        // Categories
882
        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...
883
            $extensions = File::config()->app_categories[$filters['AppCategory']];
884
            $list = $list->filter('Name:PartialMatch', $extensions);
885
        }
886
887
        // Sort folders first
888
        $list = $list->sort(
889
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
890
        );
891
892
        // Pagination
893
        if (isset($filters['page']) && isset($filters['limit'])) {
894
            $page = $filters['page'];
895
            $limit = $filters['limit'];
896
            $offset = ($page - 1) * $limit;
897
            $list = $list->limit($limit, $offset);
898
        }
899
900
        // Access checks
901
        $list = $list->filterByCallback(function(File $file) {
902
            return $file->canView();
903
        });
904
905
        return $list;
906
    }
907
908
    /**
909
     * Action handler for adding pages to a campaign
910
     *
911
     * @param array $data
912
     * @param Form $form
913
     * @return DBHTMLText|HTTPResponse
914
     */
915
    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...
916
    {
917
        $id = $data['ID'];
918
        $record = $this->getList()->byID($id);
919
920
        $handler = AddToCampaignHandler::create($this, $record);
921
        $results = $handler->addToCampaign($record, $data['Campaign']);
922
        if (!isset($results)) {
923
            return null;
924
        }
925
        $request = $this->getRequest();
926
        if($request->getHeader('X-Formschema-Request')) {
927
            $data = $this->getSchemaForForm($handler->Form($record));
928
            $data['message'] = $results;
929
930
            $response = new HTTPResponse(Convert::raw2json($data));
931
            $response->addHeader('Content-Type', 'application/json');
932
            return $response;
933
        }
934
        return $results;
935
    }
936
937
    /**
938
     * Url handler for add to campaign form
939
     *
940
     * @param HTTPRequest $request
941
     * @return Form
942
     */
943
    public function AddToCampaignForm($request)
944
    {
945
        // Get ID either from posted back value, or url parameter
946
        $id = $request->param('ID') ?: $request->postVar('ID');
947
        return $this->getAddToCampaignForm($id);
948
    }
949
950
    /**
951
     * @param int $id
952
     * @return Form
953
     */
954
    public function getAddToCampaignForm($id)
955
    {
956
        // Get record-specific fields
957
        $record = $this->getList()->byID($id);
958
959
        if (!$record) {
960
            $this->httpError(404, _t(
961
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
962
                'That {Type} couldn\'t be found',
963
                '',
964
                ['Type' => File::singleton()->i18n_singular_name()]
965
            ));
966
            return null;
967
        }
968 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...
969
            $this->httpError(403, _t(
970
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
971
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
972
                '',
973
                ['ObjectTitle' => $record->i18n_singular_name()]
974
            ));
975
            return null;
976
        }
977
978
        $handler = AddToCampaignHandler::create($this, $record);
979
        $form = $handler->Form($record);
980
981 View Code Duplication
        $form->setValidationResponseCallback(function() use ($form, $id) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
982
            $schemaId = Controller::join_links($this->Link('schema/AddToCampaignForm'), $id);
983
            return $this->getSchemaResponse($form, $schemaId);
984
        });
985
986
        return $form;
987
    }
988
989
    /**
990
     * @return Upload
991
     */
992
    protected function getUpload()
993
    {
994
        $upload = Upload::create();
995
        $upload->getValidator()->setAllowedExtensions(
996
            // filter out '' since this would be a regex problem on JS end
997
            array_filter(File::config()->get('allowed_extensions'))
998
        );
999
1000
        return $upload;
1001
    }
1002
}
1003