Completed
Pull Request — master (#215)
by
unknown
03:10
created

AssetAdmin::save()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 30
Code Lines 19

Duplication

Lines 12
Ratio 40 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 12
loc 30
rs 8.439
cc 5
eloc 19
nc 4
nop 2
1
<?php
2
3
namespace SilverStripe\AssetAdmin\Controller;
4
5
use SilverStripe\Filesystem\Storage\AssetNameGenerator;
6
use SilverStripe\ORM\DataObject;
7
use SilverStripe\ORM\SS_List;
8
use SilverStripe\Security\Member;
9
use SilverStripe\Security\PermissionProvider;
10
use SilverStripe\Security\SecurityToken;
11
use SearchContext;
12
use LeftAndMain;
13
use DateField;
14
use DropdownField;
15
use Controller;
16
use FieldList;
17
use Form;
18
use CheckboxField;
19
use File;
20
use Requirements;
21
use CMSBatchActionHandler;
22
use Injector;
23
use Folder;
24
use HeaderField;
25
use FieldGroup;
26
use SS_HTTPRequest;
27
use SS_HTTPResponse;
28
use Upload;
29
use Config;
30
use FormAction;
31
use TextField;
32
use HiddenField;
33
use ReadonlyField;
34
use LiteralField;
35
use HTMLReadonlyField;
36
use DateField_Disabled;
37
38
/**
39
 * AssetAdmin is the 'file store' section of the CMS.
40
 * It provides an interface for manipulating the File and Folder objects in the system.
41
 *
42
 * @package cms
43
 * @subpackage assets
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 = '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
    );
97
98
    /**
99
     * Set up the controller
100
     */
101
    public function init()
102
    {
103
        parent::init();
104
105
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
106
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
107
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
108
109
        CMSBatchActionHandler::register('delete', 'SilverStripe\AssetAdmin\BatchAction\DeleteAssets', 'Folder');
110
    }
111
112
    public function getClientConfig()
113
    {
114
        $baseLink = $this->Link();
115
        return array_merge( parent::getClientConfig(), [
116
            'reactRouter' => true,
117
            'createFileEndpoint' => [
118
                'url' => Controller::join_links($baseLink, 'api/createFile'),
119
                'method' => 'post',
120
                'payloadFormat' => 'urlencoded',
121
            ],
122
            'createFolderEndpoint' => [
123
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
124
                'method' => 'post',
125
                'payloadFormat' => 'urlencoded',
126
            ],
127
            'readFolderEndpoint' => [
128
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
129
                'method' => 'get',
130
                'responseFormat' => 'json',
131
            ],
132
            'searchEndpoint' => [
133
                'url' => Controller::join_links($baseLink, 'api/search'),
134
                'method' => 'get',
135
                'responseFormat' => 'json',
136
            ],
137
            'updateFolderEndpoint' => [
138
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
139
                'method' => 'put',
140
                'payloadFormat' => 'urlencoded',
141
            ],
142
            'deleteEndpoint' => [
143
                'url' => Controller::join_links($baseLink, 'api/delete'),
144
                'method' => 'delete',
145
                'payloadFormat' => 'urlencoded',
146
            ],
147
            'limit' => $this->config()->page_length,
148
            'form' => [
149
                'FileEditForm' => [
150
                    'schemaUrl' => $this->Link('schema/FileEditForm')
151
                ],
152
            ],
153
        ]);
154
    }
155
156
    /**
157
     * Fetches a collection of files by ParentID.
158
     *
159
     * @param SS_HTTPRequest $request
160
     * @return SS_HTTPResponse
161
     */
162
    public function apiReadFolder(SS_HTTPRequest $request)
163
    {
164
        $params = $request->requestVars();
165
        $items = array();
166
        $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...
167
        $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...
168
169
        if (!isset($params['id']) && !strlen($params['id'])) {
170
            $this->httpError(400);
171
        }
172
173
        $folderID = (int)$params['id'];
174
        /** @var Folder $folder */
175
        $folder = $folderID ? Folder::get()->byID($folderID) : singleton('Folder');
176
177
        if (!$folder) {
178
            $this->httpError(400);
179
        }
180
181
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
182
        $files = $this->getList()->filter('ParentID', $folderID);
183
184
        if ($files) {
185
            foreach ($files as $file) {
186
                if (!$file->canView()) {
187
                    continue;
188
                }
189
190
                $items[] = $this->getObjectFromData($file);
191
            }
192
        }
193
194
        // Build parents (for breadcrumbs)
195
        $parents = [];
196
        $next = $folder->Parent();
197
        while($next && $next->exists()) {
198
            array_unshift($parents, [
199
                'id' => $next->ID,
200
                'title' => $next->getTitle(),
201
            ]);
202
            if($next->ParentID) {
203
                $next = $next->Parent();
204
            } else {
205
                break;
206
            }
207
        }
208
209
        // Build response
210
        $response = new SS_HTTPResponse();
211
        $response->addHeader('Content-Type', 'application/json');
212
        $response->setBody(json_encode([
213
            'files' => $items,
214
            'title' => $folder->getTitle(),
215
            'count' => count($items),
216
            'parents' => $parents,
217
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
218
            'folderID' => $folderID,
219
            'canEdit' => $folder->canEdit(),
220
            'canDelete' => $folder->canDelete(),
221
        ]));
222
223
        return $response;
224
    }
225
226
    /**
227
     * @param SS_HTTPRequest $request
228
     *
229
     * @return SS_HTTPResponse
230
     */
231
    public function apiSearch(SS_HTTPRequest $request)
232
    {
233
        $params = $request->getVars();
234
        $list = $this->getList($params);
235
236
        $response = new SS_HTTPResponse();
237
        $response->addHeader('Content-Type', 'application/json');
238
        $response->setBody(json_encode([
239
            // Serialisation
240
            "files" => array_map(function($file) {
241
                return $this->getObjectFromData($file);
242
            }, $list->toArray()),
243
            "count" => $list->count(),
244
        ]));
245
246
        return $response;
247
    }
248
249
    /**
250
     * @param SS_HTTPRequest $request
251
     *
252
     * @return SS_HTTPResponse
253
     */
254
    public function apiDelete(SS_HTTPRequest $request)
255
    {
256
        parse_str($request->getBody(), $vars);
257
258
        // CSRF check
259
        $token = SecurityToken::inst();
260 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...
261
            return new SS_HTTPResponse(null, 400);
262
        }
263
264 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...
265
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
266
                ->addHeader('Content-Type', 'application/json');
267
        }
