Completed
Pull Request — master (#223)
by Ingo
02:08
created

AssetAdmin   F

Complexity

Total Complexity 95

Size/Duplication

Total Lines 865
Duplicated Lines 4.86 %

Coupling/Cohesion

Components 1
Dependencies 30

Importance

Changes 29
Bugs 7 Features 7
Metric Value
wmc 95
c 29
b 7
f 7
lcom 1
cbo 30
dl 42
loc 865
rs 1.0434

24 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 10 1
B getClientConfig() 0 46 1
C apiReadFolder() 0 64 12
A apiSearch() 0 17 1
C apiDelete() 11 38 8
C apiCreateFile() 12 53 11
C apiCreateFolder() 7 60 14
A legacyRedirectForEditView() 0 7 2
A getFileEditLink() 0 12 3
B getSearchContext() 0 41 1
A getNameGenerator() 0 5 1
A breadcrumbs() 0 4 1
A baseCSSClasses() 0 4 1
A providePermissions() 0 11 1
A getFolderEditForm() 0 11 1
B getFileEditForm() 0 68 4
A FileEditForm() 0 7 2
B save() 12 30 5
B getObjectFromData() 0 50 6
C getList() 0 60 10
A addtocampaign() 0 21 3
A AddToCampaignForm() 0 6 2
B getAddToCampaignForm() 0 28 3
A getUpload() 0 10 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AssetAdmin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AssetAdmin, and based on these observations, apply Extract Interface, too.

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\Filesystem\Storage\AssetNameGenerator;
9
use SilverStripe\ORM\ArrayList;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\FieldType\DBHTMLText;
13
use SilverStripe\Security\Member;
14
use SilverStripe\Security\PermissionProvider;
15
use SilverStripe\Security\SecurityToken;
16
use SearchContext;
17
use DateField;
18
use DropdownField;
19
use Controller;
20
use FieldList;
21
use Form;
22
use CheckboxField;
23
use File;
24
use Requirements;
25
use Injector;
26
use Folder;
27
use HeaderField;
28
use FieldGroup;
29
use SS_HTTPRequest;
30
use SS_HTTPResponse;
31
use Upload;
32
use Config;
33
use FormAction;
34
use TextField;
35
use HiddenField;
36
use ReadonlyField;
37
use LiteralField;
38
use PopoverField;
39
use HTMLReadonlyField;
40
use DateField_Disabled;
41
use Convert;
42
43
/**
44
 * AssetAdmin is the 'file store' section of the CMS.
45
 * It provides an interface for manipulating the File and Folder objects in the system.
46
 *
47
 * @package cms
48
 * @subpackage assets
49
 */
50
class AssetAdmin extends LeftAndMain implements PermissionProvider
51
{
52
    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...
53
54
    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...
55
56
    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...
57
58
    private static $tree_class = 'Folder';
0 ignored issues
show
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...
59
60
    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...
61
        // Legacy redirect for SS3-style detail view
62
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
63
        // Pass all URLs to the index, for React to unpack
64
        'show/$FolderID/edit/$FileID' => 'index',
65
        // API access points with structured data
66
        'POST api/createFolder' => 'apiCreateFolder',
67
        'POST api/createFile' => 'apiCreateFile',
68
        'GET api/readFolder' => 'apiReadFolder',
69
        'PUT api/updateFolder' => 'apiUpdateFolder',
70
        'DELETE api/delete' => 'apiDelete',
71
        'GET api/search' => 'apiSearch',
72
    ];
73
74
    /**
75
     * Amount of results showing on a single page.
76
     *
77
     * @config
78
     * @var int
79
     */
80
    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...
81
82
    /**
83
     * @config
84
     * @see Upload->allowedMaxFileSize
85
     * @var int
86
     */
87
    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...
88
89
    /**
90
     * @var array
91
     */
92
    private static $allowed_actions = array(
0 ignored issues
show
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

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

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

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

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

Loading history...
351
            $result = ['error' => 'unknown'];
352
            return (new SS_HTTPResponse(json_encode($result), 400))
353
                ->addHeader('Content-Type', 'application/json');
354
        }
355
356
        $file->ParentID = $parentRecord->ID;
357
        $file->write();
358
359
        $result = [$this->getObjectFromData($file)];
360
361
        return (new SS_HTTPResponse(json_encode($result)))
362
            ->addHeader('Content-Type', 'application/json');
363
    }
