Completed
Push — master ( cb6000...0f63fa )
by Sam
02:23
created

AssetAdmin::getFormFactory()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 3
eloc 9
nc 3
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\AssetAdmin\Forms\AssetFormFactory;
9
use SilverStripe\AssetAdmin\Forms\FileFormFactory;
10
use SilverStripe\AssetAdmin\Forms\FolderFormFactory;
11
use SilverStripe\Forms\DefaultFormFactory;
12
use SilverStripe\AssetAdmin\Forms\ImageFormFactory;
13
use SilverStripe\Assets\File;
14
use SilverStripe\Assets\Folder;
15
use SilverStripe\Assets\Image;
16
use SilverStripe\Assets\Storage\AssetNameGenerator;
17
use SilverStripe\Assets\Upload;
18
use SilverStripe\Control\Controller;
19
use SilverStripe\Control\HTTPRequest;
20
use SilverStripe\Control\HTTPResponse;
21
use SilverStripe\Core\Config\Config;
22
use SilverStripe\Core\Convert;
23
use SilverStripe\Core\Injector\Injector;
24
use SilverStripe\Forms\CheckboxField;
25
use SilverStripe\Forms\DateField;
26
use SilverStripe\Forms\DropdownField;
27
use SilverStripe\Forms\FieldGroup;
28
use SilverStripe\Forms\Form;
29
use SilverStripe\Forms\FormFactory;
30
use SilverStripe\Forms\HeaderField;
31
use SilverStripe\ORM\ArrayList;
32
use SilverStripe\ORM\DataList;
33
use SilverStripe\ORM\DataObject;
34
use SilverStripe\ORM\FieldType\DBHTMLText;
35
use SilverStripe\ORM\Search\SearchContext;
36
use SilverStripe\Security\Member;
37
use SilverStripe\Security\PermissionProvider;
38
use SilverStripe\Security\SecurityToken;
39
use SilverStripe\View\Requirements;
40
41
/**
42
 * AssetAdmin is the 'file store' section of the CMS.
43
 * It provides an interface for manipulating the File and Folder objects in the system.
44
 */
45
class AssetAdmin extends LeftAndMain implements PermissionProvider
46
{
47
    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...
48
49
    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...
50
51
    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...
52
53
    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...
54
55
    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...
56
        // Legacy redirect for SS3-style detail view
57
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
58
        // Pass all URLs to the index, for React to unpack
59
        'show/$FolderID/edit/$FileID' => 'index',
60
        // API access points with structured data
61
        'POST api/createFolder' => 'apiCreateFolder',
62
        'POST api/createFile' => 'apiCreateFile',
63
        'GET api/readFolder' => 'apiReadFolder',
64
        'PUT api/updateFolder' => 'apiUpdateFolder',
65
        'DELETE api/delete' => 'apiDelete',
66
        'GET api/search' => 'apiSearch',
67
    ];
68
69
    /**
70
     * Amount of results showing on a single page.
71
     *
72
     * @config
73
     * @var int
74
     */
75
    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...
76
77
    /**
78
     * @config
79
     * @see Upload->allowedMaxFileSize
80
     * @var int
81
     */
82
    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...
83
84
    /**
85
     * @var array
86
     */
87
    private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

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

Loading history...
88
        'legacyRedirectForEditView',
89
        'apiCreateFolder',
90
        'apiCreateFile',
91
        'apiReadFolder',
92
        'apiUpdateFolder',
93
        'apiDelete',
94
        'apiSearch',
95
        'FileEditForm',
96
        'AddToCampaignForm',
97
    );
98
99
    private static $required_permission_codes = 'CMS_ACCESS_AssetAdmin';
0 ignored issues
show
Unused Code introduced by
The property $required_permission_codes is not used and could be removed.

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

Loading history...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
100
101
    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...
102
103
    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...
104
105
    /**
106
     * Set up the controller
107
     */
108
    public function init()
