Completed
Push — master ( f369a2...2249a7 )
by Loz
11s
created

AssetAdmin::getFileEditForm()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 53
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 0 Features 2
Metric Value
c 11
b 0
f 2
dl 0
loc 53
rs 8.9849
cc 4
eloc 27
nc 4
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\Assets\File;
9
use SilverStripe\Assets\Folder;
10
use SilverStripe\Assets\Storage\AssetNameGenerator;
11
use SilverStripe\Assets\Upload;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\SS_HTTPRequest;
14
use SilverStripe\Control\SS_HTTPResponse;
15
use SilverStripe\Core\Convert;
16
use SilverStripe\Core\Injector\Injector;
17
use SilverStripe\Forms\CheckboxField;
18
use SilverStripe\Forms\DateField;
19
use SilverStripe\Forms\DropdownField;
20
use SilverStripe\Forms\FieldGroup;
21
use SilverStripe\Forms\FieldList;
22
use SilverStripe\Forms\Form;
23
use SilverStripe\Forms\FormAction;
24
use SilverStripe\Forms\HeaderField;
25
use SilverStripe\Forms\PopoverField;
26
use SilverStripe\ORM\ArrayList;
27
use SilverStripe\ORM\DataList;
28
use SilverStripe\ORM\DataObject;
29
use SilverStripe\ORM\FieldType\DBHTMLText;
30
use SilverStripe\ORM\Search\SearchContext;
31
use SilverStripe\Security\Member;
32
use SilverStripe\Security\PermissionProvider;
33
use SilverStripe\Security\SecurityToken;
34
use SilverStripe\View\Requirements;
35
36
/**
37
 * AssetAdmin is the 'file store' section of the CMS.
38
 * It provides an interface for manipulating the File and Folder objects in the system.
39
 */
40
class AssetAdmin extends LeftAndMain implements PermissionProvider
41
{
42
    private static $url_segment = 'assets';
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
43
44
    private static $url_rule = '/$Action/$ID';
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
45
46
    private static $menu_title = 'Files';
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
47
48
    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...
49
50
    private static $url_handlers = [
0 ignored issues
show
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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
51
        // Legacy redirect for SS3-style detail view
52
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
53
        // Pass all URLs to the index, for React to unpack
54
        'show/$FolderID/edit/$FileID' => 'index',
55
        // API access points with structured data
56
        'POST api/createFolder' => 'apiCreateFolder',
57
        'POST api/createFile' => 'apiCreateFile',
58
        'GET api/readFolder' => 'apiReadFolder',
59
        'PUT api/updateFolder' => 'apiUpdateFolder',
60
        'DELETE api/delete' => 'apiDelete',
61
        'GET api/search' => 'apiSearch',
62
    ];
63
64
    /**
65
     * Amount of results showing on a single page.
66
     *
67
     * @config
68
     * @var int
69
     */
70
    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...
71
72
    /**
73
     * @config
74
     * @see Upload->allowedMaxFileSize
75
     * @var int
76
     */
77
    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...
78
79
    /**
80
     * @var array
81
     */
82
    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...
83
        'legacyRedirectForEditView',
84
        'apiCreateFolder',
85
        'apiCreateFile',
86
        'apiReadFolder',
87
        'apiUpdateFolder',
88
        'apiDelete',
89
        'apiSearch',
90
        'FileEditForm',
91
        'AddToCampaignForm',
92
    );
93
94
    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...
95
96
    /**
97
     * Set up the controller
98
     */
99
    public function init()
100
    {
101
        parent::init();
102
103
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
104
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
105
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
106
107
        CMSBatchActionHandler::register(
108
            'delete',
109
            'SilverStripe\AssetAdmin\BatchAction\DeleteAssets',
110
            'SilverStripe\\Assets\\Folder'
111
        );
112
    }
113
114
    public function getClientConfig()
