Completed
Pull Request — master (#289)
by Damian
02:13
created

AssetAdmin::addToCampaignForm()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 2
eloc 3
nc 2
nop 1
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\Config\Config;
16
use SilverStripe\Core\Convert;
17
use SilverStripe\Core\Injector\Injector;
18
use SilverStripe\Forms\CheckboxField;
19
use SilverStripe\Forms\DateField;
20
use SilverStripe\Forms\DropdownField;
21
use SilverStripe\Forms\FieldGroup;
22
use SilverStripe\Forms\FieldList;
23
use SilverStripe\Forms\Form;
24
use SilverStripe\Forms\FormAction;
25
use SilverStripe\Forms\HeaderField;
26
use SilverStripe\Forms\PopoverField;
27
use SilverStripe\ORM\ArrayList;
28
use SilverStripe\ORM\DataList;
29
use SilverStripe\ORM\DataObject;
30
use SilverStripe\ORM\FieldType\DBHTMLText;
31
use SilverStripe\ORM\Search\SearchContext;
32
use SilverStripe\Security\Member;
33
use SilverStripe\Security\PermissionProvider;
34
use SilverStripe\Security\SecurityToken;
35
use SilverStripe\View\Requirements;
36
37
/**
38
 * AssetAdmin is the 'file store' section of the CMS.
39
 * It provides an interface for manipulating the File and Folder objects in the system.
40
 */
41
class AssetAdmin extends LeftAndMain implements PermissionProvider
42
{
43
    private static $url_segment = 'assets';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_segment is not used and could be removed.

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

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

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

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

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

Loading history...
48
49
    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...
50
51
    private static $url_handlers = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_handlers is not used and could be removed.

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

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

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

Loading history...
96
97
    private static $thumbnail_width = 400;
0 ignored issues
show
Unused Code introduced by
The property $thumbnail_width is not used and could be removed.

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

Loading history...
98
99
    private static $thumbnail_height = 300;
0 ignored issues
show
Unused Code introduced by
The property $thumbnail_height is not used and could be removed.

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

Loading history...
100
101
    /**
102
     * Set up the controller
103
     */
104
    public function init()
105
    {
106
        parent::init();
107
108
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
109
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
110
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
111
112
        CMSBatchActionHandler::register(
113
            'delete',
114
            'SilverStripe\AssetAdmin\BatchAction\DeleteAssets',
115
            'SilverStripe\\Assets\\Folder'
116
        );
117
    }
118
119
    public function getClientConfig()
120
    {
121
        $baseLink = $this->Link();
122
        return array_merge(parent::getClientConfig(), [
123
            'reactRouter' => true,
124
            'createFileEndpoint' => [
125
                'url' => Controller::join_links($baseLink, 'api/createFile'),
126
                'method' => 'post',
127
                'payloadFormat' => 'urlencoded',
128
            ],
129
            'createFolderEndpoint' => [
130
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
131
                'method' => 'post',
132
                'payloadFormat' => 'urlencoded',
133
            ],
134
            'readFolderEndpoint' => [
135
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
136
                'method' => 'get',
137
                'responseFormat' => 'json',
138
            ],
139
            'searchEndpoint' => [
140
                'url' => Controller::join_links($baseLink, 'api/search'),
141
                'method' => 'get',
142
                'responseFormat' => 'json',
143
            ],
144
            'updateFolderEndpoint' => [
145
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
146
                'method' => 'put',
147
                'payloadFormat' => 'urlencoded',
148
            ],
149
            'deleteEndpoint' => [
150
                'url' => Controller::join_links($baseLink, 'api/delete'),
151
                'method' => 'delete',
152
                'payloadFormat' => 'urlencoded',
153
            ],
154
            'limit' => $this->config()->page_length,
155
            'form' => [
156
                'fileEditForm' => [
157
                    'schemaUrl' => $this->Link('schema/fileEditForm')
158
                ],
159
                'addToCampaignForm' => [
160
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
161
                ],
162
            ],
163
        ]);
164
    }
165
166
    /**
167
     * Fetches a collection of files by ParentID.
168
     *
169
     * @param HTTPRequest $request
170
     * @return HTTPResponse
171
     */
172
    public function apiReadFolder(HTTPRequest $request)
173
    {
174
        $params = $request->requestVars();
175
        $items = array();
176
        $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...
177
        $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...
178
179
        if (!isset($params['id']) && !strlen($params['id'])) {
180
            $this->httpError(400);
181
        }
182
183
        $folderID = (int)$params['id'];
184
        /** @var Folder $folder */
185
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
186
187
        if (!$folder) {
188
            $this->httpError(400);
189
        }
190
191
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
192
        $files = $this->getList()->filter('ParentID', $folderID);
193
194
        if ($files) {
195
            /** @var File $file */
196
            foreach ($files as $file) {
197
                if (!$file->canView()) {
198
                    continue;
199
                }
200
201
                $items[] = $this->getObjectFromData($file);
202
            }
203
        }
204
205
        // Build parents (for breadcrumbs)
206
        $parents = [];
207
        $next = $folder->Parent();
208
        while ($next && $next->exists()) {
209
            array_unshift($parents, [
210
                'id' => $next->ID,
211
                'title' => $next->getTitle(),
212
                'filename' => $next->getFilename(),
213
            ]);
214
            if ($next->ParentID) {
215
                $next = $next->Parent();
216
            } else {
217
                break;
218
            }
219
        }
220
221
        // Build response
222
        $response = new 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
            'parent' => $parents ? $parents[count($parents) - 1] : null,
230
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
231
            'folderID' => $folderID,
232
            'canEdit' => $folder->canEdit(),
233
            'canDelete' => $folder->canDelete(),
234
        ]));