268
269
        $fileIds = $vars['ids'];
270
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
271
272 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...
273
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
274
                ->addHeader('Content-Type', 'application/json');
275
        }
276
277
        if (!min(array_map(function ($file) {
278
            return $file->canDelete();
279
        }, $files))) {
280
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
281
                ->addHeader('Content-Type', 'application/json');
282
        }
283
284
        foreach ($files as $file) {
285
            $file->delete();
286
        }
287
288
        return (new SS_HTTPResponse(json_encode(['status' => 'file was deleted'])))
289
            ->addHeader('Content-Type', 'application/json');
290
    }
291
292
    /**
293
     * Creates a single file based on a form-urlencoded upload.
294
     *
295
     * @param SS_HTTPRequest $request
296
     * @return SS_HTTPRequest|SS_HTTPResponse
297
     */
298
    public function apiCreateFile(SS_HTTPRequest $request)
299
    {
300
        $data = $request->postVars();
301
        $upload = $this->getUpload();
302
303
        // CSRF check
304
        $token = SecurityToken::inst();
305 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...
306
            return new SS_HTTPResponse(null, 400);
307
        }
308
309
        // check canAddChildren permissions
310
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
311
            $parentRecord = Folder::get()->byID($data['ParentID']);
312 View Code Duplication
            if ($parentRecord->hasMethod('canAddChildren') && !$parentRecord->canAddChildren()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
313
                return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
314
                    ->addHeader('Content-Type', 'application/json');
315
            }
