Completed
Pull Request — master (#322)
by Damian
01:59
created

AssetAdmin::apiReadFolder()   C

Complexity

Conditions 13
Paths 48

Size

Total Lines 66
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 66
rs 5.8968
cc 13
eloc 41
nc 48
nop 1

How to fix   Long Method    Complexity   

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 InvalidArgumentException;
6
use SilverStripe\Admin\AddToCampaignHandler;
7
use SilverStripe\Admin\CMSBatchActionHandler;
8
use SilverStripe\Admin\LeftAndMain;
9
use SilverStripe\AssetAdmin\Forms\UploadField;
10
use SilverStripe\AssetAdmin\Forms\FileFormFactory;
11
use SilverStripe\AssetAdmin\Forms\FolderFormFactory;
12
use SilverStripe\AssetAdmin\Forms\FileHistoryFormFactory;
13
use SilverStripe\AssetAdmin\Forms\ImageFormFactory;
14
use SilverStripe\Assets\File;
15
use SilverStripe\Assets\Folder;
16
use SilverStripe\Assets\Image;
17
use SilverStripe\Assets\Storage\AssetNameGenerator;
18
use SilverStripe\Assets\Upload;
19
use SilverStripe\Control\Controller;
20
use SilverStripe\Control\HTTPRequest;
21
use SilverStripe\Control\HTTPResponse;
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\ORM\ValidationResult;
37
use SilverStripe\Security\Member;
38
use SilverStripe\Security\PermissionProvider;
39
use SilverStripe\Security\SecurityToken;
40
use SilverStripe\View\Requirements;
41
use SilverStripe\ORM\Versioning\Versioned;
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
class AssetAdmin extends LeftAndMain implements PermissionProvider
48
{
49
    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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
50
51
    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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
52
53
    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...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
54
55
    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...
56
57
    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...
58
        // Legacy redirect for SS3-style detail view
59
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
60
        // Pass all URLs to the index, for React to unpack
61
        'show/$FolderID/edit/$FileID' => 'index',
62
        // API access points with structured data
63
        'POST api/createFolder' => 'apiCreateFolder',
64
        'POST api/createFile' => 'apiCreateFile',
65
        'GET api/readFolder' => 'apiReadFolder',
66
        'PUT api/updateFolder' => 'apiUpdateFolder',
67
        'DELETE api/delete' => 'apiDelete',
68
        'GET api/search' => 'apiSearch',
69
        'GET api/history' => 'apiHistory'
70
    ];
71
72
    /**
73
     * Amount of results showing on a single page.
74
     *
75
     * @config
76
     * @var int
77
     */
78
    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...
79
80
    /**
81
     * @config
82
     * @see Upload->allowedMaxFileSize
83
     * @var int
84
     */
85
    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...
86
87
    /**
88
     * @config
89
     *
90
     * @var int
91
     */
92
    private static $max_history_entries = 100;
0 ignored issues
show
Unused Code introduced by
The property $max_history_entries 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
94
    /**
95
     * @var array
96
     */
97
    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...
98
        'legacyRedirectForEditView',
99
        'apiCreateFolder',
100
        'apiCreateFile',
101
        'apiReadFolder',
102
        'apiUpdateFolder',
103
        'apiHistory',
104
        'apiDelete',
105
        'apiSearch',
106
        'FileEditForm',
107
        'FileHistoryForm',
108
        'AddToCampaignForm',
109
        'FileInsertForm',
110
        'schema',
111
    );
112
113
    private static $required_permission_codes = 'CMS_ACCESS_AssetAdmin';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $required_permission_codes is not used and could be removed.

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

Loading history...
114
115
    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...
116
117
    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...
118
119
    /**
120
     * Set up the controller
121
     */
122
    public function init()
123
    {
124
        parent::init();
125
126
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
127
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
128
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
129
130
        CMSBatchActionHandler::register(
131
            'delete',
132
            'SilverStripe\AssetAdmin\BatchAction\DeleteAssets',
133
            'SilverStripe\\Assets\\Folder'
134
        );
135
    }
136
137
    public function getClientConfig()