115
    {
116
        $baseLink = $this->Link();
117
        return array_merge( parent::getClientConfig(), [
118
            'reactRouter' => true,
119
            'createFileEndpoint' => [
120
                'url' => Controller::join_links($baseLink, 'api/createFile'),
121
                'method' => 'post',
122
                'payloadFormat' => 'urlencoded',
123
            ],
124
            'createFolderEndpoint' => [
125
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
126
                'method' => 'post',
127
                'payloadFormat' => 'urlencoded',
128
            ],
129
            'readFolderEndpoint' => [
130
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
131
                'method' => 'get',
132
                'responseFormat' => 'json',
133
            ],
134
            'searchEndpoint' => [
135
                'url' => Controller::join_links($baseLink, 'api/search'),
136
                'method' => 'get',
137
                'responseFormat' => 'json',
138
            ],
139
            'updateFolderEndpoint' => [
140
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
141
                'method' => 'put',
142
                'payloadFormat' => 'urlencoded',
143
            ],
144
            'deleteEndpoint' => [
145
                'url' => Controller::join_links($baseLink, 'api/delete'),
146
                'method' => 'delete',
147
                'payloadFormat' => 'urlencoded',
148
            ],
149
            'limit' => $this->config()->page_length,
150
            'form' => [
151
                'FileEditForm' => [
152
                    'schemaUrl' => $this->Link('schema/FileEditForm')
153
                ],
154
                'AddToCampaignForm' => [
155
                    'schemaUrl' => $this->Link('schema/AddToCampaignForm')
156
                ],
157
            ],
158
        ]);
159
    }
160
161
    /**
162
     * Fetches a collection of files by ParentID.
163
     *
164
     * @param SS_HTTPRequest $request
165
     * @return SS_HTTPResponse
166
     */
167
    public function apiReadFolder(SS_HTTPRequest $request)
168
    {
169
        $params = $request->requestVars();
170
        $items = array();
171
        $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...
172
        $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...
173
174
        if (!isset($params['id']) && !strlen($params['id'])) {
175
            $this->httpError(400);
176
        }
177
178
        $folderID = (int)$params['id'];
179
        /** @var Folder $folder */
180
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
181
182
        if (!$folder) {
183
            $this->httpError(400);
184
        }
185
186
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
187
        $files = $this->getList()->filter('ParentID', $folderID);
188
189
        if ($files) {
190
            /** @var File $file */
191
            foreach ($files as $file) {
192
                if (!$file->canView()) {
193
                    continue;
194
                }
195
196
                $items[] = $this->getObjectFromData($file);
197
            }
198
        }
199
200
        // Build parents (for breadcrumbs)
201
        $parents = [];
202
        $next = $folder->Parent();
203
        while($next && $next->exists()) {
204
            array_unshift($parents, [
205
                'id' => $next->ID,
206
                'title' => $next->getTitle(),
207
            ]);
208
            if($next->ParentID) {
209
                $next = $next->Parent();
210
            } else {
211
                break;
212
            }
213
        }
214
215
        // Build response
216
        $response = new SS_HTTPResponse();
217
        $response->addHeader('Content-Type', 'application/json');
218
        $response->setBody(json_encode([
219
            'files' => $items,
220
            'title' => $folder->getTitle(),
221
            'count' => count($items),
222
            'parents' => $parents,
223
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
224
            'folderID' => $folderID,
225
            'canEdit' => $folder->canEdit(),
226
            'canDelete' => $folder->canDelete(),
227
        ]));
228
229
        return $response;
230
    }
231
232
    /**
233
     * @param SS_HTTPRequest $request
234
     *
235
     * @return SS_HTTPResponse
236
     */
237
    public function apiSearch(SS_HTTPRequest $request)
