Completed
Pull Request — master (#280)
by Damian
02:12
created

AssetAdmin::saveOrPublish()   C

Complexity

Conditions 8
Paths 5

Size

Total Lines 35
Code Lines 21

Duplication

Lines 12
Ratio 34.29 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 12
loc 35
rs 5.3846
cc 8
eloc 21
nc 5
nop 3
1
<?php
2
3
namespace SilverStripe\AssetAdmin\Controller;
4
5
use SilverStripe\Admin\AddToCampaignHandler;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\LeftAndMain;
8
use SilverStripe\Assets\File;
9
use SilverStripe\Assets\Folder;
10
use SilverStripe\Assets\Storage\AssetNameGenerator;
11
use SilverStripe\Assets\Upload;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\HTTPRequest;
14
use SilverStripe\Control\HTTPResponse;
15
use SilverStripe\Core\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 HTTPRequest $request
165
     * @return HTTPResponse
166
     */
167
    public function apiReadFolder(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
                'filename' => $next->getFilename(),
208
            ]);
209
            if($next->ParentID) {
210
                $next = $next->Parent();
211
            } else {
212
                break;
213
            }
214
        }
215
216
        // Build response
217
        $response = new HTTPResponse();
218
        $response->addHeader('Content-Type', 'application/json');
219
        $response->setBody(json_encode([
220
            'files' => $items,
221
            'title' => $folder->getTitle(),
222
            'count' => count($items),
223
            'parents' => $parents,
224
            'parent' => $parents ? $parents[count($parents) - 1] : null,
225
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
226
            'folderID' => $folderID,
227
            'canEdit' => $folder->canEdit(),
228
            'canDelete' => $folder->canDelete(),
229
        ]));
230
231
        return $response;
232
    }
233
234
    /**
235
     * @param HTTPRequest $request
236
     *
237
     * @return HTTPResponse
238
     */
239
    public function apiSearch(HTTPRequest $request)
240
    {
241
        $params = $request->getVars();
242
        $list = $this->getList($params);
243
244
        $response = new HTTPResponse();
245
        $response->addHeader('Content-Type', 'application/json');
246
        $response->setBody(json_encode([
247
            // Serialisation
248
            "files" => array_map(function($file) {
249
                return $this->getObjectFromData($file);
250
            }, $list->toArray()),
251
            "count" => $list->count(),
252
        ]));
253
254
        return $response;
255
    }
256
257
    /**
258
     * @param HTTPRequest $request
259
     *
260
     * @return HTTPResponse
261
     */
262
    public function apiDelete(HTTPRequest $request)
263
    {
264
        parse_str($request->getBody(), $vars);
265
266
        // CSRF check
267
        $token = SecurityToken::inst();
268 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...
269
            return new HTTPResponse(null, 400);
270
        }
271
272 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...
273
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
274
                ->addHeader('Content-Type', 'application/json');
275
        }
276
277
        $fileIds = $vars['ids'];
278
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
279
280 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...
281
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
282
                ->addHeader('Content-Type', 'application/json');
283
        }
284
285
        if (!min(array_map(function (File $file) {
286
            return $file->canDelete();
287
        }, $files))) {
288
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
289
                ->addHeader('Content-Type', 'application/json');
290
        }
291
292
        /** @var File $file */
293
        foreach ($files as $file) {
294
            $file->delete();
295
        }
296
297
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
298
            ->addHeader('Content-Type', 'application/json');
299
    }
300
301
    /**
302
     * Creates a single file based on a form-urlencoded upload.
303
     *
304
     * @param HTTPRequest $request
305
     * @return HTTPRequest|HTTPResponse
306
     */
307
    public function apiCreateFile(HTTPRequest $request)
308
    {
309
        $data = $request->postVars();
310
        $upload = $this->getUpload();
311
312
        // CSRF check
313
        $token = SecurityToken::inst();
314 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...
315
            return new HTTPResponse(null, 400);
316
        }
317
318
        // Check parent record
319
        /** @var Folder $parentRecord */
320
        $parentRecord = null;
321
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
322
            $parentRecord = Folder::get()->byID($data['ParentID']);
323
        }
324
        $data['Parent'] = $parentRecord;
325
326
        $tmpFile = $request->postVar('Upload');
327 View Code Duplication
        if(!$upload->validate($tmpFile)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
328
            $result = ['error' => $upload->getErrors()];
329
            return (new HTTPResponse(json_encode($result), 400))
330
                ->addHeader('Content-Type', 'application/json');
331
        }
332
333
        // TODO Allow batch uploads
334
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
335
        /** @var File $file */
336
        $file = Injector::inst()->create($fileClass);
337
338
        // check canCreate permissions
339 View Code Duplication
        if (!$file->canCreate(null, $data)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
340
            return (new HTTPResponse(json_encode(['status' => 'error']), 403))
341
                ->addHeader('Content-Type', 'application/json');
342
        }
343
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 HTTPResponse(json_encode($result), 400))
348
                ->addHeader('Content-Type', 'application/json');