235
236
        return $response;
237
    }
238
239
    /**
240
     * @param HTTPRequest $request
241
     *
242
     * @return HTTPResponse
243
     */
244
    public function apiSearch(HTTPRequest $request)
245
    {
246
        $params = $request->getVars();
247
        $list = $this->getList($params);
248
249
        $response = new HTTPResponse();
250
        $response->addHeader('Content-Type', 'application/json');
251
        $response->setBody(json_encode([
252
            // Serialisation
253
            "files" => array_map(function ($file) {
254
                return $this->getObjectFromData($file);
255
            }, $list->toArray()),
256
            "count" => $list->count(),
257
        ]));
258
259
        return $response;
260
    }
261
262
    /**
263
     * @param HTTPRequest $request
264
     *
265
     * @return HTTPResponse
266
     */
267
    public function apiDelete(HTTPRequest $request)
268
    {
269
        parse_str($request->getBody(), $vars);
270
271
        // CSRF check
272
        $token = SecurityToken::inst();
273 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...
274
            return new HTTPResponse(null, 400);
275
        }
276
277 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...
278
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
279
                ->addHeader('Content-Type', 'application/json');
280
        }
281
282
        $fileIds = $vars['ids'];
283
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
284
285 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...
286
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
287
                ->addHeader('Content-Type', 'application/json');
288
        }
289
290
        if (!min(array_map(function (File $file) {
291
            return $file->canDelete();
292
        }, $files))) {
293
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
294
                ->addHeader('Content-Type', 'application/json');
295
        }
296
297
        /** @var File $file */
298
        foreach ($files as $file) {
299
            $file->delete();
300
        }
301
302
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
303
            ->addHeader('Content-Type', 'application/json');
304
    }
305
306
    /**
307
     * Creates a single file based on a form-urlencoded upload.
308
     *
309
     * @param HTTPRequest $request
310
     * @return HTTPRequest|HTTPResponse
311
     */
312
    public function apiCreateFile(HTTPRequest $request)
313
    {
314
        $data = $request->postVars();
315
        $upload = $this->getUpload();
316
317
        // CSRF check
318
        $token = SecurityToken::inst();
319 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...
320
            return new HTTPResponse(null, 400);
321
        }
322
323
        // Check parent record
324
        /** @var Folder $parentRecord */
325
        $parentRecord = null;
326
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
327
            $parentRecord = Folder::get()->byID($data['ParentID']);
328
        }
329
        $data['Parent'] = $parentRecord;
330
331
        $tmpFile = $request->postVar('Upload');
332 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...
333
            $result = ['error' => $upload->getErrors()];
334
            return (new HTTPResponse(json_encode($result), 400))
335
                ->addHeader('Content-Type', 'application/json');
336
        }
337
338
        // TODO Allow batch uploads
339
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
340
        /** @var File $file */
341
        $file = Injector::inst()->create($fileClass);
342
343
        // check canCreate permissions
344 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...
345
            return (new HTTPResponse(json_encode(['status' => 'error']), 403))
346
                ->addHeader('Content-Type', 'application/json');
347
        }
348
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 HTTPResponse(json_encode($result), 400))
353
                ->addHeader('Content-Type', 'application/json');
354
        }
