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

AssetAdmin::getRecordUpdatedResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
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)
501
                        ? $summary
502
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NOSUMMARY', "No summary available")
503
                );
504
            }
505
        }
506
507
        return
508
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
509
    }
510
511
512
    /**
513
     * Creates a single folder, within an optional parent folder.
514
     *
515
     * @param HTTPRequest $request
516
     * @return HTTPRequest|HTTPResponse
517
     */
518
    public function apiCreateFolder(HTTPRequest $request)
519
    {
520
        $data = $request->postVars();
521
522
        $class = 'SilverStripe\\Assets\\Folder';
523
524
        // CSRF check
525
        $token = SecurityToken::inst();
526 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...
527
            return new HTTPResponse(null, 400);
528
        }
529
530
        // check addchildren permissions
531
        /** @var Folder $parentRecord */
532
        $parentRecord = null;
533
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
534
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
535
        }
536
        $data['Parent'] = $parentRecord;
537
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
538
539
        // Build filename
540
        $baseFilename = isset($data['Name'])
541
            ? basename($data['Name'])
542
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
543
544
        if ($parentRecord && $parentRecord->ID) {
545
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
546
        }
547
548
        // Ensure name is unique
549
        $nameGenerator = $this->getNameGenerator($baseFilename);
550
        $filename = null;
551
        foreach ($nameGenerator as $filename) {
552
            if (! File::find($filename)) {
553
                break;
554
            }
555
        }
556
        $data['Name'] = basename($filename);
557
558
        // Create record
559
        /** @var Folder $record */
560
        $record = Injector::inst()->create($class);
561
562
        // check create permissions
563
        if (!$record->canCreate(null, $data)) {
564
            return (new HTTPResponse(null, 403))
565
                ->addHeader('Content-Type', 'application/json');
566
        }
567
568
        $record->ParentID = $data['ParentID'];
569
        $record->Name = $record->Title = basename($data['Name']);
570
        $record->write();
571
572
        $result = $this->getObjectFromData($record);
573
574
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
575
    }
576
577
    /**
578
     * Redirects 3.x style detail links to new 4.x style routing.
579
     *
580
     * @param HTTPRequest $request
581
     */
582
    public function legacyRedirectForEditView($request)
583
    {
584
        $fileID = $request->param('FileID');
585
        /** @var File $file */
586
        $file = File::get()->byID($fileID);
587
        $link = $this->getFileEditLink($file) ?: $this->Link();
588
        $this->redirect($link);
589
    }
590
591
    /**
592
     * Given a file return the CMS link to edit it
593
     *
594
     * @param File $file
595
     * @return string
596
     */
597
    public function getFileEditLink($file)
598
    {
599
        if (!$file || !$file->isInDB()) {
600
            return null;
601
        }
602
603
        return Controller::join_links(
604
            $this->Link('show'),
605
            $file->ParentID,
606
            'edit',
607
            $file->ID
608
        );
609
    }
610
611
    /**
612
     * Get the search context from {@link File}, used to create the search form
613
     * as well as power the /search API endpoint.
614
     *
615
     * @return SearchContext
616
     */
617
    public function getSearchContext()
618
    {
619
        $context = File::singleton()->getDefaultSearchContext();
620
621
        // Customize fields
622
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
623
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
624
        ->setConfig('showcalendar', true);
625
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
626
        ->setConfig('showcalendar', true);
627
        $dateGroup = FieldGroup::create(
628
            $dateHeader,
629
            $dateFrom,
630
            $dateTo
631
        );
632
        $context->addField($dateGroup);
633
        /** @skipUpgrade */
634
        $appCategories = array(
635
            'archive' => _t(
636
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
637
                'Archive'
638
            ),
639
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
640
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
641
            'flash' => _t(
642
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
643
                'Flash',
644
                'The fileformat'
645
            ),
646
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
647
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
648
        );
649
        $context->addField(
650
            $typeDropdown = new DropdownField(
651
                'AppCategory',
652
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
653
                $appCategories
654
            )
655
        );
656
657
        $typeDropdown->setEmptyString(' ');
658
659
        $currentfolderLabel = _t(
660
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
661
            'Limit to current folder?'
662
        );
663
        $context->addField(
664
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
665
        );
666
        $context->getFields()->removeByName('Title');
667
668
        return $context;
669
    }
670
671
    /**
672
     * Get an asset renamer for the given filename.
673
     *
674
     * @param  string             $filename Path name
675
     * @return AssetNameGenerator
676
     */