316
        } else {
317
            $parentRecord = singleton('Folder');
318
        }
319
320
        // check create permissions
321
        if (!$parentRecord->canCreate()) {
322
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
323
                ->addHeader('Content-Type', 'application/json');
324
        }
325
326
        $tmpFile = $request->postVar('Upload');
327
        if(!$upload->validate($tmpFile)) {
328
            $result = ['error' => $upload->getErrors()];
329
            return (new SS_HTTPResponse(json_encode($result), 400))
330
                ->addHeader('Content-Type', 'application/json');
331
        }
332
333
        // TODO Allow batch uploads
334
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
335
        $file = Injector::inst()->create($fileClass);
336
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
337 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...
338
            $result = ['error' => 'unknown'];
339
            return (new SS_HTTPResponse(json_encode($result), 400))
340
                ->addHeader('Content-Type', 'application/json');
341
        }
342
343
        $file->ParentID = $parentRecord->ID;
344
        $file->write();
345
346
        $result = [$this->getObjectFromData($file)];
347
348
        return (new SS_HTTPResponse(json_encode($result)))
349
            ->addHeader('Content-Type', 'application/json');
350
    }
351
352
    /**
353
     * Creates a single folder, within an optional parent folder.
354
     *
355
     * @param SS_HTTPRequest $request
356
     * @return SS_HTTPRequest|SS_HTTPResponse
357
     */
358
    public function apiCreateFolder(SS_HTTPRequest $request)
359
    {
360
        $data = $request->postVars();
361
362
        $class = 'Folder';
363
364
        // CSRF check
365
        $token = SecurityToken::inst();
366 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...
367
            return new SS_HTTPResponse(null, 400);
368
        }
369
370
        // check addchildren permissions
371
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
372
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
373 View Code Duplication
            if ($parentRecord->hasMethod('canAddChildren') && !$parentRecord->canAddChildren()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
374
                return (new SS_HTTPResponse(null, 403))
375
                    ->addHeader('Content-Type', 'application/json');
376
            }
377
        } else {
378
            $parentRecord = singleton($class);
379
        }
380
        $data['ParentID'] = ($parentRecord->exists()) ? (int)$parentRecord->ID : 0;
381
382
        // check create permissions
383
        if (!$parentRecord->canCreate()) {
384
            return (new SS_HTTPResponse(null, 403))
385
                ->addHeader('Content-Type', 'application/json');
386
        }
387
388
        // Build filename
389
        $baseFilename = isset($data['Name'])
390
            ? basename($data['Name'])
391
            : _t('AssetAdmin.NEWFOLDER', "NewFolder");
392
393
        if ($parentRecord && $parentRecord->ID) {
394
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
395
        }
396
397
        // Ensure name is unique
398
        $nameGenerator = $this->getNameGenerator($baseFilename);
399
        foreach ($nameGenerator as $filename) {
400
            if (! File::find($filename)) {
401
                break;
402
            }
403
        }
404
        $data['Name'] = basename($filename);
0 ignored issues
show
Bug introduced by
The variable $filename seems to be defined by a foreach iteration on line 399. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
405
406
        // Create record
407
        $record = $class::create();
408
        $record->ParentID = $data['ParentID'];
409
        $record->Name = $record->Title = basename($data['Name']);
410
        $record->write();
411
412
        $result = [
413
            "ParentID" => $record->ParentID,
414
            "ID" => $record->ID,
415
            "Filename" => $record->Filename,
416
        ];
417
418
        return (new SS_HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
419
    }
420
421
    /**
422
     * Redirects 3.x style detail links to new 4.x style routing.
423
     *
424
     * @param $request
425
     */
426
    public function legacyRedirectForEditView($request)
427
    {
428
        $fileID = $request->param('FileID');
429
        $file = File::get()->byID($fileID);
430
        $link = $this->getFileEditLink($file) ?: $this->Link();
0 ignored issues
show
Documentation introduced by
$file is of type object<SilverStripe\ORM\DataObject>|null, but the function expects a object<File>.

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...
431
        $this->redirect($link);
432
    }