355
356
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
357
        $file->write();
358
359
        $result = [$this->getObjectFromData($file)];
360
361
        return (new 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 HTTPRequest $request
369
     * @return HTTPRequest|HTTPResponse
370
     */
371
    public function apiCreateFolder(HTTPRequest $request)
372
    {
373
        $data = $request->postVars();
374
375
        $class = 'SilverStripe\\Assets\\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 HTTPResponse(null, 400);
381
        }
382
383
        // check addchildren permissions
384
        /** @var Folder $parentRecord */
385
        $parentRecord = null;
386
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
387
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
388
        }
389
        $data['Parent'] = $parentRecord;
390
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
391
392
        // Build filename
393
        $baseFilename = isset($data['Name'])
394
            ? basename($data['Name'])
395
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
396
397
        if ($parentRecord && $parentRecord->ID) {
398
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
399
        }
400
401
        // Ensure name is unique
402
        $nameGenerator = $this->getNameGenerator($baseFilename);
403
        $filename = null;
404
        foreach ($nameGenerator as $filename) {
405
            if (! File::find($filename)) {
406
                break;
407
            }
408
        }
409
        $data['Name'] = basename($filename);
410
411
        // Create record
412
        /** @var Folder $record */
413
        $record = Injector::inst()->create($class);
414
415
        // check create permissions
416
        if (!$record->canCreate(null, $data)) {
417
            return (new HTTPResponse(null, 403))
418
                ->addHeader('Content-Type', 'application/json');
419
        }
420
421
        $record->ParentID = $data['ParentID'];
422
        $record->Name = $record->Title = basename($data['Name']);
423
        $record->write();
424
425
        $result = $this->getObjectFromData($record);
426
427
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
428
    }
429
430
    /**
431
     * Redirects 3.x style detail links to new 4.x style routing.
432
     *
433
     * @param HTTPRequest $request
434
     */
435
    public function legacyRedirectForEditView($request)
436
    {
437
        $fileID = $request->param('FileID');
438
        /** @var File $file */
439
        $file = File::get()->byID($fileID);
440
        $link = $this->getFileEditLink($file) ?: $this->Link();
441
        $this->redirect($link);
442
    }
443
444
    /**
445
     * Given a file return the CMS link to edit it
446
     *
447
     * @param File $file
448
     * @return string
449
     */
450
    public function getFileEditLink($file)
451
    {
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
        /** @skipUpgrade */
487
        $appCategories = array(
488
            'archive' => _t(
489
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
490
                'Archive'
491
            ),
492
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
493
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
494
            'flash' => _t(
495
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
496
                'Flash',
497
                'The fileformat'
498
            ),
499
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
500
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
501
        );
502
        $context->addField(
503
            $typeDropdown = new DropdownField(
504
                'AppCategory',
505
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
506
                $appCategories
507
            )
508
        );
509
510
        $typeDropdown->setEmptyString(' ');
511
512
        $currentfolderLabel = _t(
513
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
514
            'Limit to current folder?'
515
        );
516
        $context->addField(
517
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
518
        );
519
        $context->getFields()->removeByName('Title');
520
521
        return $context;
522
    }
523
524
    /**
525
     * Get an asset renamer for the given filename.
526
     *
527
     * @param  string             $filename Path name
528
     * @return AssetNameGenerator
529
     */
530
    protected function getNameGenerator($filename)
531
    {
532
        return Injector::inst()
533
            ->createWithArgs('AssetNameGenerator', array($filename));
534
    }
535
536
    /**
537
     * @todo Implement on client
538
     *
539
     * @param bool $unlinked
540
     * @return ArrayList
541
     */
542
    public function breadcrumbs($unlinked = false)
543
    {
544
        return null;
545
    }
546
547
548
    /**
549
     * Don't include class namespace in auto-generated CSS class
550
     */
551
    public function baseCSSClasses()
552
    {
553
        return 'AssetAdmin LeftAndMain';
554
    }
555
556
    public function providePermissions()
557
    {
558
        return array(
559
            "CMS_ACCESS_AssetAdmin" => array(
560
                '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...
561
                    'title' => static::menu_title()
562
                )),
563
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
564
            )
565
        );
566
    }
567
568
    /**
569
     * The form is used to generate a form schema,
570
     * as well as an intermediary object to process data through API endpoints.
571
     * Since it's used directly on API endpoints, it does not have any form actions.
572
     * It handles both {@link File} and {@link Folder} records.
573
     *
574
     * @param int $id
575
     * @return Form
576
     */