109
    {
110
        parent::init();
111
112
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
113
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
114
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
115
116
        CMSBatchActionHandler::register(
117
            'delete',
118
            'SilverStripe\AssetAdmin\BatchAction\DeleteAssets',
119
            'SilverStripe\\Assets\\Folder'
120
        );
121
    }
122
123
    public function getClientConfig()
124
    {
125
        $baseLink = $this->Link();
126
        return array_merge(parent::getClientConfig(), [
127
            'reactRouter' => true,
128
            'createFileEndpoint' => [
129
                'url' => Controller::join_links($baseLink, 'api/createFile'),
130
                'method' => 'post',
131
                'payloadFormat' => 'urlencoded',
132
            ],
133
            'createFolderEndpoint' => [
134
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
135
                'method' => 'post',
136
                'payloadFormat' => 'urlencoded',
137
            ],
138
            'readFolderEndpoint' => [
139
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
140
                'method' => 'get',
141
                'responseFormat' => 'json',
142
            ],
143
            'searchEndpoint' => [
144
                'url' => Controller::join_links($baseLink, 'api/search'),
145
                'method' => 'get',
146
                'responseFormat' => 'json',
147
            ],
148
            'updateFolderEndpoint' => [
149
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
150
                'method' => 'put',
151
                'payloadFormat' => 'urlencoded',
152
            ],
153
            'deleteEndpoint' => [
154
                'url' => Controller::join_links($baseLink, 'api/delete'),
155
                'method' => 'delete',
156
                'payloadFormat' => 'urlencoded',
157
            ],
158
            'limit' => $this->config()->page_length,
159
            'form' => [
160
                'FileEditForm' => [
161
                    'schemaUrl' => $this->Link('schema/FileEditForm')
162
                ],
163
                'AddToCampaignForm' => [
164
                    'schemaUrl' => $this->Link('schema/AddToCampaignForm')
165
                ],
166
            ],
167
        ]);
168
    }
169
170
    /**
171
     * Fetches a collection of files by ParentID.
172
     *
173
     * @param HTTPRequest $request
174
     * @return HTTPResponse
175
     */
176
    public function apiReadFolder(HTTPRequest $request)
177
    {
178
        $params = $request->requestVars();
179
        $items = array();
180
        $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...
181
        $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...
182
183
        if (!isset($params['id']) && !strlen($params['id'])) {
184
            $this->httpError(400);
185
        }
186
187
        $folderID = (int)$params['id'];
188
        /** @var Folder $folder */
189
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
190
191
        if (!$folder) {
192
            $this->httpError(400);
193
        }
194
195
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
196
        $files = $this->getList()->filter('ParentID', $folderID);
197
198
        if ($files) {
199
            /** @var File $file */
200
            foreach ($files as $file) {
201
                if (!$file->canView()) {
202
                    continue;
203
                }
204
205
                $items[] = $this->getObjectFromData($file);
206
            }
207
        }
208
209
        // Build parents (for breadcrumbs)
210
        $parents = [];
211
        $next = $folder->Parent();
212
        while ($next && $next->exists()) {
213
            array_unshift($parents, [
214
                'id' => $next->ID,
215
                'title' => $next->getTitle(),
216
                'filename' => $next->getFilename(),
217
            ]);
218
            if ($next->ParentID) {
219
                $next = $next->Parent();
220
            } else {
221
                break;
222
            }
223
        }
224
225
        // Build response
226
        $response = new HTTPResponse();
227
        $response->addHeader('Content-Type', 'application/json');
228
        $response->setBody(json_encode([
229
            'files' => $items,
230
            'title' => $folder->getTitle(),
231
            'count' => count($items),
232
            'parents' => $parents,
233
            'parent' => $parents ? $parents[count($parents) - 1] : null,
234
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
235
            'folderID' => $folderID,
236
            'canEdit' => $folder->canEdit(),
237
            'canDelete' => $folder->canDelete(),
238
        ]));
239
240
        return $response;
241
    }