433
434
    /**
435
     * Given a file return the CMS link to edit it
436
     *
437
     * @param File $file
438
     * @return string
439
     */
440
    public function getFileEditLink($file) {
441
        if(!$file || !$file->isInDB()) {
442
            return null;
443
        }
444
445
        return Controller::join_links(
446
            $this->Link('show'),
447
            $file->ParentID,
448
            'edit',
449
            $file->ID
450
        );
451
    }
452
453
    /**
454
     * Get the search context from {@link File}, used to create the search form
455
     * as well as power the /search API endpoint.
456
     *
457
     * @return SearchContext
458
     */
459
    public function getSearchContext()
460
    {
461
        $context = singleton('File')->getDefaultSearchContext();
462
463
        // Customize fields
464
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
465
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
466
        ->setConfig('showcalendar', true);
467
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
468
        ->setConfig('showcalendar', true);
469
        $dateGroup = FieldGroup::create(
470
            $dateHeader,
471
            $dateFrom,
472
            $dateTo
473
        );
474
        $context->addField($dateGroup);
475
        $appCategories = array(
476
            'archive' => _t('AssetAdmin.AppCategoryArchive', 'Archive', 'A collection of files'),
477
            'audio' => _t('AssetAdmin.AppCategoryAudio', 'Audio'),
478
            'document' => _t('AssetAdmin.AppCategoryDocument', 'Document'),
479
            'flash' => _t('AssetAdmin.AppCategoryFlash', 'Flash', 'The fileformat'),
480
            'image' => _t('AssetAdmin.AppCategoryImage', 'Image'),
481
            'video' => _t('AssetAdmin.AppCategoryVideo', 'Video'),
482
        );
483
        $context->addField(
484
            $typeDropdown = new DropdownField(
485
                'AppCategory',
486
                _t('AssetAdmin.Filetype', 'File type'),
487
                $appCategories
488
            )
489
        );
490
491
        $typeDropdown->setEmptyString(' ');
492
493
        $context->addField(
494
            new CheckboxField('CurrentFolderOnly', _t('AssetAdmin.CurrentFolderOnly', 'Limit to current folder?'))
495
        );
496
        $context->getFields()->removeByName('Title');
497
498
        return $context;
499
    }
500
501
    /**
502
     * Get an asset renamer for the given filename.
503
     *
504
     * @param  string             $filename Path name
505
     * @return AssetNameGenerator
506
     */
507
    protected function getNameGenerator($filename)
508
    {
509
        return Injector::inst()
510
            ->createWithArgs('AssetNameGenerator', array($filename));
511
    }
512
513
    /**
514
     * @todo Implement on client
515
     */
516
    public function breadcrumbs($unlinked = false)
517
    {
518
        return null;
519
    }
520
521
522
    /**
523
     * Don't include class namespace in auto-generated CSS class
524
     */
525
    public function baseCSSClasses()
526
    {
527
        return 'AssetAdmin LeftAndMain';
528
    }
529
530
    public function providePermissions()