349
        }
350
351
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
352
        $file->write();
353
354
        $result = [$this->getObjectFromData($file)];
355
356
        return (new 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 HTTPRequest $request
364
     * @return HTTPRequest|HTTPResponse
365
     */
366
    public function apiCreateFolder(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 HTTPResponse(null, 400);
376
        }
377
378
        // check addchildren permissions
379
        /** @var Folder $parentRecord */
380
        $parentRecord = null;
381
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
382
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
383
        }
384
        $data['Parent'] = $parentRecord;
385
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
386
387
        // Build filename
388
        $baseFilename = isset($data['Name'])
389
            ? basename($data['Name'])
390
            : _t('AssetAdmin.NEWFOLDER', "NewFolder");
391
392
        if ($parentRecord && $parentRecord->ID) {
393
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
394
        }
395
396
        // Ensure name is unique
397
        $nameGenerator = $this->getNameGenerator($baseFilename);
398
        $filename = null;
399
        foreach ($nameGenerator as $filename) {
400
            if (! File::find($filename)) {
401
                break;
402
            }
403
        }
404
        $data['Name'] = basename($filename);
405
406
        // Create record
407
        /** @var Folder $record */
408
        $record = Injector::inst()->create($class);
409
410
        // check create permissions
411
        if (!$record->canCreate(null, $data)) {
412
            return (new HTTPResponse(null, 403))
413
                ->addHeader('Content-Type', 'application/json');
414
        }
415
416
        $record->ParentID = $data['ParentID'];
417
        $record->Name = $record->Title = basename($data['Name']);
418
        $record->write();
419
420
        $result = $this->getObjectFromData($record);
421
422
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
423
    }
424
425
    /**
426
     * Redirects 3.x style detail links to new 4.x style routing.
427
     *
428
     * @param HTTPRequest $request
429
     */
430
    public function legacyRedirectForEditView($request)
431
    {
432
        $fileID = $request->param('FileID');
433
        /** @var File $file */
434
        $file = File::get()->byID($fileID);
435
        $link = $this->getFileEditLink($file) ?: $this->Link();
436
        $this->redirect($link);
437
    }
438
439
    /**
440
     * Given a file return the CMS link to edit it
441
     *
442
     * @param File $file
443
     * @return string
444
     */
445
    public function getFileEditLink($file) {
446
        if(!$file || !$file->isInDB()) {
447
            return null;
448
        }
449
450
        return Controller::join_links(
451
            $this->Link('show'),
452
            $file->ParentID,
453
            'edit',
454
            $file->ID
455
        );
456
    }
457
458
    /**
459
     * Get the search context from {@link File}, used to create the search form
460
     * as well as power the /search API endpoint.
461
     *
462
     * @return SearchContext
463
     */
464
    public function getSearchContext()
465
    {
466
        $context = File::singleton()->getDefaultSearchContext();
467
468
        // Customize fields
469
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
470
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
471
        ->setConfig('showcalendar', true);
472
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
473
        ->setConfig('showcalendar', true);
474
        $dateGroup = FieldGroup::create(
475
            $dateHeader,
476
            $dateFrom,
477
            $dateTo
478
        );
479
        $context->addField($dateGroup);
480
        /** @skipUpgrade */
481
        $appCategories = array(
482
            'archive' => _t('AssetAdmin.AppCategoryArchive', 'Archive', 'A collection of files'),
483
            'audio' => _t('AssetAdmin.AppCategoryAudio', 'Audio'),
484
            'document' => _t('AssetAdmin.AppCategoryDocument', 'Document'),
485
            'flash' => _t('AssetAdmin.AppCategoryFlash', 'Flash', 'The fileformat'),
486
            'image' => _t('AssetAdmin.AppCategoryImage', 'Image'),
487
            'video' => _t('AssetAdmin.AppCategoryVideo', 'Video'),
488
        );
489
        $context->addField(
490
            $typeDropdown = new DropdownField(
491
                'AppCategory',
492
                _t('AssetAdmin.Filetype', 'File type'),
493
                $appCategories
494
            )
495
        );
496
497
        $typeDropdown->setEmptyString(' ');
498
499
        $context->addField(
500
            new CheckboxField('CurrentFolderOnly', _t('AssetAdmin.CurrentFolderOnly', 'Limit to current folder?'))
501
        );
502
        $context->getFields()->removeByName('Title');
503
504
        return $context;
505
    }
506
507
    /**
508
     * Get an asset renamer for the given filename.
509
     *
510
     * @param  string             $filename Path name
511
     * @return AssetNameGenerator
512
     */
513
    protected function getNameGenerator($filename)
514
    {
515
        return Injector::inst()
516
            ->createWithArgs('AssetNameGenerator', array($filename));
517
    }
518
519
    /**
520
     * @todo Implement on client
521
     *
522
     * @param bool $unlinked
523
     * @return ArrayList
524
     */
525
    public function breadcrumbs($unlinked = false)
526
    {
527
        return null;
528
    }
