Completed
Pull Request — master (#287)
by Ingo
03:22
created

AssetAdmin::breadcrumbs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
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
        if(!$file || !$file->isInDB()) {
452
            return null;
453
        }
454
455
        return Controller::join_links(
456
            $this->Link('show'),
457
            $file->ParentID,
458
            'edit',
459
            $file->ID
460
        );
461
    }
462
463
    /**
464
     * Get the search context from {@link File}, used to create the search form
465
     * as well as power the /search API endpoint.
466
     *
467
     * @return SearchContext
468
     */
469
    public function getSearchContext()
470
    {
471
        $context = File::singleton()->getDefaultSearchContext();
472
473
        // Customize fields
474
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
475
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
476
        ->setConfig('showcalendar', true);
477
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
478
        ->setConfig('showcalendar', true);
479
        $dateGroup = FieldGroup::create(
480
            $dateHeader,
481
            $dateFrom,
482
            $dateTo
483
        );
484
        $context->addField($dateGroup);
485
        /** @skipUpgrade */
486
        $appCategories = array(
487
            'archive' => _t(
488
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
489
                'Archive'
490
            ),
491
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
492
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
493
            'flash' => _t(
494
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash', 'Flash',
495
                'The fileformat'
496
            ),
497
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
498
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
499
        );
500
        $context->addField(
501
            $typeDropdown = new DropdownField(
502
                'AppCategory',
503
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
504
                $appCategories
505
            )
506
        );
507
508
        $typeDropdown->setEmptyString(' ');
509
510
        $currentfolderLabel = _t(
511
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
512
            'Limit to current folder?'
513
        );
514
        $context->addField(
515
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
516
        );
517
        $context->getFields()->removeByName('Title');
518
519
        return $context;
520
    }
521
522
    /**
523
     * Get an asset renamer for the given filename.
524
     *
525
     * @param  string             $filename Path name
526
     * @return AssetNameGenerator
527
     */
528
    protected function getNameGenerator($filename)
529
    {
530
        return Injector::inst()
531
            ->createWithArgs('AssetNameGenerator', array($filename));
532
    }
533
534
    /**
535
     * @todo Implement on client
536
     *
537
     * @param bool $unlinked
538
     * @return ArrayList
539
     */
540
    public function breadcrumbs($unlinked = false)
541
    {
542
        return null;
543
    }
544
545
546
    /**
547
     * Don't include class namespace in auto-generated CSS class
548
     */
549
    public function baseCSSClasses()
550
    {
551
        return 'AssetAdmin LeftAndMain';
552
    }
553
554
    public function providePermissions()
555
    {
556
        return array(
557
            "CMS_ACCESS_AssetAdmin" => array(
558
                '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...
559
                    'title' => static::menu_title()
560
                )),
561
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
562
            )
563
        );
564
    }
565
566
    /**
567
     * The form is used to generate a form schema,
568
     * as well as an intermediary object to process data through API endpoints.
569
     * Since it's used directly on API endpoints, it does not have any form actions.
570
     * It handles both {@link File} and {@link Folder} records.
571
     *
572
     * @param int $id
573
     * @return Form
574
     */
575
    public function getFileEditForm($id)
576
    {
577
        /** @var File $file */
578
        $file = $this->getList()->byID($id);
579
580 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...
581
            $this->httpError(403, _t(
582
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
583
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
584
                '',
585
                ['ObjectTitle' => $file->i18n_singular_name()]
586
            ));
587
            return null;
588
        }
589
590
        $fields = $file->getCMSFields();
591
592
        $actions = $this->getFileEditActions($file);
593
594
        $form = Form::create(
595
            $this,
596
            'FileEditForm',
597
            $fields,
598
            $actions
599
        );
600
601
        // Load into form
602
        if($id && $file) {
603
            $form->loadDataFrom($file);
604
        }
605
606
        // Configure form to respond to validation errors with form schema
607
        // if requested via react.
608
        $form->setValidationResponseCallback(function() use ($form, $file) {
609
            $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $file->exists() ? $file->ID : '');
610
            return $this->getSchemaResponse($form, $schemaId);
0 ignored issues
show
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with $schemaId.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
611
        });
612
613
        return $form;