138
    {
139
        $baseLink = $this->Link();
140
        return array_merge(parent::getClientConfig(), [
141
            'reactRouter' => true,
142
            'createFileEndpoint' => [
143
                'url' => Controller::join_links($baseLink, 'api/createFile'),
144
                'method' => 'post',
145
                'payloadFormat' => 'urlencoded',
146
            ],
147
            'createFolderEndpoint' => [
148
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
149
                'method' => 'post',
150
                'payloadFormat' => 'urlencoded',
151
            ],
152
            'readFolderEndpoint' => [
153
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
154
                'method' => 'get',
155
                'responseFormat' => 'json',
156
            ],
157
            'searchEndpoint' => [
158
                'url' => Controller::join_links($baseLink, 'api/search'),
159
                'method' => 'get',
160
                'responseFormat' => 'json',
161
            ],
162
            'updateFolderEndpoint' => [
163
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
164
                'method' => 'put',
165
                'payloadFormat' => 'urlencoded',
166
            ],
167
            'deleteEndpoint' => [
168
                'url' => Controller::join_links($baseLink, 'api/delete'),
169
                'method' => 'delete',
170
                'payloadFormat' => 'urlencoded',
171
            ],
172
            'historyEndpoint' => [
173
                'url' => Controller::join_links($baseLink, 'api/history'),
174
                'method' => 'get',
175
                'responseFormat' => 'json'
176
            ],
177
            'limit' => $this->config()->page_length,
178
            'form' => [
179
                'FileEditForm' => [
180
                    'schemaUrl' => $this->Link('schema/FileEditForm')
181
                ],
182
                'FileInsertForm' => [
183
                    'schemaUrl' => $this->Link('schema/FileInsertForm')
184
                ],
185
                'AddToCampaignForm' => [
186
                    'schemaUrl' => $this->Link('schema/AddToCampaignForm')
187
                ],
188
                'FileHistoryForm' => [
189
                    'schemaUrl' => $this->Link('schema/FileHistoryForm')
190
                ]
191
            ],
192
        ]);
193
    }
194
195
    /**
196
     * Fetches a collection of files by ParentID.
197
     *
198
     * @param HTTPRequest $request
199
     * @return HTTPResponse
200
     */
201
    public function apiReadFolder(HTTPRequest $request)
202
    {
203
        $params = $request->requestVars();
204
        $items = array();
205
        $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...
206
        $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...
207
208
        if (!isset($params['id']) && !strlen($params['id'])) {
209
            $this->httpError(400);
210
        }
211
212
        $folderID = (int)$params['id'];
213
        /** @var Folder $folder */
214
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
215
216
        if (!$folder) {
217
            $this->httpError(400);
218
        }
219
220
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
221
        $files = $this->getList()->filter('ParentID', $folderID);
222
223
        if ($files) {
224
            /** @var File $file */
225
            foreach ($files as $file) {
226
                if (!$file->canView()) {
227
                    continue;
228
                }
229
230
                $items[] = $this->getObjectFromData($file);
231
            }
232
        }
233
234
        // Build parents (for breadcrumbs)
235
        $parents = [];
236
        $next = $folder->Parent();
237
        while ($next && $next->exists()) {
238
            array_unshift($parents, [
239
                'id' => $next->ID,
240
                'title' => $next->getTitle(),
241
                'filename' => $next->getFilename(),
242
            ]);
243
            if ($next->ParentID) {
244
                $next = $next->Parent();
245
            } else {
246
                break;
247
            }
248
        }
249
250
        // Build response
251
        $response = new HTTPResponse();
252
        $response->addHeader('Content-Type', 'application/json');
253
        $response->setBody(json_encode([
254
            'files' => $items,
255
            'title' => $folder->getTitle(),
256
            'count' => count($items),
257
            'parents' => $parents,
258
            'parent' => $parents ? $parents[count($parents) - 1] : null,
259
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
260
            'folderID' => $folderID,
261
            'canEdit' => $folder->canEdit(),
262
            'canDelete' => $folder->canArchive(),
263
        ]));
264
265
        return $response;
266
    }
267
268
    /**
269
     * @param HTTPRequest $request
270
     *
271
     * @return HTTPResponse
272
     */
273
    public function apiSearch(HTTPRequest $request)
274
    {
275
        $params = $request->getVars();
276
        $list = $this->getList($params);
277
278
        $response = new HTTPResponse();
279
        $response->addHeader('Content-Type', 'application/json');
280
        $response->setBody(json_encode([
281
            // Serialisation
282
            "files" => array_map(function ($file) {
283
                return $this->getObjectFromData($file);
284
            }, $list->toArray()),
285
            "count" => $list->count(),
286
        ]));
287
288
        return $response;
289
    }
290
291
    /**
292
     * @param HTTPRequest $request
293
     *
294
     * @return HTTPResponse
295
     */
296
    public function apiDelete(HTTPRequest $request)
297
    {
298
        parse_str($request->getBody(), $vars);
299
300
        // CSRF check
301
        $token = SecurityToken::inst();
302 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...
303
            return new HTTPResponse(null, 400);
304
        }
305
306 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...
307
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
308
                ->addHeader('Content-Type', 'application/json');
309
        }
310
311
        $fileIds = $vars['ids'];
312
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
313
314 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...
315
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
316
                ->addHeader('Content-Type', 'application/json');
317
        }
318
319
        if (!min(array_map(function (File $file) {
320
            return $file->canArchive();
321
        }, $files))) {
322
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
323
                ->addHeader('Content-Type', 'application/json');
324
        }