364
365
    /**
366
     * Creates a single folder, within an optional parent folder.
367
     *
368
     * @param SS_HTTPRequest $request
369
     * @return SS_HTTPRequest|SS_HTTPResponse
370
     */
371
    public function apiCreateFolder(SS_HTTPRequest $request)
372
    {
373
        $data = $request->postVars();
374
375
        $class = 'Folder';
376
377
        // CSRF check
378
        $token = SecurityToken::inst();
379 View Code Duplication
        if (empty($data[$token->getName()]) || !$token->check($data[$token->getName()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
380
            return new SS_HTTPResponse(null, 400);
381
        }
382
383
        // check addchildren permissions
384
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
385
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
386 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...
387
                return (new SS_HTTPResponse(null, 403))
388
                    ->addHeader('Content-Type', 'application/json');
389
            }
390
        } else {
391
            $parentRecord = singleton($class);
392
        }
393
        $data['ParentID'] = ($parentRecord->exists()) ? (int)$parentRecord->ID : 0;
394
395
        // check create permissions
396
        if (!$parentRecord->canCreate()) {
397
            return (new SS_HTTPResponse(null, 403))
398
                ->addHeader('Content-Type', 'application/json');
399
        }
400
401
        // Build filename
402
        $baseFilename = isset($data['Name'])
403
            ? basename($data['Name'])
404
            : _t('AssetAdmin.NEWFOLDER', "NewFolder");
405
406
        if ($parentRecord && $parentRecord->ID) {
407
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
408
        }
409
410
        // Ensure name is unique
411
        $nameGenerator = $this->getNameGenerator($baseFilename);
412
        $filename = null;
413
        foreach ($nameGenerator as $filename) {
414
            if (! File::find($filename)) {
415
                break;
416
            }
417
        }
418
        $data['Name'] = basename($filename);
419
420
        // Create record
421
        /** @var Folder $record */
422
        $record = Injector::inst()->create($class);
423
        $record->ParentID = $data['ParentID'];
424
        $record->Name = $record->Title = basename($data['Name']);
425
        $record->write();
426
427
        $result = $this->getObjectFromData($record);
428
429
        return (new SS_HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
430
    }
431
432
    /**
433
     * Redirects 3.x style detail links to new 4.x style routing.
434
     *
435
     * @param SS_HTTPRequest $request
436
     */
437
    public function legacyRedirectForEditView($request)
438
    {
439
        $fileID = $request->param('FileID');
440
        $file = File::get()->byID($fileID);
441
        $link = $this->getFileEditLink($file) ?: $this->Link();
442
        $this->redirect($link);
443
    }
444
445
    /**
446
     * Given a file return the CMS link to edit it
447
     *
448
     * @param File $file
449
     * @return string
450
     */
451
    public function getFileEditLink($file) {
452
        if(!$file || !$file->isInDB()) {
453
            return null;
454
        }
455
456
        return Controller::join_links(
457
            $this->Link('show'),
458
            $file->ParentID,
459
            'edit',
460
            $file->ID
461
        );
462
    }
463
464
    /**
465
     * Get the search context from {@link File}, used to create the search form
466
     * as well as power the /search API endpoint.
467
     *
468
     * @return SearchContext
469
     */
470
    public function getSearchContext()
471
    {
472
        $context = File::singleton()->getDefaultSearchContext();
473
474
        // Customize fields
475
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
476
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
477
        ->setConfig('showcalendar', true);
478
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
479
        ->setConfig('showcalendar', true);
480
        $dateGroup = FieldGroup::create(
481
            $dateHeader,
482
            $dateFrom,
483
            $dateTo
484
        );
485
        $context->addField($dateGroup);
486
        $appCategories = array(
487
            'archive' => _t('AssetAdmin.AppCategoryArchive', 'Archive', 'A collection of files'),
488
            'audio' => _t('AssetAdmin.AppCategoryAudio', 'Audio'),
489
            'document' => _t('AssetAdmin.AppCategoryDocument', 'Document'),
490
            'flash' => _t('AssetAdmin.AppCategoryFlash', 'Flash', 'The fileformat'),
491
            'image' => _t('AssetAdmin.AppCategoryImage', 'Image'),
492
            'video' => _t('AssetAdmin.AppCategoryVideo', 'Video'),
493
        );
494
        $context->addField(
495
            $typeDropdown = new DropdownField(
496
                'AppCategory',
497
                _t('AssetAdmin.Filetype', 'File type'),
498
                $appCategories
499
            )
500
        );
501
502
        $typeDropdown->setEmptyString(' ');
503
504
        $context->addField(
505
            new CheckboxField('CurrentFolderOnly', _t('AssetAdmin.CurrentFolderOnly', 'Limit to current folder?'))
506
        );
507
        $context->getFields()->removeByName('Title');
508
509
        return $context;
510
    }
511
512
    /**
513
     * Get an asset renamer for the given filename.
514
     *
515
     * @param  string             $filename Path name
516
     * @return AssetNameGenerator
517
     */
518
    protected function getNameGenerator($filename)
519
    {
520
        return Injector::inst()
521
            ->createWithArgs('AssetNameGenerator', array($filename));
522
    }
523
524
    /**
525
     * @todo Implement on client
526
     *
527
     * @param bool $unlinked
528
     * @return ArrayList
529
     */
530
    public function breadcrumbs($unlinked = false)
531
    {
532
        return null;
533
    }
534
535
536
    /**
537
     * Don't include class namespace in auto-generated CSS class
538
     */
539
    public function baseCSSClasses()
540
    {
541
        return 'AssetAdmin LeftAndMain';
542
    }
543
544
    public function providePermissions()
545
    {
546
        return array(
547
            "CMS_ACCESS_AssetAdmin" => array(
548
                '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,?,{"title":"?"}>, 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...
549
                    'title' => static::menu_title()
550
                )),
551
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
552
            )
553
        );