577
    public function getFileEditForm($id)
578
    {
579
        /** @var File $file */
580
        $file = $this->getList()->byID($id);
581
582 View Code Duplication
        if (!$file->canView()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
583
            $this->httpError(403, _t(
584
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
585
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
586
                '',
587
                ['ObjectTitle' => $file->i18n_singular_name()]
588
            ));
589
            return null;
590
        }
591
592
        $fields = $file->getCMSFields();
593
594
        $actions = $this->getFileEditActions($file);
595
596
        $form = Form::create(
597
            $this,
598
            'fileEditForm',
599
            $fields,
600
            $actions
601
        );
602
603
        // Load into form
604
        if ($id && $file) {
605
            $form->loadDataFrom($file);
606
        }
607
608
        // Configure form to respond to validation errors with form schema
609
        // if requested via react.
610
        $form->setValidationResponseCallback(function () use ($form) {
611
            return $this->getSchemaResponse($form);
612
        });
613
614
        return $form;
615
    }
616
617
    /**
618
     * Get file edit form
619
     *
620
     * @return Form
621
     */
622
    public function fileEditForm()
623
    {
624
        // Get ID either from posted back value, or url parameter
625
        $request = $this->getRequest();
626
        $id = $request->param('ID') ?: $request->postVar('ID');
627
        return $this->getFileEditForm($id);
628
    }
629
630
    /**
631
     * @param array $data
632
     * @param Form $form
633
     * @return HTTPResponse
634
     */
635
    public function save($data, $form)
636
    {
637
        return $this->saveOrPublish($data, $form, false);
638
    }
639
640
641
    /**
642
     * @param array $data
643
     * @param Form $form
644
     * @return HTTPResponse
645
     */
646
    public function publish($data, $form)
647
    {
648
        return $this->saveOrPublish($data, $form, true);
649
    }
650
651
    /**
652
     * Update thisrecord
653
     *
654
     * @param array $data
655
     * @param Form $form
656
     * @param bool $doPublish
657
     * @return HTTPResponse
658
     */
659
    protected function saveOrPublish($data, $form, $doPublish = false)
660
    {
661 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...
662
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
663
                ->addHeader('Content-Type', 'application/json');
664
        }
665
666
        $id = (int) $data['ID'];
667
        /** @var File $record */
668
        $record = $this->getList()->filter('ID', $id)->first();
669
670 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...
671
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
672
                ->addHeader('Content-Type', 'application/json');
673
        }
674
675 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...
676
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
677
                ->addHeader('Content-Type', 'application/json');
678
        }
679
680
        $form->saveInto($record);
681
        $record->write();
682
683
        // Publish this record and owned objects
684
        if ($doPublish) {
685
            $record->publishRecursive();
686
        }
687
688
        // Return the record data in the same response as the schema to save a postback
689
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
0 ignored issues
show
Bug introduced by
It seems like $this->getFileEditForm($id) can be null; however, getSchemaForForm() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
690
        $schemaData['record'] = $this->getObjectFromData($record);
691
        $response = new HTTPResponse(Convert::raw2json($schemaData));
692
        $response->addHeader('Content-Type', 'application/json');
693
        return $response;
694
    }
695
696
    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...
697
    {
698 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...
699
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
700
                ->addHeader('Content-Type', 'application/json');
701
        }
702
703
        $id = (int) $data['ID'];
704
        /** @var File $record */
705
        $record = $this->getList()->filter('ID', $id)->first();
706
707
        if (!$record) {
708
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
709
                ->addHeader('Content-Type', 'application/json');
710
        }
711
712
        if (!$record->canUnpublish()) {
713
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
714
                ->addHeader('Content-Type', 'application/json');
715
        }
716
717
        $record->doUnpublish();
718
719
        // Return the record data in the same response as the schema to save a postback
720
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
0 ignored issues
show
Bug introduced by
It seems like $this->getFileEditForm($id) can be null; however, getSchemaForForm() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
721
        $schemaData['record'] = $this->getObjectFromData($record);
722
        $response = new HTTPResponse(Convert::raw2json($schemaData));
723
        $response->addHeader('Content-Type', 'application/json');
724
        return $response;
725
    }
726
727
    /**
728
     * @param File $file
729
     *
730
     * @return array
731
     */
732
    protected function getObjectFromData(File $file)