325
326
        /** @var File $file */
327
        foreach ($files as $file) {
328
            $file->doArchive();
329
        }
330
331
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
332
            ->addHeader('Content-Type', 'application/json');
333
    }
334
335
    /**
336
     * Creates a single file based on a form-urlencoded upload.
337
     *
338
     * @param HTTPRequest $request
339
     * @return HTTPRequest|HTTPResponse
340
     */
341
    public function apiCreateFile(HTTPRequest $request)
342
    {
343
        $data = $request->postVars();
344
        $upload = $this->getUpload();
345
346
        // CSRF check
347
        $token = SecurityToken::inst();
348 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...
349
            return new HTTPResponse(null, 400);
350
        }
351
352
        // Check parent record
353
        /** @var Folder $parentRecord */
354
        $parentRecord = null;
355
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
356
            $parentRecord = Folder::get()->byID($data['ParentID']);
357
        }
358
        $data['Parent'] = $parentRecord;
359
360
        $tmpFile = $request->postVar('Upload');
361
        if (!$upload->validate($tmpFile)) {
362
            $result = ['message' => null];
363
            $errors = $upload->getErrors();
364
            if ($message = array_shift($errors)) {
365
                $result['message'] = [
366
                    'type' => 'error',
367
                    'value' => $message,
368
                ];
369
            }
370
            return (new HTTPResponse(json_encode($result), 400))
371
                ->addHeader('Content-Type', 'application/json');
372
        }
373
374
        // TODO Allow batch uploads
375
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
376
        /** @var File $file */
377
        $file = Injector::inst()->create($fileClass);
378
379
        // check canCreate permissions
380 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...
381
            $result = ['message' => [
382
                'type' => 'error',
383
                'value' => _t(
384
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
385
                    'You do not have permission to add files'
386
                )
387
            ]];
388
            return (new HTTPResponse(json_encode($result), 403))
389
                ->addHeader('Content-Type', 'application/json');
390
        }
391
392
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
393 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...
394
            $result = ['message' => [
395
                'type' => 'error',
396
                'value' => _t(
397
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
398
                    'Failed to load file'
399
                )
400
            ]];
401
            return (new HTTPResponse(json_encode($result), 400))
402
                ->addHeader('Content-Type', 'application/json');
403
        }
404
405
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
406
        $file->write();
407
408
        $result = [$this->getObjectFromData($file)];
409
410
        return (new HTTPResponse(json_encode($result)))
411
            ->addHeader('Content-Type', 'application/json');
412
    }
413
414
    /**
415
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
416
     *
417
     * @param HTTPRequest $request
418
     * @return HTTPResponse
419
     */
420
    public function apiHistory(HTTPRequest $request)
421
    {
422
        // CSRF check not required as the GET request has no side effects.
423
        $fileId = $request->getVar('fileId');
424
425
        if (!$fileId || !is_numeric($fileId)) {
426
            return new HTTPResponse(null, 400);
427
        }
428
429
        $class = File::class;
430
        $file = DataObject::get($class)->byID($fileId);
431
432
        if (!$file) {
433
            return new HTTPResponse(null, 404);
434
        }
435
436
        if (!$file->canView()) {
437
            return new HTTPResponse(null, 403);
438
        }
439
440
        $versions = Versioned::get_all_versions($class, $fileId)
441
            ->limit($this->config()->max_history_entries)
442
            ->sort('Version', 'DESC');
443
444
        $output = array();
445
        $next = array();
446
        $prev = null;
447
448
        // swap the order so we can get the version number to compare against.
449
        // i.e version 3 needs to know version 2 is the previous version
450
        $copy = $versions->map('Version', 'Version')->toArray();
451
        foreach (array_reverse($copy) as $k => $v) {
452
            if ($prev) {
453
                $next[$v] = $prev;
454
            }
455
456
            $prev = $v;
457
        }
458
459
        $_cachedMembers = array();
460
461
        /** @var File $version */
462
        foreach ($versions as $version) {
463
            $author = null;
464
465
            if ($version->AuthorID) {
466
                if (!isset($_cachedMembers[$version->AuthorID])) {
467
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
468
                        ->byID($version->AuthorID);
469
                }
470
471
                $author = $_cachedMembers[$version->AuthorID];
472
            }
473
474
            if ($version->canView()) {
475
                $published = true;
0 ignored issues
show
Unused Code introduced by
$published 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...
476
477
                if (isset($next[$version->Version])) {
478
                    $summary = $version->humanizedChanges(
479
                        $version->Version,
480
                        $next[$version->Version]
481
                    );
482
483
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
484
                    // generic message
485
                    if (!$summary) {
486
                        $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.SAVEDFILE', "Saved file");
487
                    }
488
                } else {
489
                    $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UPLOADEDFILE', "Uploaded file");
490
                }
491
492
                $output[] = array(
493
                    'versionid' => $version->Version,
494
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
495
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
496
                    'status' => ($version->WasPublished) ? _t('File.PUBLISHED', 'Published') : '',
497
                    'author' => ($author)
498
                        ? $author->Name
499
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNKNOWN', "Unknown"),
500
                    'summary' => ($summary) ? $summary : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NOSUMMARY', "No summary available")
501
                );
502
            }
503
        }
504
505
        return
506
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
507
    }
