Completed
Pull Request — master (#233)
by Ingo
02:24
created

AssetAdmin::getFileEditForm()   C

Complexity

Conditions 7
Paths 16

Size

Total Lines 74
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 0 Features 2
Metric Value
c 8
b 0
f 2
dl 0
loc 74
rs 6.6374
cc 7
eloc 47
nc 16
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Filesystem\Storage\AssetNameGenerator;
9
use SilverStripe\ORM\ArrayList;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\FieldType\DBHTMLText;
13
use SilverStripe\Security\Member;
14
use SilverStripe\Security\PermissionProvider;
15
use SilverStripe\Security\SecurityToken;
16
use SearchContext;
17
use DateField;
18
use DropdownField;
19
use Controller;
20
use FieldList;
21
use Form;
22
use CheckboxField;
23
use File;
24
use Requirements;
25
use Injector;
26
use Folder;
27
use HeaderField;
28
use FieldGroup;
29
use SS_HTTPRequest;
30
use SS_HTTPResponse;
31
use Upload;
32
use Config;
33
use FormAction;
34
use TextField;
35
use HiddenField;
36
use ReadonlyField;
37
use LiteralField;
38
use PopoverField;
39
use HTMLReadonlyField;
40
use Convert;
41
use DatetimeField;
42
43
/**
44
 * AssetAdmin is the 'file store' section of the CMS.
45
 * It provides an interface for manipulating the File and Folder objects in the system.
46
 *
47
 * @package cms
48
 * @subpackage assets
49
 */
50
class AssetAdmin extends LeftAndMain implements PermissionProvider
51
{
52
    private static $url_segment = 'assets';
0 ignored issues
show
Unused Code introduced by
The property $url_segment is not used and could be removed.

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

Loading history...
53
54
    private static $url_rule = '/$Action/$ID';
0 ignored issues
show
Unused Code introduced by
The property $url_rule is not used and could be removed.

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

Loading history...
55
56
    private static $menu_title = 'Files';
0 ignored issues
show
Unused Code introduced by
The property $menu_title is not used and could be removed.

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

Loading history...
57
58
    private static $tree_class = 'Folder';
0 ignored issues
show
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...
59
60
    private static $url_handlers = [
0 ignored issues
show
Unused Code introduced by
The property $url_handlers is not used and could be removed.

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

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

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

Loading history...
93
        'legacyRedirectForEditView',
94
        'apiCreateFolder',
95
        'apiCreateFile',
96
        'apiReadFolder',
97
        'apiUpdateFolder',
98
        'apiDelete',
99
        'apiSearch',
100
        'FileEditForm',
101
        'AddToCampaignForm',
102
    );
103
104
    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...
105
106
    /**
107
     * Set up the controller
108
     */
109
    public function init()
110
    {
111
        parent::init();
112
113
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
114
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
115
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
116
117
        CMSBatchActionHandler::register('delete', 'SilverStripe\AssetAdmin\BatchAction\DeleteAssets', 'Folder');
118
    }
119
120
    public function getClientConfig()
121
    {
122
        $baseLink = $this->Link();
123
        return array_merge( parent::getClientConfig(), [
124
            'reactRouter' => true,
125
            'createFileEndpoint' => [
126
                'url' => Controller::join_links($baseLink, 'api/createFile'),
127
                'method' => 'post',
128
                'payloadFormat' => 'urlencoded',
129
            ],
130
            'createFolderEndpoint' => [
131
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
132
                'method' => 'post',
133
                'payloadFormat' => 'urlencoded',
134
            ],
135
            'readFolderEndpoint' => [
136
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
137
                'method' => 'get',
138
                'responseFormat' => 'json',
139
            ],
140
            'searchEndpoint' => [
141
                'url' => Controller::join_links($baseLink, 'api/search'),
142
                'method' => 'get',
143
                'responseFormat' => 'json',
144
            ],
145
            'updateFolderEndpoint' => [
146
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
147
                'method' => 'put',
148
                'payloadFormat' => 'urlencoded',
149
            ],
150
            'deleteEndpoint' => [
151
                'url' => Controller::join_links($baseLink, 'api/delete'),
152
                'method' => 'delete',
153
                'payloadFormat' => 'urlencoded',
154
            ],
155
            'limit' => $this->config()->page_length,
156
            'form' => [
157
                'FileEditForm' => [
158
                    'schemaUrl' => $this->Link('schema/FileEditForm')
159
                ],
160
                'AddToCampaignForm' => [
161
                    'schemaUrl' => $this->Link('schema/AddToCampaignForm')
162
                ],
163
            ],
164
        ]);
165
    }
166
167
    /**
168
     * Fetches a collection of files by ParentID.
169
     *
170
     * @param SS_HTTPRequest $request
171
     * @return SS_HTTPResponse
172
     */
173
    public function apiReadFolder(SS_HTTPRequest $request)
174
    {
175
        $params = $request->requestVars();
176
        $items = array();
177
        $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...
178
        $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...
179
180
        if (!isset($params['id']) && !strlen($params['id'])) {
181
            $this->httpError(400);
182
        }
183
184
        $folderID = (int)$params['id'];
185
        /** @var Folder $folder */
186
        $folder = $folderID ? Folder::get()->byID($folderID) : singleton('Folder');
187
188
        if (!$folder) {
189
            $this->httpError(400);
190
        }
191
192
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
193
        $files = $this->getList()->filter('ParentID', $folderID);
194
195
        if ($files) {
196
            /** @var File $file */
197
            foreach ($files as $file) {
198
                if (!$file->canView()) {
199
                    continue;
200
                }
201
202
                $items[] = $this->getObjectFromData($file);
203
            }
204
        }
205
206
        // Build parents (for breadcrumbs)
207
        $parents = [];
208
        $next = $folder->Parent();
209
        while($next && $next->exists()) {
210
            array_unshift($parents, [
211
                'id' => $next->ID,
212
                'title' => $next->getTitle(),
213
            ]);
214
            if($next->ParentID) {
215
                $next = $next->Parent();
216
            } else {
217
                break;
218
            }
219
        }
220
221
        // Build response
222
        $response = new SS_HTTPResponse();
223
        $response->addHeader('Content-Type', 'application/json');
224
        $response->setBody(json_encode([
225
            'files' => $items,
226
            'title' => $folder->getTitle(),
227
            'count' => count($items),
228
            'parents' => $parents,
229
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
230
            'folderID' => $folderID,
231
            'canEdit' => $folder->canEdit(),
232
            'canDelete' => $folder->canDelete(),
233
        ]));
234
235
        return $response;
236
    }
237
238
    /**
239
     * @param SS_HTTPRequest $request
240
     *
241
     * @return SS_HTTPResponse
242
     */
243
    public function apiSearch(SS_HTTPRequest $request)
244
    {
245
        $params = $request->getVars();
246
        $list = $this->getList($params);
247
248
        $response = new SS_HTTPResponse();
249
        $response->addHeader('Content-Type', 'application/json');
250
        $response->setBody(json_encode([
251
            // Serialisation
252
            "files" => array_map(function($file) {
253
                return $this->getObjectFromData($file);
254
            }, $list->toArray()),
255
            "count" => $list->count(),
256
        ]));
257
258
        return $response;
259
    }
260
261
    /**
262
     * @param SS_HTTPRequest $request
263
     *
264
     * @return SS_HTTPResponse
265
     */
266
    public function apiDelete(SS_HTTPRequest $request)
267
    {
268
        parse_str($request->getBody(), $vars);
269
270
        // CSRF check
271
        $token = SecurityToken::inst();
272 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...
273
            return new SS_HTTPResponse(null, 400);
274
        }
275
276 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...
277
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
278
                ->addHeader('Content-Type', 'application/json');
279
        }
280
281
        $fileIds = $vars['ids'];
282
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
283
284 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...
285
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
286
                ->addHeader('Content-Type', 'application/json');
287
        }