242
243
    /**
244
     * @param HTTPRequest $request
245
     *
246
     * @return HTTPResponse
247
     */
248
    public function apiSearch(HTTPRequest $request)
249
    {
250
        $params = $request->getVars();
251
        $list = $this->getList($params);
252
253
        $response = new HTTPResponse();
254
        $response->addHeader('Content-Type', 'application/json');
255
        $response->setBody(json_encode([
256
            // Serialisation
257
            "files" => array_map(function ($file) {
258
                return $this->getObjectFromData($file);
259
            }, $list->toArray()),
260
            "count" => $list->count(),
261
        ]));
262
263
        return $response;
264
    }
265
266
    /**
267
     * @param HTTPRequest $request
268
     *
269
     * @return HTTPResponse
270
     */
271
    public function apiDelete(HTTPRequest $request)
272
    {
273
        parse_str($request->getBody(), $vars);
274
275
        // CSRF check
276
        $token = SecurityToken::inst();
277 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...
278
            return new HTTPResponse(null, 400);
279
        }
280
281 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...
282
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
283
                ->addHeader('Content-Type', 'application/json');
284
        }
285
286
        $fileIds = $vars['ids'];
287
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
288
289 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...
290
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
291
                ->addHeader('Content-Type', 'application/json');
292
        }
293
294
        if (!min(array_map(function (File $file) {
295
            return $file->canDelete();
296
        }, $files))) {
297
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
298
                ->addHeader('Content-Type', 'application/json');
299
        }
300
301
        /** @var File $file */
302
        foreach ($files as $file) {
303
            $file->delete();
304
        }
305
306
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
307
            ->addHeader('Content-Type', 'application/json');
308
    }
309
310
    /**
311
     * Creates a single file based on a form-urlencoded upload.
312
     *
313
     * @param HTTPRequest $request
314
     * @return HTTPRequest|HTTPResponse
315
     */
316
    public function apiCreateFile(HTTPRequest $request)
317
    {
318
        $data = $request->postVars();
319
        $upload = $this->getUpload();
320
321
        // CSRF check
322
        $token = SecurityToken::inst();
323 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...
324
            return new HTTPResponse(null, 400);
325
        }
326
327
        // Check parent record
328
        /** @var Folder $parentRecord */
329
        $parentRecord = null;
330
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
331
            $parentRecord = Folder::get()->byID($data['ParentID']);
332
        }
333
        $data['Parent'] = $parentRecord;
334
335
        $tmpFile = $request->postVar('Upload');
336 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...
337
            $result = ['error' => $upload->getErrors()];
338
            return (new HTTPResponse(json_encode($result), 400))
339
                ->addHeader('Content-Type', 'application/json');
340
        }
341
342
        // TODO Allow batch uploads
343
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
344
        /** @var File $file */
345
        $file = Injector::inst()->create($fileClass);
346
347
        // check canCreate permissions
348 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...
349
            return (new HTTPResponse(json_encode(['status' => 'error']), 403))
350
                ->addHeader('Content-Type', 'application/json');
351
        }
352
353
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
354 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...
355
            $result = ['error' => 'unknown'];
356
            return (new HTTPResponse(json_encode($result), 400))
357
                ->addHeader('Content-Type', 'application/json');
358
        }
359
360
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
361
        $file->write();
362
363
        $result = [$this->getObjectFromData($file)];
364
365
        return (new HTTPResponse(json_encode($result)))
366
            ->addHeader('Content-Type', 'application/json');
367
    }
368
369
    /**
370
     * Creates a single folder, within an optional parent folder.
371
     *
372
     * @param HTTPRequest $request
373
     * @return HTTPRequest|HTTPResponse
374
     */
375
    public function apiCreateFolder(HTTPRequest $request)
376
    {
377
        $data = $request->postVars();
378
379
        $class = 'SilverStripe\\Assets\\Folder';
380
381
        // CSRF check
382
        $token = SecurityToken::inst();
383 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...
384
            return new HTTPResponse(null, 400);
385
        }