614
    }
615
616
    /**
617
     * Get file edit form
618
     *
619
     * @return Form
620
     */
621
    public function FileEditForm()
622
    {
623
        // Get ID either from posted back value, or url parameter
624
        $request = $this->getRequest();
625
        $id = $request->param('ID') ?: $request->postVar('ID');
626
        return $this->getFileEditForm($id);
627
    }
628
629
    /**
630
     * @param array $data
631
     * @param Form $form
632
     * @return HTTPResponse
633
     */
634
    public function save($data, $form)
635
    {
636
        return $this->saveOrPublish($data, $form, false);
637
    }
638
639
640
    /**
641
     * @param array $data
642
     * @param Form $form
643
     * @return HTTPResponse
644
     */
645
    public function publish($data, $form) {
646
        return $this->saveOrPublish($data, $form, true);
647
    }
648
649
    /**
650
     * Update thisrecord
651
     *
652
     * @param array $data
653
     * @param Form $form
654
     * @param bool $doPublish
655
     * @return HTTPResponse
656
     */
657
    protected function saveOrPublish($data, $form, $doPublish = false) {
658 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...
659
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
660
                ->addHeader('Content-Type', 'application/json');
661
        }
662
663
        $id = (int) $data['ID'];
664
        /** @var File $record */
665
        $record = $this->getList()->filter('ID', $id)->first();
666
667 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...
668
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
669
                ->addHeader('Content-Type', 'application/json');
670
        }
671
672 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...
673
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
674
                ->addHeader('Content-Type', 'application/json');
675
        }
676
677
        $form->saveInto($record);
678
        $record->write();
679
680
        // Publish this record and owned objects
681
        if ($doPublish) {
682
            $record->publishRecursive();
683
        }
684
685
        // Return the record data in the same response as the schema to save a postback
686
        $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $record->exists() ? $record->ID : '');
687
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id), $schemaId);
0 ignored issues
show
Bug introduced by
It seems like $this->getFileEditForm($id) can be null; however, getSchemaForForm() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Unused Code introduced by
The call to AssetAdmin::getSchemaForForm() has too many arguments starting with $schemaId.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
688
        $schemaData['record'] = $this->getObjectFromData($record);
689
        $response = new HTTPResponse(Convert::raw2json($schemaData));
690
        $response->addHeader('Content-Type', 'application/json');
691
        return $response;
692
    }
693
694
    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...
695 View Code Duplication
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
696
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
697
                ->addHeader('Content-Type', 'application/json');
698
        }
699
700
        $id = (int) $data['ID'];
701
        /** @var File $record */
702
        $record = $this->getList()->filter('ID', $id)->first();
703
704
        if (!$record) {
705
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
706
                ->addHeader('Content-Type', 'application/json');
707
        }
708
709
        if (!$record->canUnpublish()) {
710
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
711
                ->addHeader('Content-Type', 'application/json');
712
        }
713
714
        $record->doUnpublish();
715
716
        // Return the record data in the same response as the schema to save a postback
717
        $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $record->exists() ? $record->ID : '');
718
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id), $schemaId);
0 ignored issues
show
Bug introduced by
It seems like $this->getFileEditForm($id) can be null; however, getSchemaForForm() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Unused Code introduced by
The call to AssetAdmin::getSchemaForForm() has too many arguments starting with $schemaId.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
719
        $schemaData['record'] = $this->getObjectFromData($record);
720
        $response = new HTTPResponse(Convert::raw2json($schemaData));
721
        $response->addHeader('Content-Type', 'application/json');
722
        return $response;
723
    }
724
725
    /**
726
     * @param File $file
727
     *
728
     * @return array
729
     */
730
    protected function getObjectFromData(File $file)