733
    {
734
        $object = array(
735
            'id' => $file->ID,
736
            'created' => $file->Created,
737
            'lastUpdated' => $file->LastEdited,
738
            'owner' => null,
739
            'parent' => null,
740
            'title' => $file->Title,
741
            'exists' => $file->exists(), // Broken file check
742
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
743
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
744
            'name' => $file->Name,
745
            'filename' => $file->Filename,
746
            '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...
747
            'size' => $file->Size,
748
            'url' => $file->AbsoluteURL,
749
            'published' => $file->isPublished(),
750
            'modified' => $file->isModifiedOnDraft(),
751
            'draft' => $file->isOnDraftOnly(),
752
            'canEdit' => $file->canEdit(),
753
            'canDelete' => $file->canDelete(),
754
        );
755
756
        /** @var Member $owner */
757
        $owner = $file->Owner();
758
759
        if ($owner) {
760
            $object['owner'] = array(
761
                'id' => $owner->ID,
762
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
763
            );
764
        }
765
766
        /** @var Folder $parent */
767
        $parent = $file->Parent();
768
769
        if ($parent) {
770
            $object['parent'] = array(
771
                'id' => $parent->ID,
772
                'title' => $parent->Title,
773
                'filename' => $parent->Filename,
774
            );
775
        }
776
777
        /** @var File $file */
778
        if ($file->getIsImage()) {
779
            $width = (int)Config::inst()->get(self::class, 'thumbnail_width');
780
            $height = (int)Config::inst()->get(self::class, 'thumbnail_height');
781
782
            $thumbnail = $file->FitMax($width, $height);
783
            if ($thumbnail && $thumbnail->exists()) {
784
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
785
            }
786
            $object['dimensions']['width'] = $file->Width;
787
            $object['dimensions']['height'] = $file->Height;
0 ignored issues
show
Bug introduced by
The property Height does not seem to exist. Did you mean asset_preview_height?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
788
        }
789
790
        return $object;
791
    }
792
793
794
    /**
795
     * Returns the files and subfolders contained in the currently selected folder,
796
     * defaulting to the root node. Doubles as search results, if any search parameters
797
     * are set through {@link SearchForm()}.
798
     *
799
     * @param array $params Unsanitised request parameters
800
     * @return DataList
801
     */
802
    protected function getList($params = array())
803
    {
804
        $context = $this->getSearchContext();
805
806
        // Overwrite name filter to search both Name and Title attributes
807
        $context->removeFilterByName('Name');
808
809
        // Lazy loaded list. Allows adding new filters through SearchContext.
810
        /** @var DataList $list */
811
        $list = $context->getResults($params);
812
813
        // Re-add previously removed "Name" filter as combined filter
814
        // TODO Replace with composite SearchFilter once that API exists
815
        if (!empty($params['Name'])) {
816
            $list = $list->filterAny(array(
817
                'Name:PartialMatch' => $params['Name'],
818
                'Title:PartialMatch' => $params['Name']
819
            ));
820
        }
821
822
        // Optionally limit search to a folder (non-recursive)
823
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
824
            $list = $list->filter('ParentID', $params['ParentID']);
825
        }
826
827
        // Date filtering
828 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...
829
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
830
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
831
        }
832 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...
833
            $toDate = new DateField(null, null, $params['CreatedTo']);
834
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
835
        }
836
837
        // Categories
838
        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...
839
            $extensions = File::config()->app_categories[$filters['AppCategory']];
840
            $list = $list->filter('Name:PartialMatch', $extensions);
841
        }
842
843
        // Sort folders first
844
        $list = $list->sort(
845
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
846
        );
847
848
        // Pagination
849
        if (isset($filters['page']) && isset($filters['limit'])) {
850
            $page = $filters['page'];
851
            $limit = $filters['limit'];
852
            $offset = ($page - 1) * $limit;
853
            $list = $list->limit($limit, $offset);
854
        }
855
856
        // Access checks
857
        $list = $list->filterByCallback(function (File $file) {
858
            return $file->canView();
859
        });
860
861
        return $list;
862
    }
863
864
    /**
865
     * Action handler for adding pages to a campaign
866
     *
867
     * @param array $data
868
     * @param Form $form
869
     * @return DBHTMLText|HTTPResponse
870
     */
871
    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...