386
387
        // check addchildren permissions
388
        /** @var Folder $parentRecord */
389
        $parentRecord = null;
390
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
391
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
392
        }
393
        $data['Parent'] = $parentRecord;
394
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
395
396
        // Build filename
397
        $baseFilename = isset($data['Name'])
398
            ? basename($data['Name'])
399
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
400
401
        if ($parentRecord && $parentRecord->ID) {
402
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
403
        }
404
405
        // Ensure name is unique
406
        $nameGenerator = $this->getNameGenerator($baseFilename);
407
        $filename = null;
408
        foreach ($nameGenerator as $filename) {
409
            if (! File::find($filename)) {
410
                break;
411
            }
412
        }
413
        $data['Name'] = basename($filename);
414
415
        // Create record
416
        /** @var Folder $record */
417
        $record = Injector::inst()->create($class);
418
419
        // check create permissions
420
        if (!$record->canCreate(null, $data)) {
421
            return (new HTTPResponse(null, 403))
422
                ->addHeader('Content-Type', 'application/json');
423
        }
424
425
        $record->ParentID = $data['ParentID'];
426
        $record->Name = $record->Title = basename($data['Name']);
427
        $record->write();
428
429
        $result = $this->getObjectFromData($record);
430
431
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
432
    }
433
434
    /**
435
     * Redirects 3.x style detail links to new 4.x style routing.
436
     *
437
     * @param HTTPRequest $request
438
     */
439
    public function legacyRedirectForEditView($request)
440
    {
441
        $fileID = $request->param('FileID');
442
        /** @var File $file */
443
        $file = File::get()->byID($fileID);
444
        $link = $this->getFileEditLink($file) ?: $this->Link();
445
        $this->redirect($link);
446
    }
447
448
    /**
449
     * Given a file return the CMS link to edit it
450
     *
451
     * @param File $file
452
     * @return string
453
     */
454
    public function getFileEditLink($file)
455
    {
456
        if (!$file || !$file->isInDB()) {
457
            return null;
458
        }
459
460
        return Controller::join_links(
461
            $this->Link('show'),
462
            $file->ParentID,
463
            'edit',
464
            $file->ID
465
        );
466
    }
467
468
    /**
469
     * Get the search context from {@link File}, used to create the search form
470
     * as well as power the /search API endpoint.
471
     *
472
     * @return SearchContext
473
     */
474
    public function getSearchContext()
475
    {
476
        $context = File::singleton()->getDefaultSearchContext();
477
478
        // Customize fields
479
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
480
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
481
        ->setConfig('showcalendar', true);
482
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
483
        ->setConfig('showcalendar', true);
484
        $dateGroup = FieldGroup::create(
485
            $dateHeader,
486
            $dateFrom,
487
            $dateTo
488
        );
489
        $context->addField($dateGroup);
490
        /** @skipUpgrade */
491
        $appCategories = array(
492
            'archive' => _t(
493
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
494
                'Archive'
495
            ),
496
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
497
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
498
            'flash' => _t(
499
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
500
                'Flash',
501
                'The fileformat'
502
            ),
503
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
504
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
505
        );
506
        $context->addField(
507
            $typeDropdown = new DropdownField(
508
                'AppCategory',
509
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
510
                $appCategories
511
            )
512
        );
513
514
        $typeDropdown->setEmptyString(' ');
515
516
        $currentfolderLabel = _t(
517
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
518
            'Limit to current folder?'
519
        );
520
        $context->addField(
521
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
522
        );
523
        $context->getFields()->removeByName('Title');
524
525
        return $context;
526
    }
527
528
    /**
529
     * Get an asset renamer for the given filename.
530
     *
531
     * @param  string             $filename Path name
532
     * @return AssetNameGenerator
533
     */
534
    protected function getNameGenerator($filename)
535
    {
536
        return Injector::inst()
537
            ->createWithArgs('AssetNameGenerator', array($filename));
538
    }