531
    {
532
        $title = _t("AssetAdmin.MENUTITLE", LeftAndMain::menu_title_for_class($this->class));
0 ignored issues
show
Deprecated Code introduced by
The method LeftAndMain::menu_title_for_class() has been deprecated with message: 5.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
533
534
        return array(
535
            "CMS_ACCESS_AssetAdmin" => array(
536
                'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
0 ignored issues
show
Documentation introduced by
array('title' => $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...
537
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
538
            )
539
        );
540
    }
541
542
    /**
543
     * The form is used to generate a form schema,
544
     * as well as an intermediary object to process data through API endpoints.
545
     * Since it's used directly on API endpoints, it does not have any form actions.
546
     *
547
     * @param Folder $folder
548
     * @return Form
549
     */
550
    public function getFolderEditForm(Folder $folder)
551
    {
552
        $form = Form::create(
553
            $this,
554
            'FolderEditForm',
555
            $folder->getCMSFields(),
556
            FieldList::create()
557
        );
558
559
        return $form;
560
    }
561
562
    /**
563
     * See {@link getFolderEditForm()} for details.
564
     *
565
     * @param int $id
566
     * @return Form
567
     */
568
    public function getFileEditForm($id)
569
    {
570
        /** @var File $file */
571
        $file = $this->getList()->byID($id);
572
573
        // TODO use $file->getCMSFields()
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
574
        $fields = FieldList::create([
575
            HeaderField::create('TitleHeader', $file->Title, 1),
576
            LiteralField::create("ImageFull", $file->PreviewThumbnail()),
577
            TextField::create("Title", _t('AssetTableField.TITLE','Title')),
578
            TextField::create("Name", _t('AssetTableField.FILENAME','Filename')),
579
            ReadonlyField::create(
580
                'Location',
581
                _t('AssetTableField.FOLDER', 'Folder'),
582
                dirname($file->getSourceURL())
583
            ),
584
            HiddenField::create('ID', $id),
585
        ]);
586
        if ($file->getIsImage()) {
587
            $fields->push(ReadonlyField::create(
588
                "DisplaySize",
589
                "Size",
590
                sprintf('%spx, %s', $file->getDimensions(), $file->getSize()))
591
            );
592
            $fields->push(HTMLReadonlyField::create(
593
                'ClickableURL',
594
                _t('AssetTableField.URL','URL'),
595
                sprintf('<a href="%s" target="_blank">%s</a>', $file->Link(), $file->Link())
596
            ));
597
            $fields->push(DateField_Disabled::create(
598
                "LastEdited",
599
                _t('AssetTableField.LASTEDIT','Last changed')
600
            ));
601
        }
602
603
        $actions = FieldList::create([
604
            FormAction::create('save', _t('CMSMain.SAVE', 'Save'))
605
                ->setIcon('save'),
606
        ]);
607
608
        $form = Form::create(
609
            $this,
610
            'FileEditForm',
611
            $fields,
612
            $actions
613
        );
614
615
        // Load into form
616
        if($id && $file) {
617
            $form->loadDataFrom($file);
618
        }
619
620
        // Configure form to respond to validation errors with form schema
621
        // if requested via react.
622
        $form->setValidationResponseCallback(function() use ($form) {
623
            return $this->getSchemaResponse($form);
624
        });
625
626
        return $form;
627
    }
628
629
    /**
630
     * Get file edit form
631
     *
632
     * @return Form
633
     */
634
    public function FileEditForm()
635
    {
636
        // Get ID either from posted back value, or url parameter
637
        $request = $this->getRequest();
638
        $id = $request->param('ID') ?: $request->postVar('ID');
639
        return $this->getFileEditForm($id);
640
    }
641
642
    /**
643
     * @param array $data
644
     * @param Form $form
645
     * @return SS_HTTPResponse
646
     */
647
    public function save($data, $form)
648
    {
649 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...
650
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
651
                ->addHeader('Content-Type', 'application/json');
652
        }
653
654
        $id = (int) $data['ID'];
655
        $record = $this->getList()->filter('ID', $id)->first();
656
657 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...
658
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
659
                ->addHeader('Content-Type', 'application/json');
660
        }
661
662 View Code Duplication
        if (!$record->canEdit()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
663
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
664
                ->addHeader('Content-Type', 'application/json');
665
        }
666
667
        $form->saveInto($record);
668
        $record->write();
669
670
        // Return the record data in the same response as the schema to save a postback
671
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
672
        $schemaData['record'] = $this->getObjectFromData($record);
673
        $response = new SS_HTTPResponse(\Convert::raw2json($schemaData));
674
        $response->addHeader('Content-Type', 'application/json');
675
        return $response;
676
    }
677
678
    /**
679
     * @param File $file
680
     *
681
     * @return array
682
     */
683
    protected function getObjectFromData(File $file)