288
289
        if (!min(array_map(function (File $file) {
290
            return $file->canDelete();
291
        }, $files))) {
292
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
293
                ->addHeader('Content-Type', 'application/json');
294
        }
295
296
        /** @var File $file */
297
        foreach ($files as $file) {
298
            $file->delete();
299
        }
300
301
        return (new SS_HTTPResponse(json_encode(['status' => 'file was deleted'])))
302
            ->addHeader('Content-Type', 'application/json');
303
    }
304
305
    /**
306
     * Creates a single file based on a form-urlencoded upload.
307
     *
308
     * @param SS_HTTPRequest $request
309
     * @return SS_HTTPRequest|SS_HTTPResponse
310
     */
311
    public function apiCreateFile(SS_HTTPRequest $request)
312
    {
313
        $data = $request->postVars();
314
        $upload = $this->getUpload();
315
316
        // CSRF check
317
        $token = SecurityToken::inst();
318 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...
319
            return new SS_HTTPResponse(null, 400);
320
        }
321
322
        // check canAddChildren permissions
323
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
324
            $parentRecord = Folder::get()->byID($data['ParentID']);
325 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...
326
                return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
327
                    ->addHeader('Content-Type', 'application/json');
328
            }
329
        } else {
330
            $parentRecord = singleton('Folder');
331
        }