731
    {
732
        $object = array(
733
            'id' => $file->ID,
734
            'created' => $file->Created,
735
            'lastUpdated' => $file->LastEdited,
736
            'owner' => null,
737
            'parent' => null,
738
            'title' => $file->Title,
739
            'exists' => $file->exists(), // Broken file check
740
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
741
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
742
            'name' => $file->Name,
743
            'filename' => $file->Filename,
744
            '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...
745
            'size' => $file->Size,
746
            'url' => $file->AbsoluteURL,
747
            'published' => $file->isPublished(),
748
            'modified' => $file->isModifiedOnDraft(),
749
            'draft' => $file->isOnDraftOnly(),
750
            'canEdit' => $file->canEdit(),
751
            'canDelete' => $file->canDelete(),
752
        );
753
754
        /** @var Member $owner */
755
        $owner = $file->Owner();
756
757
        if ($owner) {
758
            $object['owner'] = array(
759
                'id' => $owner->ID,
760
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
761
            );
762
        }
763
764
        /** @var Folder $parent */
765
        $parent = $file->Parent();
766
767
        if ($parent) {
768
            $object['parent'] = array(
769
                'id' => $parent->ID,
770
                'title' => $parent->Title,
771
                'filename' => $parent->Filename,
772
            );
773
        }
774
775
        /** @var File $file */
776
        if ($file->getIsImage()) {
777
            $width = (int)Config::inst()->get(self::class, 'thumbnail_width');
778
            $height = (int)Config::inst()->get(self::class, 'thumbnail_height');
779
780
            $thumbnail = $file->FitMax($width, $height);
781
            if ($thumbnail && $thumbnail->exists()) {
782
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
783
            }
784
            $object['dimensions']['width'] = $file->Width;
785
            $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...
786
        }
787
788
        return $object;
789
    }
790
791
792
    /**
793
     * Returns the files and subfolders contained in the currently selected folder,
794
     * defaulting to the root node. Doubles as search results, if any search parameters
795
     * are set through {@link SearchForm()}.
796
     *
797
     * @param array $params Unsanitised request parameters
798
     * @return DataList
799
     */
800
    protected function getList($params = array())
801
    {
802
        $context = $this->getSearchContext();
803
804
        // Overwrite name filter to search both Name and Title attributes
805
        $context->removeFilterByName('Name');
806
807
        // Lazy loaded list. Allows adding new filters through SearchContext.
808
        /** @var DataList $list */
809
        $list = $context->getResults($params);
810
811
        // Re-add previously removed "Name" filter as combined filter
812
        // TODO Replace with composite SearchFilter once that API exists
813
        if(!empty($params['Name'])) {
814
            $list = $list->filterAny(array(
815
                'Name:PartialMatch' => $params['Name'],
816
                'Title:PartialMatch' => $params['Name']
817
            ));
818
        }
819
820
        // Optionally limit search to a folder (non-recursive)
821
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
822
            $list = $list->filter('ParentID', $params['ParentID']);
823
        }
824
825
        // Date filtering
826 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...
827
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
828
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
829
        }
830 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...
831
            $toDate = new DateField(null, null, $params['CreatedTo']);
832
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
833
        }
834
835
        // Categories
836
        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...
837
            $extensions = File::config()->app_categories[$filters['AppCategory']];
838
            $list = $list->filter('Name:PartialMatch', $extensions);
839
        }
840
841
        // Sort folders first
842
        $list = $list->sort(
843
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
844
        );
845
846
        // Pagination
847
        if (isset($filters['page']) && isset($filters['limit'])) {
848
            $page = $filters['page'];
849
            $limit = $filters['limit'];
850
            $offset = ($page - 1) * $limit;
851
            $list = $list->limit($limit, $offset);
852
        }
853
854
        // Access checks
855
        $list = $list->filterByCallback(function(File $file) {
856
            return $file->canView();
857
        });
858
859
        return $list;
860
    }
861
862
    /**
863
     * Action handler for adding pages to a campaign
864
     *
865
     * @param array $data
866
     * @param Form $form
867
     * @return DBHTMLText|HTTPResponse
868
     */
869
    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...
870
    {
871
        $id = $data['ID'];
872
        $record = $this->getList()->byID($id);
873
874
        $handler = AddToCampaignHandler::create($this, $record);
875
        $results = $handler->addToCampaign($record, $data['Campaign']);
876
        if (!is_null($results)) {
877
            $request = $this->getRequest();
878
            if($request->getHeader('X-Formschema-Request')) {
879
                $data = $this->getSchemaForForm($handler->Form($record));
880
                $data['message'] = $results;
881
882
                $response = new HTTPResponse(Convert::raw2json($data));
883
                $response->addHeader('Content-Type', 'application/json');
884
                return $response;
885
            }
886
            return $results;
887
        }
888
    }