238
    {
239
        $params = $request->getVars();
240
        $list = $this->getList($params);
241
242
        $response = new SS_HTTPResponse();
243
        $response->addHeader('Content-Type', 'application/json');
244
        $response->setBody(json_encode([
245
            // Serialisation
246
            "files" => array_map(function($file) {
247
                return $this->getObjectFromData($file);
248
            }, $list->toArray()),
249
            "count" => $list->count(),
250
        ]));
251
252
        return $response;
253
    }
254
255
    /**
256
     * @param SS_HTTPRequest $request
257
     *
258
     * @return SS_HTTPResponse
259
     */
260
    public function apiDelete(SS_HTTPRequest $request)
261
    {
262
        parse_str($request->getBody(), $vars);
263
264
        // CSRF check
265
        $token = SecurityToken::inst();
266 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...
267
            return new SS_HTTPResponse(null, 400);
268
        }
269
270 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...
271
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
272
                ->addHeader('Content-Type', 'application/json');
273
        }
274
275
        $fileIds = $vars['ids'];
276
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
277
278 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...
279
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
280
                ->addHeader('Content-Type', 'application/json');
281
        }
282
283
        if (!min(array_map(function (File $file) {
284
            return $file->canDelete();
285
        }, $files))) {
286
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
287
                ->addHeader('Content-Type', 'application/json');
288
        }
289
290
        /** @var File $file */
291
        foreach ($files as $file) {
292
            $file->delete();
293
        }
294
295
        return (new SS_HTTPResponse(json_encode(['status' => 'file was deleted'])))
296
            ->addHeader('Content-Type', 'application/json');
297
    }
298
299
    /**
300
     * Creates a single file based on a form-urlencoded upload.
301
     *
302
     * @param SS_HTTPRequest $request
303
     * @return SS_HTTPRequest|SS_HTTPResponse
304
     */
305
    public function apiCreateFile(SS_HTTPRequest $request)
306
    {
307
        $data = $request->postVars();
308
        $upload = $this->getUpload();
309
310
        // CSRF check
311
        $token = SecurityToken::inst();
312 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...
313
            return new SS_HTTPResponse(null, 400);
314
        }
315
316
        // check canAddChildren permissions
317
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
318
            $parentRecord = Folder::get()->byID($data['ParentID']);
319 View Code Duplication
            if ($parentRecord->hasMethod('canAddChildren') && !$parentRecord->canAddChildren()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
320
                return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
321
                    ->addHeader('Content-Type', 'application/json');
322
            }
323
        } else {
324
            $parentRecord = Folder::singleton();
325
        }
326
327
        // check create permissions
328 View Code Duplication
        if (!$parentRecord->canCreate()) {
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...
329
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
330
                ->addHeader('Content-Type', 'application/json');
331
        }
332
333
        /** @skipUpgrade */
334
        $tmpFile = $request->postVar('Upload');
335
        if(!$upload->validate($tmpFile)) {
336
            $result = ['error' => $upload->getErrors()];
337
            return (new SS_HTTPResponse(json_encode($result), 400))
338
                ->addHeader('Content-Type', 'application/json');
339
        }
340
341
        // TODO Allow batch uploads
342
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
343
        $file = Injector::inst()->create($fileClass);
344
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
345 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...
346
            $result = ['error' => 'unknown'];
347
            return (new SS_HTTPResponse(json_encode($result), 400))
348
                ->addHeader('Content-Type', 'application/json');
349
        }
350
351
        $file->ParentID = $parentRecord->ID;
352
        $file->write();
353
354
        $result = [$this->getObjectFromData($file)];
355
356
        return (new SS_HTTPResponse(json_encode($result)))
357
            ->addHeader('Content-Type', 'application/json');
358
    }
359
360
    /**
361
     * Creates a single folder, within an optional parent folder.
362
     *
363
     * @param SS_HTTPRequest $request
364
     * @return SS_HTTPRequest|SS_HTTPResponse
365
     */
366
    public function apiCreateFolder(SS_HTTPRequest $request)