508
509
510
    /**
511
     * Creates a single folder, within an optional parent folder.
512
     *
513
     * @param HTTPRequest $request
514
     * @return HTTPRequest|HTTPResponse
515
     */
516
    public function apiCreateFolder(HTTPRequest $request)
517
    {
518
        $data = $request->postVars();
519
520
        $class = 'SilverStripe\\Assets\\Folder';
521
522
        // CSRF check
523
        $token = SecurityToken::inst();
524 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...
525
            return new HTTPResponse(null, 400);
526
        }
527
528
        // check addchildren permissions
529
        /** @var Folder $parentRecord */
530
        $parentRecord = null;
531
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
532
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
533
        }
534
        $data['Parent'] = $parentRecord;
535
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
536
537
        // Build filename
538
        $baseFilename = isset($data['Name'])
539
            ? basename($data['Name'])
540
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
541
542
        if ($parentRecord && $parentRecord->ID) {
543
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
544
        }
545
546
        // Ensure name is unique
547
        $nameGenerator = $this->getNameGenerator($baseFilename);
548
        $filename = null;
549
        foreach ($nameGenerator as $filename) {
550
            if (! File::find($filename)) {
551
                break;
552
            }
553
        }
554
        $data['Name'] = basename($filename);
555
556
        // Create record
557
        /** @var Folder $record */
558
        $record = Injector::inst()->create($class);
559
560
        // check create permissions
561
        if (!$record->canCreate(null, $data)) {
562
            return (new HTTPResponse(null, 403))
563
                ->addHeader('Content-Type', 'application/json');
564
        }
565
566
        $record->ParentID = $data['ParentID'];
567
        $record->Name = $record->Title = basename($data['Name']);
568
        $record->write();
569
570
        $result = $this->getObjectFromData($record);
571
572
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
573
    }
574
575
    /**
576
     * Redirects 3.x style detail links to new 4.x style routing.
577
     *
578
     * @param HTTPRequest $request
579
     */
580
    public function legacyRedirectForEditView($request)
581
    {
582
        $fileID = $request->param('FileID');
583
        /** @var File $file */
584
        $file = File::get()->byID($fileID);
585
        $link = $this->getFileEditLink($file) ?: $this->Link();
586
        $this->redirect($link);
587
    }
588
589
    /**
590
     * Given a file return the CMS link to edit it
591
     *
592
     * @param File $file
593
     * @return string
594
     */
595
    public function getFileEditLink($file)
596
    {
597
        if (!$file || !$file->isInDB()) {
598
            return null;
599
        }
600
601
        return Controller::join_links(
602
            $this->Link('show'),
603
            $file->ParentID,
604
            'edit',
605
            $file->ID
606
        );
607
    }
608
609
    /**
610
     * Get the search context from {@link File}, used to create the search form
611
     * as well as power the /search API endpoint.
612
     *
613
     * @return SearchContext
614
     */
615
    public function getSearchContext()
616
    {
617
        $context = File::singleton()->getDefaultSearchContext();
618
619
        // Customize fields
620
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
621
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
622
        ->setConfig('showcalendar', true);
623
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
624
        ->setConfig('showcalendar', true);
625
        $dateGroup = FieldGroup::create(
626
            $dateHeader,
627
            $dateFrom,
628
            $dateTo
629
        );
630
        $context->addField($dateGroup);
631
        /** @skipUpgrade */
632
        $appCategories = array(
633
            'archive' => _t(
634
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
635
                'Archive'
636
            ),
637
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
638
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
639
            'flash' => _t(
640
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
641
                'Flash',
642
                'The fileformat'
643
            ),
644
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
645
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
646
        );
647
        $context->addField(
648
            $typeDropdown = new DropdownField(
649
                'AppCategory',
650
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
651
                $appCategories
652
            )
653
        );
654
655
        $typeDropdown->setEmptyString(' ');
656
657
        $currentfolderLabel = _t(
658
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
659
            'Limit to current folder?'
660
        );
661
        $context->addField(
662
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
663
        );
664
        $context->getFields()->removeByName('Title');
665
666
        return $context;
667
    }
668
669
    /**
670
     * Get an asset renamer for the given filename.
671
     *
672
     * @param  string            $filename Path name
673
     * @return AssetNameGenerator
674
     */
675
    protected function getNameGenerator($filename)