684
    {
685
        $object = array(
686
            'id' => $file->ID,
687
            'created' => $file->Created,
688
            'lastUpdated' => $file->LastEdited,
689
            'owner' => null,
690
            'parent' => null,
691
            'title' => $file->Title,
692
            'exists' => $file->exists(), // Broken file check
693
            'type' => $file->is_a('Folder') ? 'folder' : $file->FileType,
694
            'category' => $file->is_a('Folder') ? 'folder' : $file->appCategory(),
695
            'name' => $file->Name,
696
            'filename' => $file->Filename,
697
            '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...
698
            'size' => $file->Size,
699
            'url' => $file->AbsoluteURL,
700
            'canEdit' => $file->canEdit(),
701
            'canDelete' => $file->canDelete()
702
        );
703
704
        /** @var Member $owner */
705
        $owner = $file->Owner();
706
707
        if ($owner) {
708
            $object['owner'] = array(
709
                'id' => $owner->ID,
710
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
711
            );
712
        }
713
714
        /** @var Folder $parent */
715
        $parent = $file->Parent();
716
717
        if ($parent) {
718
            $object['parent'] = array(
719
                'id' => $parent->ID,
720
                'title' => $parent->Title,
721
                'filename' => $parent->Filename,
722
            );
723
        }
724
725
        /** @var File $file */
726
        if ($file->getIsImage()) {
727
            $object['dimensions']['width'] = $file->Width;
728
            $object['dimensions']['height'] = $file->Height;
0 ignored issues
show
Bug introduced by
The property Height does not seem to exist. Did you mean cms_thumbnail_height?

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

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

Loading history...
729
        }
730
731
        return $object;
732
    }
733
734
735
    /**
736
     * Returns the files and subfolders contained in the currently selected folder,
737
     * defaulting to the root node. Doubles as search results, if any search parameters
738
     * are set through {@link SearchForm()}.
739
     *
740
     * @param array $params Unsanitised request parameters
741
     * @return SS_List
742
     */
743
    protected function getList($params = array())
744
    {
745
        $context = $this->getSearchContext();
746
747
        // Overwrite name filter to search both Name and Title attributes
748
        $context->removeFilterByName('Name');
749
750
        // Lazy loaded list. Allows adding new filters through SearchContext.
751
        $list = $context->getResults($params);
752
753
        // Re-add previously removed "Name" filter as combined filter
754
        // TODO Replace with composite SearchFilter once that API exists
755
        if(!empty($params['Name'])) {
756
            $list = $list->filterAny(array(
757
                'Name:PartialMatch' => $params['Name'],
758
                'Title:PartialMatch' => $params['Name']
759
            ));
760
        }
761
762
        // Optionally limit search to a folder (non-recursive)
763
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
764
            $list = $list->filter('ParentID', $params['ParentID']);
765
        }
766
767
        // Date filtering
768
        if (!empty($params['CreatedFrom'])) {
769
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
770
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
771
        }
772
        if (!empty($params['CreatedTo'])) {
773
            $toDate = new DateField(null, null, $params['CreatedTo']);
774
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
775
        }
776
777
        // Categories
778
        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...
779
            $extensions = File::config()->app_categories[$filters['AppCategory']];
780
            $list = $list->filter('Name:PartialMatch', $extensions);
781
        }
782
783
        // Sort folders first
784
        $list = $list->sort(
785
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
786
        );
787
788
        // Pagination
789
        if (isset($filters['page']) && isset($filters['limit'])) {
790
            $page = $filters['page'];
791
            $limit = $filters['limit'];
792
            $offset = ($page - 1) * $limit;
793
            $list = $list->limit($limit, $offset);
794
        }
795
796
        // Access checks
797
        $list = $list->filterByCallback(function($file) {return $file->canView();});
798
799
        return $list;
800
    }
801
802
    /**
803
     * @return Upload
804
     */
805
    protected function getUpload()
806
    {
807
        $upload = Upload::create();
808
        $upload->getValidator()->setAllowedExtensions(
809
            // filter out '' since this would be a regex problem on JS end
810
            array_filter(Config::inst()->get('File', 'allowed_extensions'))
811
        );
812
813
        return $upload;
814
    }
815
}
816