367
    {
368
        $data = $request->postVars();
369
370
        $class = 'SilverStripe\\Assets\\Folder';
371
372
        // CSRF check
373
        $token = SecurityToken::inst();
374 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...
375
            return new SS_HTTPResponse(null, 400);
376
        }
377
378
        // check addchildren permissions
379
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
380
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
381 View Code Duplication
            if ($parentRecord->hasMethod('canAddChildren') && !$parentRecord->canAddChildren()) {
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...
382
                return (new SS_HTTPResponse(null, 403))
383
                    ->addHeader('Content-Type', 'application/json');
384
            }
385
        } else {
386
            $parentRecord = singleton($class);
387
        }
388
        $data['ParentID'] = ($parentRecord->exists()) ? (int)$parentRecord->ID : 0;
389
390
        // check create permissions
391
        if (!$parentRecord->canCreate()) {
392
            return (new SS_HTTPResponse(null, 403))
393
                ->addHeader('Content-Type', 'application/json');
394
        }
395
396
        // Build filename
397
        $baseFilename = isset($data['Name'])
398
            ? basename($data['Name'])
399
            : _t('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
        $record->ParentID = $data['ParentID'];
419
        $record->Name = $record->Title = basename($data['Name']);
420
        $record->write();
421
422
        $result = $this->getObjectFromData($record);
423
424
        return (new SS_HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
425
    }
426
427
    /**
428
     * Redirects 3.x style detail links to new 4.x style routing.
429
     *
430
     * @param SS_HTTPRequest $request
431
     */
432
    public function legacyRedirectForEditView($request)
433
    {
434
        $fileID = $request->param('FileID');
435
        /** @var File $file */
436
        $file = File::get()->byID($fileID);
437
        $link = $this->getFileEditLink($file) ?: $this->Link();
438
        $this->redirect($link);
439
    }
440
441
    /**
442
     * Given a file return the CMS link to edit it
443
     *
444
     * @param File $file
445
     * @return string
446
     */
447
    public function getFileEditLink($file) {
448
        if(!$file || !$file->isInDB()) {
449
            return null;
450
        }
451
452
        return Controller::join_links(
453
            $this->Link('show'),
454
            $file->ParentID,
455
            'edit',
456
            $file->ID
457
        );
458
    }
459
460
    /**
461
     * Get the search context from {@link File}, used to create the search form
462
     * as well as power the /search API endpoint.
463
     *
464
     * @return SearchContext
465
     */
466
    public function getSearchContext()
467
    {
468
        $context = File::singleton()->getDefaultSearchContext();
469
470
        // Customize fields
471
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
472
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
473
            ->setConfig('showcalendar', true);
474
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
475
            ->setConfig('showcalendar', true);
476
        $dateGroup = FieldGroup::create(
477
            $dateHeader,
478
            $dateFrom,
479
            $dateTo
480
        );
481
        $context->addField($dateGroup);
482
        /** @skipUpgrade */
483
        $appCategories = array(
484
            'archive' => _t('AssetAdmin.AppCategoryArchive', 'Archive', 'A collection of files'),
485
            'audio' => _t('AssetAdmin.AppCategoryAudio', 'Audio'),
486
            'document' => _t('AssetAdmin.AppCategoryDocument', 'Document'),
487
            'flash' => _t('AssetAdmin.AppCategoryFlash', 'Flash', 'The fileformat'),
488
            'image' => _t('AssetAdmin.AppCategoryImage', 'Image'),
489
            'video' => _t('AssetAdmin.AppCategoryVideo', 'Video'),
490
        );
491
        $context->addField(
492
            $typeDropdown = new DropdownField(
493
                'AppCategory',
494
                _t('AssetAdmin.Filetype', 'File type'),
495
                $appCategories
496
            )
497
        );
498
499
        $typeDropdown->setEmptyString(' ');
500
501
        $context->addField(
502
            new CheckboxField('CurrentFolderOnly', _t('AssetAdmin.CurrentFolderOnly', 'Limit to current folder?'))
503
        );
504
        $context->getFields()->removeByName('Title');
505
506
        return $context;
507
    }
508
509
    /**
510
     * Get an asset renamer for the given filename.
511
     *
512
     * @param  string             $filename Path name
513
     * @return AssetNameGenerator
514
     */
515
    protected function getNameGenerator($filename)
516
    {
517
        return Injector::inst()
518
            ->createWithArgs('AssetNameGenerator', array($filename));
519
    }
520
521
    /**
522
     * @todo Implement on client
523
     *
524
     * @param bool $unlinked
525
     * @return ArrayList
526
     */
527
    public function breadcrumbs($unlinked = false)
528
    {
529
        return null;
530
    }
531
532
533
    /**
534
     * Don't include class namespace in auto-generated CSS class
535
     */
536
    public function baseCSSClasses()
537
    {
538
        return 'AssetAdmin LeftAndMain';
539
    }
540
541
    public function providePermissions()
542
    {
543
        return array(
544
            "CMS_ACCESS_AssetAdmin" => array(
545
                '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...
546
                    'title' => static::menu_title()
547
                )),
548
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
549
            )
550
        );