676
    {
677
        return Injector::inst()
678
            ->createWithArgs('AssetNameGenerator', array($filename));
679
    }
680
681
    /**
682
     * @todo Implement on client
683
     *
684
     * @param bool $unlinked
685
     * @return ArrayList
686
     */
687
    public function breadcrumbs($unlinked = false)
688
    {
689
        return null;
690
    }
691
692
693
    /**
694
     * Don't include class namespace in auto-generated CSS class
695
     */
696
    public function baseCSSClasses()
697
    {
698
        return 'AssetAdmin LeftAndMain';
699
    }
700
701
    public function providePermissions()
702
    {
703
        return array(
704
            "CMS_ACCESS_AssetAdmin" => array(
705
                '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...
706
                    'title' => static::menu_title()
707
                )),
708
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
709
            )
710
        );
711
    }
712
713
    /**
714
     * Build a form scaffolder for this model
715
     *
716
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
717
     *
718
     * @param File $file
719
     * @return FormFactory
720
     */
721
    public function getFormFactory(File $file)
722
    {
723
        // Get service name based on file class
724
        $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...
725
        if ($file instanceof Folder) {
726
            $name = FolderFormFactory::class;
727
        } elseif ($file instanceof Image) {
728
            $name = ImageFormFactory::class;
729
        } else {
730
            $name = FileFormFactory::class;
731
        }
732
        return Injector::inst()->get($name);
733
    }
734
735
    /**
736
     * The form is used to generate a form schema,
737
     * as well as an intermediary object to process data through API endpoints.
738
     * Since it's used directly on API endpoints, it does not have any form actions.
739
     * It handles both {@link File} and {@link Folder} records.
740
     *
741
     * @param int $id
742
     * @return Form
743
     */
744
    public function getFileEditForm($id)
745
    {
746
        /** @var File $file */
747
        $file = $this->getList()->byID($id);
748
749 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...
750
            $this->httpError(403, _t(
751
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
752
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
753
                '',
754
                ['ObjectTitle' => $file->i18n_singular_name()]
755
            ));
756
            return null;
757
        }
758
759
        $scaffolder = $this->getFormFactory($file);
760
        $form = $scaffolder->getForm($this, 'FileEditForm', [
761
            'Record' => $file
762
        ]);
763
764
        // Configure form to respond to validation errors with form schema
765
        // if requested via react.
766 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $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...
767
            $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $id);
768
            return $this->getSchemaResponse($schemaId, $form, $errors);
0 ignored issues
show
Documentation introduced by
$schemaId is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with $errors.

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

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

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

Loading history...
769
        });
770
771
        return $form;
772
    }
773
774
    /**
775
     * Get file edit form
776
     *
777
     * @return Form
778
     */
779 View Code Duplication
    public function FileEditForm()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
780
    {
781
        // Get ID either from posted back value, or url parameter
782
        $request = $this->getRequest();
783
        $id = $request->param('ID') ?: $request->postVar('ID');
784
        return $this->getFileEditForm($id);
785
    }
786
787
    /**
788
     * The form is used to generate a form schema,
789
     * as well as an intermediary object to process data through API endpoints.
790
     * Since it's used directly on API endpoints, it does not have any form actions.
791
     * It handles both {@link File} and {@link Folder} records.
792
     *
793
     * @param int $id
794
     * @return Form
795
     */
796
    public function getFileInsertForm($id)
797
    {
798
        /** @var File $file */
799
        $file = $this->getList()->byID($id);
800
801 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...
802
            $this->httpError(403, _t(
803
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
804
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
805
                '',
806
                ['ObjectTitle' => $file->i18n_singular_name()]
807
            ));
808
            return null;
809
        }
810
811
        $scaffolder = $this->getFormFactory($file);
812
        $form = $scaffolder->getForm($this, 'FileInsertForm', [
813
            'Record' => $file,
814
            'Type' => 'insert',
815
        ]);
816
817
        return $form;
818
    }
819
820
    /**
821
     * Get file insert form
822
     *
823
     * @return Form
824
     */
825 View Code Duplication
    public function FileInsertForm()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
826
    {
827
        // Get ID either from posted back value, or url parameter
828
        $request = $this->getRequest();
829
        $id = $request->param('ID') ?: $request->postVar('ID');
830
        return $this->getFileInsertForm($id);
831
    }
832
833
    /**
834
     * @param array $context
835
     * @return Form
836
     * @throws InvalidArgumentException
837
     */
838
    public function getFileHistoryForm($context)
839
    {
840
        // Check context
841
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
842
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
843
        }
844
        $id = $context['RecordID'];
845
        $versionId = $context['RecordVersion'];
846
        if (!$id || !$versionId) {
847
            return $this->httpError(404);
848
        }
849
850
        /** @var File $file */
851
        $file = Versioned::get_version(File::class, $id, $versionId);