529
530
531
    /**
532
     * Don't include class namespace in auto-generated CSS class
533
     */
534
    public function baseCSSClasses()
535
    {
536
        return 'AssetAdmin LeftAndMain';
537
    }
538
539
    public function providePermissions()
540
    {
541
        return array(
542
            "CMS_ACCESS_AssetAdmin" => array(
543
                '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...
544
                    'title' => static::menu_title()
545
                )),
546
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
547
            )
548
        );
549
    }
550
551
    /**
552
     * The form is used to generate a form schema,
553
     * as well as an intermediary object to process data through API endpoints.
554
     * Since it's used directly on API endpoints, it does not have any form actions.
555
     * It handles both {@link File} and {@link Folder} records.
556
     *
557
     * @param int $id
558
     * @return Form
559
     */
560
    public function getFileEditForm($id)
561
    {
562
        /** @var File $file */
563
        $file = $this->getList()->byID($id);
564
565
        $fields = $file->getCMSFields();
566
567
        $actions = $this->getFileEditActions($file);
568
569
        $form = Form::create(
570
            $this,
571
            'FileEditForm',
572
            $fields,
573
            $actions
574
        );
575
576
        // Load into form
577
        if($id && $file) {
578
            $form->loadDataFrom($file);
579
        }
580
581
        // Configure form to respond to validation errors with form schema
582
        // if requested via react.
583
        $form->setValidationResponseCallback(function() use ($form) {
584
            return $this->getSchemaResponse($form);
585
        });
586
587
        return $form;
588
    }
589
590
    /**
591
     * Get file edit form
592
     *
593
     * @return Form
594
     */
595
    public function FileEditForm()
596
    {
597
        // Get ID either from posted back value, or url parameter
598
        $request = $this->getRequest();
599
        $id = $request->param('ID') ?: $request->postVar('ID');
600
        return $this->getFileEditForm($id);
601
    }
602
603
    /**
604
     * @param array $data
605
     * @param Form $form
606
     * @return HTTPResponse
607
     */
608
    public function save($data, $form)
609
    {
610
        return $this->saveOrPublish($data, $form, false);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->saveOrPublish($data, $form, false); (SilverStripe\Control\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...
611
    }
612
613
614
    /**
615
     * @param array $data
616
     * @param Form $form
617
     * @return HTTPResponse
618
     */
619
    public function publish($data, $form) {
620
        return $this->saveOrPublish($data, $form, true);
621
    }
622
623
    /**
624
     * Update thisrecord
625
     *
626
     * @param array $data
627
     * @param Form $form
628
     * @param bool $doPublish
629
     * @return HTTPResponse
630
     */
631
    protected function saveOrPublish($data, $form, $doPublish = false) {
632 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...
633
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
634
                ->addHeader('Content-Type', 'application/json');
635
        }
636
637
        $id = (int) $data['ID'];
638
        /** @var File $record */
639
        $record = $this->getList()->filter('ID', $id)->first();
640
641 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...
642
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
643
                ->addHeader('Content-Type', 'application/json');
644
        }
645
646 View Code Duplication
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
647
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
648
                ->addHeader('Content-Type', 'application/json');
649
        }
650
651
        $form->saveInto($record);
652
        $record->write();
653
654
        // Publish this record and owned objects
655
        if ($doPublish) {
656
            $record->publishRecursive();
657
        }
658
659
        // Return the record data in the same response as the schema to save a postback
660
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
661
        $schemaData['record'] = $this->getObjectFromData($record);
662
        $response = new HTTPResponse(Convert::raw2json($schemaData));
663
        $response->addHeader('Content-Type', 'application/json');
664
        return $response;
665
    }
666
667
    public function unpublish($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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

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

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

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

Loading history...
942
        $deleteAction->setIcon('trash-bin');
943
944
        // Add file-specific actions
945
        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...
946
            // Add to campaign action
947
            $addToCampaignAction = FormAction::create(
948
                'addtocampaign',
949
                _t('CAMPAIGNS.ADDTOCAMPAIGN', 'Add to campaign')
950
            );
951
            $popoverActions = [
952
                $addToCampaignAction
953
            ];
954
            // Add unpublish if available
955
            if ($file->isPublished() && $file->canUnpublish()) {
956
                $unpublishText = _t(
957
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNPUBLISH_BUTTON',
958
                    'Unpublish'
959
                );
960
                $unpublishAction = FormAction::create('unpublish', $unpublishText);
961
                $unpublishAction->setIcon('trash-bin');
962
                $popoverActions[] = $unpublishAction;
963
            }
964
            // Delete
965
            $popoverActions[] = $deleteAction;
966
967
            // Build popover menu
968
            $popoverField = PopoverField::create($popoverActions);
969
            $popoverField->setPlacement('top');
970
            $actions->push($popoverField);
971
        } else {
972
            $actions->push($deleteAction);
973
        }
974
975
        $this->extend('updateCMSActions', $actions);
976
        return $actions;
977
    }
978
}
979