539
540
    /**
541
     * @todo Implement on client
542
     *
543
     * @param bool $unlinked
544
     * @return ArrayList
545
     */
546
    public function breadcrumbs($unlinked = false)
547
    {
548
        return null;
549
    }
550
551
552
    /**
553
     * Don't include class namespace in auto-generated CSS class
554
     */
555
    public function baseCSSClasses()
556
    {
557
        return 'AssetAdmin LeftAndMain';
558
    }
559
560
    public function providePermissions()
561
    {
562
        return array(
563
            "CMS_ACCESS_AssetAdmin" => array(
564
                '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...
565
                    'title' => static::menu_title()
566
                )),
567
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
568
            )
569
        );
570
    }
571
572
    /**
573
     * Build a form scaffolder for this model
574
     *
575
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
576
     *
577
     * @param File $file
578
     * @return FormFactory
579
     */
580
    public function getFormFactory(File $file)
581
    {
582
        // Get service name based on file class
583
        $name = null;
0 ignored issues
show
Unused Code introduced by
$name 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...
584
        if ($file instanceof Folder) {
585
            $name = FolderFormFactory::class;
586
        } elseif ($file instanceof Image) {
587
            $name = ImageFormFactory::class;
588
        } else {
589
            $name = FileFormFactory::class;
590
        }
591
        return Injector::inst()->get($name);
592
    }
593
594
    /**
595
     * The form is used to generate a form schema,
596
     * as well as an intermediary object to process data through API endpoints.
597
     * Since it's used directly on API endpoints, it does not have any form actions.
598
     * It handles both {@link File} and {@link Folder} records.
599
     *
600
     * @param int $id
601
     * @return Form
602
     */
603
    public function getFileEditForm($id)
604
    {
605
        /** @var File $file */
606
        $file = $this->getList()->byID($id);
607
608 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...
609
            $this->httpError(403, _t(
610
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
611
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
612
                '',
613
                ['ObjectTitle' => $file->i18n_singular_name()]
614
            ));
615
            return null;
616
        }
617
618
        $scaffolder = $this->getFormFactory($file);
619
        $form = $scaffolder->getForm($this, 'FileEditForm', [
620
            'Record' => $file
621
        ]);
622
623
        // Configure form to respond to validation errors with form schema
624
        // if requested via react.
625
        $form->setValidationResponseCallback(function () use ($form) {
626
            return $this->getSchemaResponse($form);
627
        });
628
629
        return $form;
630
    }
631
632
    /**
633
     * Get file edit form
634
     *
635
     * @return Form
636
     */
637
    public function FileEditForm()
638
    {
639
        // Get ID either from posted back value, or url parameter
640
        $request = $this->getRequest();
641
        $id = $request->param('ID') ?: $request->postVar('ID');
642
        return $this->getFileEditForm($id);
643
    }
644
645
    /**
646
     * @param array $data
647
     * @param Form $form
648
     * @return HTTPResponse
649
     */
650
    public function save($data, $form)
651
    {
652
        return $this->saveOrPublish($data, $form, false);
653
    }
654
655
656
    /**
657
     * @param array $data
658
     * @param Form $form
659
     * @return HTTPResponse
660
     */
661
    public function publish($data, $form)
662
    {
663
        return $this->saveOrPublish($data, $form, true);
664
    }
665
666
    /**
667
     * Update thisrecord
668
     *
669
     * @param array $data
670
     * @param Form $form
671
     * @param bool $doPublish
672
     * @return HTTPResponse
673
     */
674
    protected function saveOrPublish($data, $form, $doPublish = false)
675
    {
676 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...
677
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
678
                ->addHeader('Content-Type', 'application/json');
679
        }
680
681
        $id = (int) $data['ID'];
682
        /** @var File $record */
683
        $record = $this->getList()->filter('ID', $id)->first();
684
685 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...
686
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
687
                ->addHeader('Content-Type', 'application/json');
688
        }
689
690 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...
691
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
692
                ->addHeader('Content-Type', 'application/json');