332
333
        // check create permissions
334
        if (!$parentRecord->canCreate()) {
335
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
336
                ->addHeader('Content-Type', 'application/json');
337
        }
338
339
        $tmpFile = $request->postVar('Upload');
340
        if(!$upload->validate($tmpFile)) {
341
            $result = ['error' => $upload->getErrors()];
342
            return (new SS_HTTPResponse(json_encode($result), 400))
343
                ->addHeader('Content-Type', 'application/json');
344
        }
345
346
        // TODO Allow batch uploads
347
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
348
        $file = Injector::inst()->create($fileClass);
349
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
350 View Code Duplication
        if(!$uploadResult) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
351
            $result = ['error' => 'unknown'];
352
            return (new SS_HTTPResponse(json_encode($result), 400))
353
                ->addHeader('Content-Type', 'application/json');
354
        }
355
356
        $file->ParentID = $parentRecord->ID;
357
        $file->write();
358
359
        $result = [$this->getObjectFromData($file)];
360
361
        return (new SS_HTTPResponse(json_encode($result)))
362
            ->addHeader('Content-Type', 'application/json');
363
    }
364
365
    /**
366
     * Creates a single folder, within an optional parent folder.
367
     *
368
     * @param SS_HTTPRequest $request
369
     * @return SS_HTTPRequest|SS_HTTPResponse
370
     */
371
    public function apiCreateFolder(SS_HTTPRequest $request)
372
    {
373
        $data = $request->postVars();
374
375
        $class = 'Folder';
376
377
        // CSRF check
378
        $token = SecurityToken::inst();
379 View Code Duplication
        if (empty($data[$token->getName()]) || !$token->check($data[$token->getName()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
668
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
669
                ->addHeader('Content-Type', 'application/json');
670
        }
671
672 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...
673
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
674
                ->addHeader('Content-Type', 'application/json');
675
        }
676
677
        $form->saveInto($record);
678
        $record->write();
679
680
        // Return the record data in the same response as the schema to save a postback
681
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
682
        $schemaData['record'] = $this->getObjectFromData($record);
683
        $response = new SS_HTTPResponse(\Convert::raw2json($schemaData));
684
        $response->addHeader('Content-Type', 'application/json');
685
        return $response;
686
    }
687
688
    /**
689
     * @param File $file
690
     *
691
     * @return array
692
     */
693
    protected function getObjectFromData(File $file)
694
    {
695
        $object = array(
696
            'id' => $file->ID,
697
            'created' => $file->Created,
698
            'lastUpdated' => $file->LastEdited,
699
            'owner' => null,
700
            'parent' => null,
701
            'title' => $file->Title,
702
            'exists' => $file->exists(), // Broken file check
703
            'type' => $file->is_a('Folder') ? 'folder' : $file->FileType,
704
            'category' => $file->is_a('Folder') ? 'folder' : $file->appCategory(),
705
            'name' => $file->Name,
706
            'filename' => $file->Filename,
707
            '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...
708
            'size' => $file->Size,
709
            'url' => $file->AbsoluteURL,
710
            'canEdit' => $file->canEdit(),
711
            'canDelete' => $file->canDelete()
712
        );
713
714
        /** @var Member $owner */
715
        $owner = $file->Owner();
716
717
        if ($owner) {
718
            $object['owner'] = array(
719
                'id' => $owner->ID,
720
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
721
            );
722
        }
723
724
        /** @var Folder $parent */
725
        $parent = $file->Parent();
726
727
        if ($parent) {
728
            $object['parent'] = array(
729
                'id' => $parent->ID,
730
                'title' => $parent->Title,
731
                'filename' => $parent->Filename,
732
            );
733
        }
734
735
        /** @var File $file */
736
        if ($file->getIsImage()) {
737
            $object['dimensions']['width'] = $file->Width;
738
            $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...
739
        }
740
741
        return $object;
742
    }
743
744
745
    /**
746
     * Returns the files and subfolders contained in the currently selected folder,
747
     * defaulting to the root node. Doubles as search results, if any search parameters
748
     * are set through {@link SearchForm()}.
749
     *
750
     * @param array $params Unsanitised request parameters
751
     * @return DataList
752
     */
753
    protected function getList($params = array())
754
    {
755
        $context = $this->getSearchContext();
756
757
        // Overwrite name filter to search both Name and Title attributes
758
        $context->removeFilterByName('Name');
759
760
        // Lazy loaded list. Allows adding new filters through SearchContext.
761
        $list = $context->getResults($params);
762
763
        // Re-add previously removed "Name" filter as combined filter
764
        // TODO Replace with composite SearchFilter once that API exists
765
        if(!empty($params['Name'])) {
766
            $list = $list->filterAny(array(
767
                'Name:PartialMatch' => $params['Name'],
768
                'Title:PartialMatch' => $params['Name']
769
            ));
770
        }
771
772
        // Optionally limit search to a folder (non-recursive)
773
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
774
            $list = $list->filter('ParentID', $params['ParentID']);
775
        }
776
777
        // Date filtering
778
        if (!empty($params['CreatedFrom'])) {
779
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
780
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
781
        }
782
        if (!empty($params['CreatedTo'])) {
783
            $toDate = new DateField(null, null, $params['CreatedTo']);
784
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
785
        }
786
787
        // Categories
788
        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...
789
            $extensions = File::config()->app_categories[$filters['AppCategory']];
790
            $list = $list->filter('Name:PartialMatch', $extensions);
791
        }