852
        if (!$file) {
853
            return $this->httpError(404);
854
        }
855
856 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...
857
            $this->httpError(403, _t(
858
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
859
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
860
                '',
861
                ['ObjectTitle' => $file->i18n_singular_name()]
862
            ));
863
            return null;
864
        }
865
866
        $effectiveContext = array_merge($context, ['Record' => $file]);
867
        /** @var FormFactory $scaffolder */
868
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
869
        $form = $scaffolder->getForm($this, 'FileHistoryForm', $effectiveContext);
870
871
        // Configure form to respond to validation errors with form schema
872
        // if requested via react.
873 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id, $versionId) {
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...
874
            $schemaId = Controller::join_links($this->Link('schema/FileHistoryForm'), $id, $versionId);
875
            return $this->getSchemaResponse($schemaId, $form, $errors);
0 ignored issues
show
Documentation introduced by
$schemaId is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with $errors.

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

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

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

Loading history...
876
        });
877
878
        return $form;
879
    }
880
881
    /**
882
     * Gets a JSON schema representing the current edit form.
883
     *
884
     * WARNING: Experimental API.
885
     *
886
     * @param HTTPRequest $request
887
     * @return HTTPResponse
888
     */
889
    public function schema($request)
890
    {
891
        $formName = $request->param('FormName');
892
        if ($formName !== 'FileHistoryForm') {
893
            return parent::schema($request);
894
        }
895
896
        // Get schema for history form
897
        // @todo Eventually all form scaffolding will be based on context rather than record ID
898
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
899
        $itemID = $request->param('ItemID');
900
        $version = $request->param('OtherItemID');
901
        $form = $this->getFileHistoryForm([
902
            'RecordID' => $itemID,
903
            'RecordVersion' => $version,
904
        ]);
905
906
        // Respond with this schema
907
        $response = $this->getResponse();
908
        $response->addHeader('Content-Type', 'application/json');
909
        $schemaID = $this->getRequest()->getURL();
910
        $schema = $this
0 ignored issues
show
Bug introduced by
The method getFormSchema() does not exist on SilverStripe\AssetAdmin\Controller\AssetAdmin. Did you maybe mean schema()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
911
            ->getFormSchema()
912
            ->getMultipartSchema($this->getRequest(), $schemaID, $form);
913
        $response->setBody(Convert::raw2json($schema));
914
        return $response;
915
    }
916
917
    /**
918
     * Get file history form
919
     *
920
     * @return Form
921
     */
922
    public function FileHistoryForm()
923
    {
924
        $request = $this->getRequest();
925
        $id = $request->param('ID') ?: $request->postVar('ID');
926
        $version = $request->param('OtherID') ?: $request->postVar('Version');
927
        $form = $this->getFileHistoryForm([
928
            'RecordID' => $id,
929
            'RecordVersion' => $version,
930
        ]);
931
        return $form;
932
    }
933
934
    /**
935
     * @param array $data
936
     * @param Form $form
937
     * @return HTTPResponse
938
     */
939
    public function save($data, $form)
940
    {
941
        return $this->saveOrPublish($data, $form, false);
942
    }
943
944
    /**
945
     * @param array $data
946
     * @param Form $form
947
     * @return HTTPResponse
948
     */
949
    public function publish($data, $form)
950
    {
951
        return $this->saveOrPublish($data, $form, true);
952
    }
953
954
    /**
955
     * Update thisrecord
956
     *
957
     * @param array $data
958
     * @param Form $form
959
     * @param bool $doPublish
960
     * @return HTTPResponse
961
     */
962
    protected function saveOrPublish($data, $form, $doPublish = false)
963
    {
964 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...
965
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
966
                ->addHeader('Content-Type', 'application/json');
967
        }
968
969
        $id = (int) $data['ID'];
970
        /** @var File $record */
971
        $record = $this->getList()->filter('ID', $id)->first();
972
973 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...
974
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
975
                ->addHeader('Content-Type', 'application/json');
976
        }
977
978
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
979
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
980
                ->addHeader('Content-Type', 'application/json');
981
        }
982
983
        $form->saveInto($record);
984
        $record->write();
985
986
        // Publish this record and owned objects
987
        if ($doPublish) {
988
            $record->publishRecursive();
989
        }
990
991
        // Note: Force return of schema / state in success result
992
        return $this->getRecordUpdatedResponse($record, $form);
993
    }
994
995
    public function unpublish($data, $form)
996
    {
997 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...
998
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
999
                ->addHeader('Content-Type', 'application/json');
1000
        }
1001
1002
        $id = (int) $data['ID'];
1003
        /** @var File $record */
1004
        $record = $this->getList()->filter('ID', $id)->first();
1005
1006
        if (!$record) {
1007
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1008
                ->addHeader('Content-Type', 'application/json');
1009
        }