554
    }
555
556
    /**
557
     * The form is used to generate a form schema,
558
     * as well as an intermediary object to process data through API endpoints.
559
     * Since it's used directly on API endpoints, it does not have any form actions.
560
     *
561
     * @param Folder $folder
562
     * @return Form
563
     */
564
    public function getFolderEditForm(Folder $folder)
565
    {
566
        $form = Form::create(
567
            $this,
568
            'FolderEditForm',
569
            $folder->getCMSFields(),
570
            FieldList::create()
571
        );
572
573
        return $form;
574
    }
575
576
    /**
577
     * See {@link getFolderEditForm()} for details.
578
     *
579
     * @param int $id
580
     * @return Form
581
     */
582
    public function getFileEditForm($id)
583
    {
584
        /** @var File $file */
585
        $file = $this->getList()->byID($id);
586
587
        // TODO use $file->getCMSFields()
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
588
        $fields = FieldList::create([
589
            HeaderField::create('TitleHeader', $file->Title, 1),
590
            LiteralField::create("ImageFull", $file->PreviewThumbnail()),
591
            TextField::create("Title", _t('AssetTableField.TITLE','Title')),
592
            TextField::create("Name", _t('AssetTableField.FILENAME','Filename')),
593
            ReadonlyField::create(
594
                'Location',
595
                _t('AssetTableField.FOLDER', 'Folder'),
596
                dirname($file->getSourceURL())
597
            ),
598
            HiddenField::create('ID', $id),
599
        ]);
600
        if ($file->getIsImage()) {
601
            $fields->push(ReadonlyField::create(
602
                "DisplaySize",
603
                "Size",
604
                sprintf('%spx, %s', $file->getDimensions(), $file->getSize()))
605
            );
606
            $fields->push(HTMLReadonlyField::create(
607
                'ClickableURL',
608
                _t('AssetTableField.URL','URL'),
609
                sprintf('<a href="%s" target="_blank">%s</a>', $file->Link(), $file->Link())
610
            ));
611
            $fields->push(DateField_Disabled::create(
612
                "LastEdited",
613
                _t('AssetTableField.LASTEDIT','Last changed')
614
            ));
615
        }
616
617
        $actions = FieldList::create([
618
            FormAction::create('save', _t('CMSMain.SAVE', 'Save'))
619
                ->setIcon('save'),
620
            PopoverField::create([
621
                FormAction::create(
622
                    'addtocampaign',
623
                    _t('CAMPAIGNS.ADDTOCAMPAIGN',
624
                    'Add to campaign')
625
                ),
626
            ])
627
                ->setPlacement('top'),
628
        ]);
629
630
        $form = Form::create(
631
            $this,
632
            'FileEditForm',
633
            $fields,
634
            $actions
635
        );
636
637
        // Load into form
638
        if($id && $file) {
639
            $form->loadDataFrom($file);
640
        }
641
642
        // Configure form to respond to validation errors with form schema
643
        // if requested via react.
644
        $form->setValidationResponseCallback(function() use ($form) {
645
            return $this->getSchemaResponse($form);
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 SS_HTTPResponse
668
     */
669
    public function save($data, $form)
670
    {
671 View Code Duplication
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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