693
        }
694
695
        $form->saveInto($record);
696
        $record->write();
697
698
        // Publish this record and owned objects
699
        if ($doPublish) {
700
            $record->publishRecursive();
701
        }
702
703
        // Return the record data in the same response as the schema to save a postback
704
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
705
        $schemaData['record'] = $this->getObjectFromData($record);
706
        $response = new HTTPResponse(Convert::raw2json($schemaData));
707
        $response->addHeader('Content-Type', 'application/json');
708
        return $response;
709
    }
710
711
    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...
712
    {
713 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...
714
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
715
                ->addHeader('Content-Type', 'application/json');
716
        }
717
718
        $id = (int) $data['ID'];
719
        /** @var File $record */
720
        $record = $this->getList()->filter('ID', $id)->first();
721
722
        if (!$record) {
723
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
724
                ->addHeader('Content-Type', 'application/json');
725
        }
726
727
        if (!$record->canUnpublish()) {
728
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
729
                ->addHeader('Content-Type', 'application/json');
730
        }
731
732
        $record->doUnpublish();
733
734
        // Return the record data in the same response as the schema to save a postback
735
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
736
        $schemaData['record'] = $this->getObjectFromData($record);
737
        $response = new HTTPResponse(Convert::raw2json($schemaData));
738
        $response->addHeader('Content-Type', 'application/json');
739
        return $response;
740
    }
741
742
    /**
743
     * @param File $file
744
     *
745
     * @return array
746
     */
747
    protected function getObjectFromData(File $file)
748
    {
749
        $object = array(
750
            'id' => $file->ID,
751
            'created' => $file->Created,
752
            'lastUpdated' => $file->LastEdited,
753
            'owner' => null,
754
            'parent' => null,
755
            'title' => $file->Title,
756
            'exists' => $file->exists(), // Broken file check
757
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
758
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
759
            'name' => $file->Name,
760
            'filename' => $file->Filename,
761
            '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...
762
            'size' => $file->Size,
763
            'url' => $file->AbsoluteURL,
764
            'published' => $file->isPublished(),
765
            'modified' => $file->isModifiedOnDraft(),
766
            'draft' => $file->isOnDraftOnly(),
767
            'canEdit' => $file->canEdit(),
768
            'canDelete' => $file->canDelete(),
769
        );
770
771
        /** @var Member $owner */
772
        $owner = $file->Owner();
773
774
        if ($owner) {
775
            $object['owner'] = array(
776
                'id' => $owner->ID,
777
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
778
            );
779
        }
780
781
        /** @var Folder $parent */
782
        $parent = $file->Parent();
783
784
        if ($parent) {
785
            $object['parent'] = array(
786
                'id' => $parent->ID,
787
                'title' => $parent->Title,
788
                'filename' => $parent->Filename,
789
            );
790
        }
791
792
        /** @var File $file */
793
        if ($file->getIsImage()) {
794
            $width = (int)Config::inst()->get(self::class, 'thumbnail_width');
795
            $height = (int)Config::inst()->get(self::class, 'thumbnail_height');
796
797
            $thumbnail = $file->FitMax($width, $height);
798
            if ($thumbnail && $thumbnail->exists()) {
799
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
800
            }
801
            $object['dimensions']['width'] = $file->Width;
802
            $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...
803
        }
804
805
        return $object;
806
    }
807
808
809
    /**
810
     * Returns the files and subfolders contained in the currently selected folder,
811
     * defaulting to the root node. Doubles as search results, if any search parameters
812
     * are set through {@link SearchForm()}.
813
     *
814
     * @param array $params Unsanitised request parameters
815
     * @return DataList
816
     */
817
    protected function getList($params = array())