889
890
    /**
891
     * Url handler for add to campaign form
892
     *
893
     * @param HTTPRequest $request
894
     * @return Form
895
     */
896
    public function AddToCampaignForm($request)
897
    {
898
        // Get ID either from posted back value, or url parameter
899
        $id = $request->param('ID') ?: $request->postVar('ID');
900
        return $this->getAddToCampaignForm($id);
901
    }
902
903
    /**
904
     * @param int $id
905
     * @return Form
906
     */
907
    public function getAddToCampaignForm($id)
908
    {
909
        // Get record-specific fields
910
        $record = $this->getList()->byID($id);
911
912
        if (!$record) {
913
            $this->httpError(404, _t(
914
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
915
                'That {Type} couldn\'t be found',
916
                '',
917
                ['Type' => File::singleton()->i18n_singular_name()]
918
            ));
919
            return null;
920
        }
921 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...
922
            $this->httpError(403, _t(
923
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
924
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
925
                '',
926
                ['ObjectTitle' => $record->i18n_singular_name()]
927
            ));
928
            return null;
929
        }
930
931
        $handler = AddToCampaignHandler::create($this, $record);
932
        return $handler->Form($record);
933
    }
934
935
    /**
936
     * @return Upload
937
     */
938
    protected function getUpload()
939
    {
940
        $upload = Upload::create();
941
        $upload->getValidator()->setAllowedExtensions(
942
            // filter out '' since this would be a regex problem on JS end
943
            array_filter(File::config()->get('allowed_extensions'))
944
        );
945
946
        return $upload;
947
    }
948
949
    /**
950
     * Get actions for file edit
951
     *
952
     * @param File $file
953
     * @return FieldList
954
     */
955
    protected function getFileEditActions($file)
956
    {
957
        $actions = FieldList::create();
958
959
        // Save and/or publish
960
        if ($file->canEdit()) {
961
            // Create save button
962
            $saveAction = FormAction::create('save', _t('CMSMain.SAVE', 'Save'));
963
            $saveAction->setIcon('save');
964
            $actions->push($saveAction);
965
966
            // Folders are automatically published
967
            if ($file->canPublish() && (!$file instanceof Folder)) {
968
                $publishText = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.PUBLISH_BUTTON', 'Publish');
969
                $publishAction = FormAction::create('publish', $publishText);
970
                $publishAction->setIcon('rocket');
971
                $publishAction->setSchemaData(['data' => ['buttonStyle' => 'primary']]);
972
                $actions->push($publishAction);
973
            }
974
        }
975
976
        // Delete action
977
        $deleteText = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.DELETE_BUTTON', 'Delete');
978
        $deleteAction = FormAction::create('delete', $deleteText);
979
        //$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...
980
        $deleteAction->setIcon('trash-bin');
981
982
        // Add file-specific actions
983
        if (!$file instanceof Folder) {
984
            // Add to campaign action
985
            $addToCampaignAction = FormAction::create(
986
                'addtocampaign',
987
                _t(
988
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ADDTOCAMPAIGN',
989
                    'Add to campaign'
990
                )
991
            );
992
            $popoverActions = [
993
                $addToCampaignAction
994
            ];
995
            // Add unpublish if available
996
            if ($file->isPublished() && $file->canUnpublish()) {
997
                $unpublishText = _t(
998
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNPUBLISH_BUTTON',
999
                    'Unpublish'
1000
                );
1001
                $unpublishAction = FormAction::create('unpublish', $unpublishText);
1002
                $unpublishAction->setIcon('cancel-circled');
1003
                $popoverActions[] = $unpublishAction;
1004
            }
1005
            // Delete
1006
            $popoverActions[] = $deleteAction;
1007
1008
            // Build popover menu
1009
            $popoverField = PopoverField::create($popoverActions);
1010
            $popoverField->setPlacement('top');
1011
            $actions->push($popoverField);
1012
        } else {
1013
            $actions->push($deleteAction);
1014
        }
1015
1016
        $this->extend('updateFileEditActions', $actions);
1017
        return $actions;
1018
    }
1019
}
1020