551
    }
552
553
    /**
554
     * The form is used to generate a form schema,
555
     * as well as an intermediary object to process data through API endpoints.
556
     * Since it's used directly on API endpoints, it does not have any form actions.
557
     * It handles both {@link File} and {@link Folder} records.
558
     *
559
     * @param int $id
560
     * @return Form
561
     */
562
    public function getFileEditForm($id)
563
    {
564
        /** @var File $file */
565
        $file = $this->getList()->byID($id);
566
567
        $fields = $file->getCMSFields();
568
569
        $actions = FieldList::create([
570
            FormAction::create('save', _t('CMSMain.SAVE', 'Save'))
571
                ->setIcon('save')
572
        ]);
573
574
        // Delete action
575
        $actions->push(
576
            FormAction::create(
577
                'delete',
578
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.DELETE_BUTTON', 'Delete')
579
            )
580
                ->setIcon('trash-bin')
581
        );
582
583
        // Add to campaign action
584
        if (!$file instanceof Folder) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
585
            $actions->push(PopoverField::create([
586
                FormAction::create(
587
                    'addtocampaign',
588
                    _t('CAMPAIGNS.ADDTOCAMPAIGN', 'Add to campaign')
589
                ),
590
            ])
591
                ->setPlacement('top')
592
            );
593
        }
594
595
        $form = Form::create(
596
            $this,
597
            'FileEditForm',
598
            $fields,
599
            $actions
600
        );
601
602
        // Load into form
603
        if($id && $file) {
604
            $form->loadDataFrom($file);
605
        }
606
607
        // Configure form to respond to validation errors with form schema
608
        // if requested via react.
609
        $form->setValidationResponseCallback(function() use ($form) {
610
            return $this->getSchemaResponse($form);
611
        });
612
613
        return $form;
614
    }
615
616
    /**
617
     * Get file edit form
618
     *
619
     * @return Form
620
     */
621
    public function FileEditForm()
622
    {
623
        // Get ID either from posted back value, or url parameter
624
        $request = $this->getRequest();
625
        $id = $request->param('ID') ?: $request->postVar('ID');
626
        return $this->getFileEditForm($id);
627
    }
628
629
    /**
630
     * @param array $data
631
     * @param Form $form
632
     * @return SS_HTTPResponse
633
     */
634
    public function save($data, $form)
635
    {
636 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...
637
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
638
                ->addHeader('Content-Type', 'application/json');
639
        }
640
641
        $id = (int) $data['ID'];
642
        $record = $this->getList()->filter('ID', $id)->first();
643
644 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...
645
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
646
                ->addHeader('Content-Type', 'application/json');
647
        }
648
649 View Code Duplication
        if (!$record->canEdit()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
650
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
651
                ->addHeader('Content-Type', 'application/json');
652
        }