1010
1011
        if (!$record->canUnpublish()) {
1012
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1013
                ->addHeader('Content-Type', 'application/json');
1014
        }
1015
1016
        $record->doUnpublish();
1017
        return $this->getRecordUpdatedResponse($record, $form);
1018
    }
1019
1020
    /**
1021
     * @param File $file
1022
     *
1023
     * @return array
1024
     */
1025
    public function getObjectFromData(File $file)
1026
    {
1027
        $object = array(
1028
            'id' => $file->ID,
1029
            'created' => $file->Created,
1030
            'lastUpdated' => $file->LastEdited,
1031
            'owner' => null,
1032
            'parent' => null,
1033
            'title' => $file->Title,
1034
            'exists' => $file->exists(), // Broken file check
1035
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1036
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
1037
            'name' => $file->Name,
1038
            'filename' => $file->Filename,
1039
            '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...
1040
            'size' => $file->Size,
1041
            'url' => $file->AbsoluteURL,
1042
            'published' => $file->isPublished(),
1043
            'modified' => $file->isModifiedOnDraft(),
1044
            'draft' => $file->isOnDraftOnly(),
1045
            'canEdit' => $file->canEdit(),
1046
            'canDelete' => $file->canArchive(),
1047
        );
1048
1049
        /** @var Member $owner */
1050
        $owner = $file->Owner();
1051
1052
        if ($owner) {
1053
            $object['owner'] = array(
1054
                'id' => $owner->ID,
1055
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1056
            );
1057
        }
1058
1059
        /** @var Folder $parent */
1060
        $parent = $file->Parent();
1061
1062
        if ($parent) {
1063
            $object['parent'] = array(
1064
                'id' => $parent->ID,
1065
                'title' => $parent->Title,
1066
                'filename' => $parent->Filename,
1067
            );
1068
        }
1069
1070
        /** @var File $file */
1071
        if ($file->getIsImage()) {
1072
            // Small thumbnail
1073
            $smallWidth = UploadField::config()->get('thumbnail_width');
1074
            $smallHeight = UploadField::config()->get('thumbnail_height');
1075
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1076
            if ($smallThumbnail && $smallThumbnail->exists()) {
1077
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1078
            }
1079
1080
            // Large thumbnail
1081
            $width = $this->config()->get('thumbnail_width');
1082
            $height = $this->config()->get('thumbnail_height');
1083
            $thumbnail = $file->FitMax($width, $height);
1084
            if ($thumbnail && $thumbnail->exists()) {
1085
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1086
            }
1087
            $object['dimensions']['width'] = $file->Width;
1088
            $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...
1089
        }
1090
1091
        return $object;
1092
    }
1093
1094
    /**
1095
     * Returns the files and subfolders contained in the currently selected folder,
1096
     * defaulting to the root node. Doubles as search results, if any search parameters
1097
     * are set through {@link SearchForm()}.
1098
     *
1099
     * @param array $params Unsanitised request parameters
1100
     * @return DataList
1101
     */
1102
    protected function getList($params = array())
1103
    {
1104
        $context = $this->getSearchContext();
1105
1106
        // Overwrite name filter to search both Name and Title attributes
1107
        $context->removeFilterByName('Name');
1108
1109
        // Lazy loaded list. Allows adding new filters through SearchContext.
1110
        /** @var DataList $list */
1111
        $list = $context->getResults($params);
1112
1113
        // Re-add previously removed "Name" filter as combined filter
1114
        // TODO Replace with composite SearchFilter once that API exists
1115
        if (!empty($params['Name'])) {
1116
            $list = $list->filterAny(array(
1117
                'Name:PartialMatch' => $params['Name'],
1118
                'Title:PartialMatch' => $params['Name']
1119
            ));
1120
        }
1121
1122
        // Optionally limit search to a folder (non-recursive)
1123
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
1124
            $list = $list->filter('ParentID', $params['ParentID']);
1125
        }
1126
1127
        // Date filtering
1128 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...
1129
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
1130
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
1131
        }
1132 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...
1133
            $toDate = new DateField(null, null, $params['CreatedTo']);
1134
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
1135
        }
1136
1137
        // Categories
1138
        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...
1139
            $extensions = File::config()->app_categories[$filters['AppCategory']];
1140
            $list = $list->filter('Name:PartialMatch', $extensions);
1141
        }
1142
1143
        // Sort folders first
1144
        $list = $list->sort(
1145
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
1146
        );
1147
1148
        // Pagination
1149
        if (isset($filters['page']) && isset($filters['limit'])) {
1150
            $page = $filters['page'];
1151
            $limit = $filters['limit'];
1152
            $offset = ($page - 1) * $limit;
1153
            $list = $list->limit($limit, $offset);
1154
        }
1155
1156
        // Access checks