872
    {
873
        $id = $data['ID'];
874
        $record = $this->getList()->byID($id);
875
876
        $handler = AddToCampaignHandler::create($this, $record);
877
        $results = $handler->addToCampaign($record, $data['Campaign']);
878
        if (!is_null($results)) {
879
            $request = $this->getRequest();
880
            if ($request->getHeader('X-Formschema-Request')) {
881
                $data = $this->getSchemaForForm($handler->Form($record));
882
                $data['message'] = $results;
883
884
                $response = new HTTPResponse(Convert::raw2json($data));
885
                $response->addHeader('Content-Type', 'application/json');
886
                return $response;
887
            }
888
            return $results;
889
        }
890
    }
891
892
    /**
893
     * Url handler for add to campaign form
894
     *
895
     * @param HTTPRequest $request
896
     * @return Form
897
     */
898
    public function addToCampaignForm($request)
899
    {
900
        // Get ID either from posted back value, or url parameter
901
        $id = $request->param('ID') ?: $request->postVar('ID');
902
        return $this->getAddToCampaignForm($id);
903
    }
904
905
    /**
906
     * @param int $id
907
     * @return Form
908
     */
909
    public function getAddToCampaignForm($id)
910
    {
911
        // Get record-specific fields
912
        $record = $this->getList()->byID($id);
913
914
        if (!$record) {
915
            $this->httpError(404, _t(
916
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
917
                'That {Type} couldn\'t be found',
918
                '',
919
                ['Type' => File::singleton()->i18n_singular_name()]
920
            ));
921
            return null;
922
        }
923 View Code Duplication
        if (!$record->canView()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
924
            $this->httpError(403, _t(
925
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
926
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
927
                '',
928
                ['ObjectTitle' => $record->i18n_singular_name()]
929
            ));
930
            return null;
931
        }
932
933
        $handler = AddToCampaignHandler::create($this, $record);
934
        return $handler->Form($record);
935
    }
936
937
    /**
938
     * @return Upload
939
     */
940
    protected function getUpload()
941
    {
942
        $upload = Upload::create();
943
        $upload->getValidator()->setAllowedExtensions(
944
            // filter out '' since this would be a regex problem on JS end
945
            array_filter(File::config()->get('allowed_extensions'))
946
        );
947
948
        return $upload;
949
    }
950
951
    /**
952
     * Get actions for file edit
953
     *
954
     * @param File $file
955
     * @return FieldList
956
     */
957
    protected function getFileEditActions($file)
958
    {
959
        $actions = FieldList::create();
960
961
        // Save and/or publish
962
        if ($file->canEdit()) {
963
            // Create save button
964
            $saveAction = FormAction::create('save', _t('CMSMain.SAVE', 'Save'));
965
            $saveAction->setIcon('save');
966
            $actions->push($saveAction);
967
968
            // Folders are automatically published
969
            if ($file->canPublish() && (!$file instanceof Folder)) {
970
                $publishText = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.PUBLISH_BUTTON', 'Publish');
971
                $publishAction = FormAction::create('publish', $publishText);
972
                $publishAction->setIcon('rocket');
973
                $publishAction->setSchemaData(['data' => ['buttonStyle' => 'primary']]);
974
                $actions->push($publishAction);
975
            }
976
        }
977
978
        // Delete action
979
        $deleteText = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.DELETE_BUTTON', 'Delete');
980
        $deleteAction = FormAction::create('delete', $deleteText);
981
        //$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...
982
        $deleteAction->setIcon('trash-bin');
983
984
        // Add file-specific actions
985
        if (!$file instanceof Folder) {
986
            // Add to campaign action
987
            $addToCampaignAction = FormAction::create(
988
                'addtocampaign',
989
                _t(
990
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ADDTOCAMPAIGN',
991
                    'Add to campaign'
992
                )
993
            );
994
            $popoverActions = [
995
                $addToCampaignAction
996
            ];
997
            // Add unpublish if available
998
            if ($file->isPublished() && $file->canUnpublish()) {
999
                $unpublishText = _t(
1000
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNPUBLISH_BUTTON',
1001
                    'Unpublish'
1002
                );
1003
                $unpublishAction = FormAction::create('unpublish', $unpublishText);
1004
                $unpublishAction->setIcon('cancel-circled');
1005
                $popoverActions[] = $unpublishAction;
1006
            }
1007
            // Delete
1008
            $popoverActions[] = $deleteAction;
1009
1010
            // Build popover menu
1011
            $popoverField = PopoverField::create($popoverActions);
1012
            $popoverField->setPlacement('top');
1013
            $actions->push($popoverField);
1014
        } else {
1015
            $actions->push($deleteAction);
1016
        }
1017
1018
        $this->extend('updateFileEditActions', $actions);
1019
        return $actions;
1020
    }
1021
}
1022