653
654
        $form->saveInto($record);
655
        $record->write();
656
657
        // Return the record data in the same response as the schema to save a postback
658
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
659
        $schemaData['record'] = $this->getObjectFromData($record);
0 ignored issues
show
Documentation introduced by
$record is of type object<SilverStripe\ORM\DataObject>, but the function expects a object<SilverStripe\Assets\File>.

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...
660
        $response = new SS_HTTPResponse(Convert::raw2json($schemaData));
661
        $response->addHeader('Content-Type', 'application/json');
662
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $response; (SilverStripe\Control\SS_HTTPResponse) is incompatible with the return type of the parent method SilverStripe\Admin\LeftAndMain::save of type SS_HTTPResponse|null.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
663
    }
664
665
    /**
666
     * @param File $file
667
     *
668
     * @return array
669
     */
670
    protected function getObjectFromData(File $file)
671
    {
672
        $object = array(
673
            'id' => $file->ID,
674
            'created' => $file->Created,
675
            'lastUpdated' => $file->LastEdited,
676
            'owner' => null,
677
            'parent' => null,
678
            'title' => $file->Title,
679
            'exists' => $file->exists(), // Broken file check
680
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
682
            'name' => $file->Name,
683
            'filename' => $file->Filename,
684
            'extension' => $file->Extension,
685
            'size' => $file->Size,
686
            'url' => $file->AbsoluteURL,
687
            'canEdit' => $file->canEdit(),
688
            'canDelete' => $file->canDelete()
689
        );
690
691
        /** @var Member $owner */
692
        $owner = $file->Owner();
693
694
        if ($owner) {
695
            $object['owner'] = array(
696
                'id' => $owner->ID,
697
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
698
            );
699
        }
700
701
        /** @var Folder $parent */
702
        $parent = $file->Parent();
703
704
        if ($parent) {
705
            $object['parent'] = array(
706
                'id' => $parent->ID,
707
                'title' => $parent->Title,
708
                'filename' => $parent->Filename,
709
            );
710
        }
711
712
        /** @var File $file */
713
        if ($file->getIsImage()) {
714
            $object['dimensions']['width'] = $file->Width;
715
            $object['dimensions']['height'] = $file->Height;
716
        }
717
718
        return $object;
719
    }
720
721
722
    /**
723
     * Returns the files and subfolders contained in the currently selected folder,
724
     * defaulting to the root node. Doubles as search results, if any search parameters
725
     * are set through {@link SearchForm()}.
726
     *
727
     * @param array $params Unsanitised request parameters
728
     * @return DataList
729
     */
730
    protected function getList($params = array())
731
    {
732
        $context = $this->getSearchContext();
733
734
        // Overwrite name filter to search both Name and Title attributes
735
        $context->removeFilterByName('Name');
736
737
        // Lazy loaded list. Allows adding new filters through SearchContext.
738
        $list = $context->getResults($params);
739
740
        // Re-add previously removed "Name" filter as combined filter
741
        // TODO Replace with composite SearchFilter once that API exists
742
        if(!empty($params['Name'])) {
743
            $list = $list->filterAny(array(
744
                'Name:PartialMatch' => $params['Name'],
745
                'Title:PartialMatch' => $params['Name']
746
            ));
747
        }
748
749
        // Optionally limit search to a folder (non-recursive)
750
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
751
            $list = $list->filter('ParentID', $params['ParentID']);
752
        }
753
754
        // Date filtering
755 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...
756
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
757
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
758
        }
759 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...
760
            $toDate = new DateField(null, null, $params['CreatedTo']);
761
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
762
        }
763
764
        // Categories
765
        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...
766
            $extensions = File::config()->app_categories[$filters['AppCategory']];
767
            $list = $list->filter('Name:PartialMatch', $extensions);
768
        }
769
770
        // Sort folders first
771
        $list = $list->sort(
772
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
773
        );