818
    {
819
        $context = $this->getSearchContext();
820
821
        // Overwrite name filter to search both Name and Title attributes
822
        $context->removeFilterByName('Name');
823
824
        // Lazy loaded list. Allows adding new filters through SearchContext.
825
        /** @var DataList $list */
826
        $list = $context->getResults($params);
827
828
        // Re-add previously removed "Name" filter as combined filter
829
        // TODO Replace with composite SearchFilter once that API exists
830
        if (!empty($params['Name'])) {
831
            $list = $list->filterAny(array(
832
                'Name:PartialMatch' => $params['Name'],
833
                'Title:PartialMatch' => $params['Name']
834
            ));
835
        }
836
837
        // Optionally limit search to a folder (non-recursive)
838
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
839
            $list = $list->filter('ParentID', $params['ParentID']);
840
        }
841
842
        // Date filtering
843 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...
844
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
845
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
846
        }
847 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...
848
            $toDate = new DateField(null, null, $params['CreatedTo']);
849
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
850
        }
851
852
        // Categories
853
        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...
854
            $extensions = File::config()->app_categories[$filters['AppCategory']];
855
            $list = $list->filter('Name:PartialMatch', $extensions);
856
        }
857
858
        // Sort folders first
859
        $list = $list->sort(
860
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
861
        );
862
863
        // Pagination
864
        if (isset($filters['page']) && isset($filters['limit'])) {
865
            $page = $filters['page'];
866
            $limit = $filters['limit'];
867
            $offset = ($page - 1) * $limit;
868
            $list = $list->limit($limit, $offset);
869
        }
870
871
        // Access checks
872
        $list = $list->filterByCallback(function (File $file) {
873
            return $file->canView();
874
        });
875
876
        return $list;
877
    }
878
879
    /**
880
     * Action handler for adding pages to a campaign
881
     *
882
     * @param array $data
883
     * @param Form $form
884
     * @return DBHTMLText|HTTPResponse
885
     */
886
    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...
887
    {
888
        $id = $data['ID'];
889
        $record = $this->getList()->byID($id);
890
891
        $handler = AddToCampaignHandler::create($this, $record);
892
        $results = $handler->addToCampaign($record, $data['Campaign']);
893
        if (!is_null($results)) {
894
            $request = $this->getRequest();
895
            if ($request->getHeader('X-Formschema-Request')) {
896
                $data = $this->getSchemaForForm($handler->Form($record));
897
                $data['message'] = $results;
898
899
                $response = new HTTPResponse(Convert::raw2json($data));
900
                $response->addHeader('Content-Type', 'application/json');
901
                return $response;
902
            }
903
            return $results;
904
        }
905
    }
906
907
    /**
908
     * Url handler for add to campaign form
909
     *
910
     * @param HTTPRequest $request
911
     * @return Form
912
     */
913
    public function AddToCampaignForm($request)
914
    {
915
        // Get ID either from posted back value, or url parameter
916
        $id = $request->param('ID') ?: $request->postVar('ID');
917
        return $this->getAddToCampaignForm($id);
918
    }
919
920
    /**
921
     * @param int $id
922
     * @return Form
923
     */
924
    public function getAddToCampaignForm($id)
925
    {
926
        // Get record-specific fields
927
        $record = $this->getList()->byID($id);
928
929
        if (!$record) {
930
            $this->httpError(404, _t(
931
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
932
                'That {Type} couldn\'t be found',
933
                '',
934
                ['Type' => File::singleton()->i18n_singular_name()]
935
            ));
936
            return null;
937
        }
938 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...
939
            $this->httpError(403, _t(
940
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
941
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
942
                '',
943
                ['ObjectTitle' => $record->i18n_singular_name()]
944
            ));
945
            return null;
946
        }
947
948
        $handler = AddToCampaignHandler::create($this, $record);
949
        return $handler->Form($record);
950
    }
951
952
    /**
953
     * @return Upload
954
     */
955
    protected function getUpload()
956
    {
957
        $upload = Upload::create();
958
        $upload->getValidator()->setAllowedExtensions(
959
            // filter out '' since this would be a regex problem on JS end
960
            array_filter(File::config()->get('allowed_extensions'))
961
        );
962
963
        return $upload;
964
    }
965
}
966