677
    protected function getNameGenerator($filename)
678
    {
679
        return Injector::inst()
680
            ->createWithArgs('AssetNameGenerator', array($filename));
681
    }
682
683
    /**
684
     * @todo Implement on client
685
     *
686
     * @param bool $unlinked
687
     * @return ArrayList
688
     */
689
    public function breadcrumbs($unlinked = false)
690
    {
691
        return null;
692
    }
693
694
695
    /**
696
     * Don't include class namespace in auto-generated CSS class
697
     */
698
    public function baseCSSClasses()
699
    {
700
        return 'AssetAdmin LeftAndMain';
701
    }
702
703
    public function providePermissions()
704
    {
705
        return array(
706
            "CMS_ACCESS_AssetAdmin" => array(
707
                '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...
708
                    'title' => static::menu_title()
709
                )),
710
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
711
            )
712
        );
713
    }
714
715
    /**
716
     * Build a form scaffolder for this model
717
     *
718
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
719
     *
720
     * @param File $file
721
     * @return FormFactory
722
     */
723
    public function getFormFactory(File $file)
724
    {
725
        // Get service name based on file class
726
        $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...
727
        if ($file instanceof Folder) {
728
            $name = FolderFormFactory::class;
729
        } elseif ($file instanceof Image) {
730
            $name = ImageFormFactory::class;
731
        } else {
732
            $name = FileFormFactory::class;
733
        }
734
        return Injector::inst()->get($name);
735
    }
736
737
    /**
738
     * The form is used to generate a form schema,
739
     * as well as an intermediary object to process data through API endpoints.
740
     * Since it's used directly on API endpoints, it does not have any form actions.
741
     * It handles both {@link File} and {@link Folder} records.
742
     *
743
     * @param int $id
744
     * @return Form
745
     */
746
    public function getFileEditForm($id)
747
    {
748
        /** @var File $file */
749
        $file = $this->getList()->byID($id);
750
751 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...
752
            $this->httpError(403, _t(
753
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
754
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
755
                '',
756
                ['ObjectTitle' => $file->i18n_singular_name()]
757
            ));
758
            return null;
759
        }
760
761
        $scaffolder = $this->getFormFactory($file);
762
        $form = $scaffolder->getForm($this, 'fileEditForm', [
763
            'Record' => $file
764
        ]);
765
766
        // Configure form to respond to validation errors with form schema
767
        // if requested via react.
768 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...
769
            $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $id);
770
            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...
771
        });
772
773
        return $form;
774
    }
775
776
    /**
777
     * Get file edit form
778
     *
779
     * @return Form
780
     */
781 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...
782
    {
783
        // Get ID either from posted back value, or url parameter
784
        $request = $this->getRequest();
785
        $id = $request->param('ID') ?: $request->postVar('ID');
786
        return $this->getFileEditForm($id);
787
    }
788
789
    /**
790
     * The form is used to generate a form schema,
791
     * as well as an intermediary object to process data through API endpoints.
792
     * Since it's used directly on API endpoints, it does not have any form actions.
793
     * It handles both {@link File} and {@link Folder} records.
794
     *
795
     * @param int $id
796
     * @return Form
797
     */
798
    public function getFileInsertForm($id)
799
    {
800
        /** @var File $file */
801
        $file = $this->getList()->byID($id);
802
803 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...
804
            $this->httpError(403, _t(
805
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
806
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
807
                '',
808
                ['ObjectTitle' => $file->i18n_singular_name()]
809
            ));
810
            return null;
811
        }
812
813
        $scaffolder = $this->getFormFactory($file);
814
        $form = $scaffolder->getForm($this, 'fileInsertForm', [
815
            'Record' => $file,
816
            'Type' => 'insert',
817
        ]);
818
819
        return $form;
820
    }
821
822
    /**
823
     * Get file insert form
824
     *
825
     * @return Form
826
     */
827 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...
828
    {
829
        // Get ID either from posted back value, or url parameter
830
        $request = $this->getRequest();
831
        $id = $request->param('ID') ?: $request->postVar('ID');
832
        return $this->getFileInsertForm($id);
833
    }
834
835
    /**
836
     * @param array $context
837
     * @return Form
838
     * @throws InvalidArgumentException
839
     */
840
    public function getFileHistoryForm($context)
841
    {
842
        // Check context
843
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
844
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
845
        }
846
        $id = $context['RecordID'];