774
775
        // Pagination
776
        if (isset($filters['page']) && isset($filters['limit'])) {
777
            $page = $filters['page'];
778
            $limit = $filters['limit'];
779
            $offset = ($page - 1) * $limit;
780
            $list = $list->limit($limit, $offset);
781
        }
782
783
        // Access checks
784
        $list = $list->filterByCallback(function(File $file) {
785
            return $file->canView();
786
        });
787
788
        return $list;
789
    }
790
791
    /**
792
     * Action handler for adding pages to a campaign
793
     *
794
     * @param array $data
795
     * @param Form $form
796
     * @return DBHTMLText|SS_HTTPResponse
797
     */
798
    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...
799
    {
800
        $id = $data['ID'];
801
        $record = $this->getList()->byID($id);
802
803
        $handler = AddToCampaignHandler::create($this, $record);
804
        $results = $handler->addToCampaign($record, $data['Campaign']);
0 ignored issues
show
Bug introduced by
It seems like $record defined by $this->getList()->byID($id) on line 801 can be null; however, SilverStripe\Admin\AddTo...andler::addToCampaign() 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...
805
        if (!is_null($results)) {
806
            $request = $this->getRequest();
807
            if($request->getHeader('X-Formschema-Request')) {
808
                $data = $this->getSchemaForForm($handler->Form($record));
0 ignored issues
show
Bug introduced by
It seems like $record defined by $this->getList()->byID($id) on line 801 can be null; however, SilverStripe\Admin\AddToCampaignHandler::Form() 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...
809
                $data['message'] = $results;
810
811
                $response = new SS_HTTPResponse(Convert::raw2json($data));
812
                $response->addHeader('Content-Type', 'application/json');
813
                return $response;
814
            }
815
            return $results;
816
        }
817
    }
818
819
    /**
820
     * Url handler for add to campaign form
821
     *
822
     * @param SS_HTTPRequest $request
823
     * @return Form
824
     */
825
    public function AddToCampaignForm($request)
826
    {
827
        // Get ID either from posted back value, or url parameter
828
        $id = $request->param('ID') ?: $request->postVar('ID');
829
        return $this->getAddToCampaignForm($id);
830
    }
831
832
    /**
833
     * @param int $id
834
     * @return Form
835
     */
836
    public function getAddToCampaignForm($id)
837
    {
838
        // Get record-specific fields
839
        $record = $this->getList()->byID($id);
840
841
        if (!$record) {
842
            $this->httpError(404, _t(
843
                'AssetAdmin.ErrorNotFound',
844
                'That {Type} couldn\'t be found',
845
                '',
846
                ['Type' => _t('File.SINGULARNAME')]
0 ignored issues
show
Documentation introduced by
array('Type' => _t('File.SINGULARNAME')) is of type array<string,string,{"Type":"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...
847
            ));
848
            return null;
849
        }
850
        if (!$record->canView()) {
851
            $this->httpError(403, _t(
852
                'AssetAdmin.ErrorItemPermissionDenied',
853
                'It seems you don\'t have the necessary permissions to add {ObjectTitle} to a campaign',
854
                '',
855
                ['ObjectTitle' => _t('File.SINGULARNAME')]
0 ignored issues
show
Documentation introduced by
array('ObjectTitle' => _t('File.SINGULARNAME')) is of type array<string,string,{"ObjectTitle":"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...
856
            ));
857
            return null;
858
        }
859
860
        $handler = AddToCampaignHandler::create($this, $record);
861
        return $handler->Form($record);
862
    }
863
864
    /**
865
     * @return Upload
866
     */
867
    protected function getUpload()
868
    {
869
        $upload = Upload::create();
870
        $upload->getValidator()->setAllowedExtensions(
871
            // filter out '' since this would be a regex problem on JS end
872
            array_filter(File::config()->get('allowed_extensions'))
873
        );
874
875
        return $upload;
876
    }
877
}
878