792
793
        // Sort folders first
794
        $list = $list->sort(
795
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
796
        );
797
798
        // Pagination
799
        if (isset($filters['page']) && isset($filters['limit'])) {
800
            $page = $filters['page'];
801
            $limit = $filters['limit'];
802
            $offset = ($page - 1) * $limit;
803
            $list = $list->limit($limit, $offset);
804
        }
805
806
        // Access checks
807
        $list = $list->filterByCallback(function(File $file) {
808
            return $file->canView();
809
        });
810
811
        return $list;
812
    }
813
814
    /**
815
     * Action handler for adding pages to a campaign
816
     *
817
     * @param array $data
818
     * @param Form $form
819
     * @return DBHTMLText|SS_HTTPResponse
820
     */
821
    public function addtocampaign($data, $form)
822
    {
823
        $id = $data['ID'];
824
        $record = $this->getList()->byID($id);
825
826
        $handler = AddToCampaignHandler::create($this, $record);
827
        $results = $handler->addToCampaign($record, $data['Campaign']);
828
        if (!is_null($results)) {
829
            $request = $this->getRequest();
830
            if($request->getHeader('X-Formschema-Request')) {
831
                $data = $this->getSchemaForForm($handler->Form($record));
832
                $data['message'] = $results;
833
834
                $response = new SS_HTTPResponse(Convert::raw2json($data));
835
                $response->addHeader('Content-Type', 'application/json');
836
                return $response;
837
            }
838
            return $results;
839
        }
840
    }
841
842
    /**
843
     * Url handler for add to campaign form
844
     *
845
     * @param SS_HTTPRequest $request
846
     * @return Form
847
     */
848
    public function AddToCampaignForm($request)
849
    {
850
        // Get ID either from posted back value, or url parameter
851
        $id = $request->param('ID') ?: $request->postVar('ID');
852
        return $this->getAddToCampaignForm($id);
853
    }
854
855
    /**
856
     * @param int $id
857
     * @return Form
858
     */
859
    public function getAddToCampaignForm($id)
860
    {
861
        // Get record-specific fields
862
        $record = $this->getList()->byID($id);
863
864
        if (!$record) {
865
            $this->httpError(404, _t(
866
                'AssetAdmin.ErrorNotFound',
867
                'That {Type} couldn\'t be found',
868
                '',
869
                ['Type' => _t('File.SINGULARNAME')]
0 ignored issues
show
Documentation introduced by
array('Type' => _t('File.SINGULARNAME')) is of type array<string,string,{"Type":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
870
            ));
871
            return null;
872
        }
873
        if (!$record->canView()) {
874
            $this->httpError(403, _t(
875
                'AssetAdmin.ErrorItemPermissionDenied',
876
                'It seems you don\'t have the necessary permissions to add {ObjectTitle} to a campaign',
877
                '',
878
                ['ObjectTitle' => _t('File.SINGULARNAME')]
0 ignored issues
show
Documentation introduced by
array('ObjectTitle' => _t('File.SINGULARNAME')) is of type array<string,string,{"ObjectTitle":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
879
            ));
880
            return null;
881
        }
882
883
        $handler = AddToCampaignHandler::create($this, $record);
884
        return $handler->Form($record);
885
    }
886
887
    /**
888
     * @return Upload
889
     */
890
    protected function getUpload()
891
    {
892
        $upload = Upload::create();
893
        $upload->getValidator()->setAllowedExtensions(
894
            // filter out '' since this would be a regex problem on JS end
895
            array_filter(Config::inst()->get('File', 'allowed_extensions'))
896
        );
897
898
        return $upload;
899
    }
900
}
901