847
        $versionId = $context['RecordVersion'];
848
        if (!$id || !$versionId) {
849
            return $this->httpError(404);
850
        }
851
852
        /** @var File $file */
853
        $file = Versioned::get_version(File::class, $id, $versionId);
854
        if (!$file) {
855
            return $this->httpError(404);
856
        }
857
858 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...
859
            $this->httpError(403, _t(
860
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
861
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
862
                '',
863
                ['ObjectTitle' => $file->i18n_singular_name()]
864
            ));
865
            return null;
866
        }
867
868
        $effectiveContext = array_merge($context, ['Record' => $file]);
869
        /** @var FormFactory $scaffolder */
870
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
871
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
872
873
        // Configure form to respond to validation errors with form schema
874
        // if requested via react.
875 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...
876
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
877
            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...
878
        });
879
880
        return $form;
881
    }
882
883
    /**
884
     * Gets a JSON schema representing the current edit form.
885
     *
886
     * WARNING: Experimental API.
887
     *
888
     * @param HTTPRequest $request
889
     * @return HTTPResponse
890
     */
891
    public function schema($request)
892
    {
893
        $formName = $request->param('FormName');
894
        if ($formName !== 'fileHistoryForm') {
895
            return parent::schema($request);
896
        }
897
898
        // Get schema for history form
899
        // @todo Eventually all form scaffolding will be based on context rather than record ID
900
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
901
        $itemID = $request->param('ItemID');
902
        $version = $request->param('OtherItemID');
903
        $form = $this->getFileHistoryForm([
904
            'RecordID' => $itemID,
905
            'RecordVersion' => $version,
906
        ]);
907
908
        // Respond with this schema
909
        $response = $this->getResponse();
910
        $response->addHeader('Content-Type', 'application/json');
911
        $schemaID = $this->getRequest()->getURL();
912
        return $this->getSchemaResponse($schemaID, $form);
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...
Bug introduced by
It seems like $form defined by $this->getFileHistoryFor...dVersion' => $version)) on line 903 can also be of type object<SilverStripe\Forms\Form>; however, SilverStripe\Admin\LeftA...in::getSchemaResponse() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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

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...
1184
    }
1185
1186
    /**
1187
     * Url handler for add to campaign form
1188
     *
1189
     * @param HTTPRequest $request
1190
     * @return Form
1191
     */
1192
    public function addToCampaignForm($request)
1193
    {
1194
        // Get ID either from posted back value, or url parameter
1195
        $id = $request->param('ID') ?: $request->postVar('ID');
1196
        return $this->getAddToCampaignForm($id);
1197
    }
1198
1199
    /**
1200
     * @param int $id
1201
     * @return Form
1202
     */
1203
    public function getAddToCampaignForm($id)
1204
    {
1205
        // Get record-specific fields
1206
        $record = $this->getList()->byID($id);
1207
1208
        if (!$record) {
1209
            $this->httpError(404, _t(
1210
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1211
                'That {Type} couldn\'t be found',
1212
                '',
1213
                ['Type' => File::singleton()->i18n_singular_name()]
1214
            ));
1215
            return null;
1216
        }
1217 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...
1218
            $this->httpError(403, _t(
1219
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1220
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1221
                '',
1222
                ['ObjectTitle' => $record->i18n_singular_name()]
1223
            ));
1224
            return null;
1225
        }
1226
1227
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1228
        $form = $handler->Form($record);
1229
1230 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...
1231
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1232
            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...
1233
        });
1234
1235
        return $form;
1236
    }
1237
1238
    /**
1239
     * @return Upload
1240
     */
1241
    protected function getUpload()
1242
    {
1243
        $upload = Upload::create();
1244
        $upload->getValidator()->setAllowedExtensions(
1245
            // filter out '' since this would be a regex problem on JS end
1246
            array_filter(File::config()->get('allowed_extensions'))
1247
        );
1248
1249
        return $upload;
1250
    }
1251
1252
    /**
1253
     * Get response for successfully updated record
1254
     *
1255
     * @param File $record
1256
     * @param Form $form
1257
     * @return HTTPResponse
1258
     */
1259
    protected function getRecordUpdatedResponse($record, $form)
1260
    {
1261
        // Return the record data in the same response as the schema to save a postback
1262
        $schemaData = ['record' => $this->getObjectFromData($record)];
1263
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1264
        return $this->getSchemaResponse($schemaId, $form, null, $schemaData);
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 null.

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...
1265
    }
1266
}
1267