1157
        $list = $list->filterByCallback(function (File $file) {
1158
            return $file->canView();
1159
        });
1160
1161
        return $list;
1162
    }
1163
1164
    /**
1165
     * Action handler for adding pages to a campaign
1166
     *
1167
     * @param array $data
1168
     * @param Form $form
1169
     * @return DBHTMLText|HTTPResponse
1170
     */
1171
    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...
1172
    {
1173
        $id = $data['ID'];
1174
        $record = $this->getList()->byID($id);
1175
1176
        $handler = AddToCampaignHandler::create($this, $record);
1177
        $results = $handler->addToCampaign($record, $data['Campaign']);
1178
        if (!isset($results)) {
1179
            return null;
1180
        }
1181
1182
        $request = $this->getRequest();
1183
        if ($request->getHeader('X-Formschema-Request')) {
1184
            $schemaId = Controller::join_links($this->Link('schema/AddToCampaignForm'), $id);
1185
            $data = $this
0 ignored issues
show
Bug introduced by
The method getFormSchema() does not exist on SilverStripe\AssetAdmin\Controller\AssetAdmin. Did you maybe mean schema()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1186
                ->getFormSchema()
1187
                ->getMultipartSchema($this->getRequest(), $schemaId, $handler->Form($record));
1188
            $data['message'] = $results;
1189
1190
            $response = new HTTPResponse(Convert::raw2json($data));
1191
            $response->addHeader('Content-Type', 'application/json');
1192
            return $response;
1193
        }
1194
        return $results;
1195
    }
1196
1197
    /**
1198
     * Url handler for add to campaign form
1199
     *
1200
     * @param HTTPRequest $request
1201
     * @return Form
1202
     */
1203
    public function AddToCampaignForm($request)
1204
    {
1205
        // Get ID either from posted back value, or url parameter
1206
        $id = $request->param('ID') ?: $request->postVar('ID');
1207
        return $this->getAddToCampaignForm($id);
1208
    }
1209
1210
    /**
1211
     * @param int $id
1212
     * @return Form
1213
     */
1214
    public function getAddToCampaignForm($id)
1215
    {
1216
        // Get record-specific fields
1217
        $record = $this->getList()->byID($id);
1218
1219
        if (!$record) {
1220
            $this->httpError(404, _t(
1221
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1222
                'That {Type} couldn\'t be found',
1223
                '',
1224
                ['Type' => File::singleton()->i18n_singular_name()]
1225
            ));
1226
            return null;
1227
        }
1228 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...
1229
            $this->httpError(403, _t(
1230
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1231
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1232
                '',
1233
                ['ObjectTitle' => $record->i18n_singular_name()]
1234
            ));
1235
            return null;
1236
        }
1237
1238
        $handler = AddToCampaignHandler::create($this, $record);
1239
        $form = $handler->Form($record);
1240
1241 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $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...
1242
            $schemaId = Controller::join_links($this->Link('schema/AddToCampaignForm'), $id);
1243
            return $this->getSchemaResponse($schemaId, $form, $errors);
0 ignored issues
show
Documentation introduced by
$schemaId is of type string, but the function expects a object<SilverStripe\Forms\Form>.

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...
Unused Code introduced by
The call to AssetAdmin::getSchemaResponse() has too many arguments starting with $errors.

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

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

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

Loading history...
1244
        });
1245
1246
        return $form;
1247
    }
1248
1249
    /**
1250
     * @return Upload
1251
     */
1252
    protected function getUpload()
1253
    {
1254
        $upload = Upload::create();
1255
        $upload->getValidator()->setAllowedExtensions(
1256
            // filter out '' since this would be a regex problem on JS end
1257
            array_filter(File::config()->get('allowed_extensions'))
1258
        );
1259
1260
        return $upload;
1261
    }
1262
1263
    /**
1264
     * Get response for successfully updated record
1265
     *
1266
     * @param File $record
1267
     * @param Form $form
1268
     * @return HTTPResponse
1269
     */
1270
    protected function getRecordUpdatedResponse($record, $form)
1271
    {
1272
        // Note: Force return of schema / state in success result
1273
        $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $record->ID);
1274
        $schemaData = $this
0 ignored issues
show
Bug introduced by
The method getFormSchema() does not exist on SilverStripe\AssetAdmin\Controller\AssetAdmin. Did you maybe mean schema()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1275
            ->getFormSchema()
1276
            ->getMultipartSchema($this->getRequest(), $schemaId, $form);
1277
1278
        // Return the record data in the same response as the schema to save a postback
1279
        $schemaData['record'] = $this->getObjectFromData($record);
1280
1281
        // Serialise
1282
        $response = new HTTPResponse(Convert::raw2json($schemaData));
1283
        $response->addHeader('Content-Type', 'application/json');
1284
        return $response;
1285
    }
1286
}
1287