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

AssetAdmin::getSearchContext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 53
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 53
rs 9.5797
cc 1
eloc 37
nc 1
nop 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\AssetAdmin\Controller;
4
5
use InvalidArgumentException;
6
use SilverStripe\Admin\AddToCampaignHandler;
7
use SilverStripe\Admin\CMSBatchActionHandler;
8
use SilverStripe\Admin\LeftAndMain;
9
use SilverStripe\AssetAdmin\BatchAction\DeleteAssets;
10
use SilverStripe\AssetAdmin\Forms\AssetFormFactory;
11
use SilverStripe\AssetAdmin\Forms\UploadField;
12
use SilverStripe\AssetAdmin\Forms\FileFormFactory;
13
use SilverStripe\AssetAdmin\Forms\FolderFormFactory;
14
use SilverStripe\AssetAdmin\Forms\FileHistoryFormFactory;
15
use SilverStripe\AssetAdmin\Forms\ImageFormFactory;
16
use SilverStripe\Assets\File;
17
use SilverStripe\Assets\Folder;
18
use SilverStripe\Assets\Image;
19
use SilverStripe\Assets\Storage\AssetNameGenerator;
20
use SilverStripe\Assets\Upload;
21
use SilverStripe\Control\Controller;
22
use SilverStripe\Control\HTTPRequest;
23
use SilverStripe\Control\HTTPResponse;
24
use SilverStripe\Core\Convert;
25
use SilverStripe\Core\Injector\Injector;
26
use SilverStripe\Forms\CheckboxField;
27
use SilverStripe\Forms\DateField;
28
use SilverStripe\Forms\DropdownField;
29
use SilverStripe\Forms\FieldGroup;
30
use SilverStripe\Forms\Form;
31
use SilverStripe\Forms\FormFactory;
32
use SilverStripe\Forms\HeaderField;
33
use SilverStripe\ORM\ArrayList;
34
use SilverStripe\ORM\DataList;
35
use SilverStripe\ORM\DataObject;
36
use SilverStripe\ORM\FieldType\DBHTMLText;
37
use SilverStripe\ORM\Search\SearchContext;
38
use SilverStripe\Security\Member;
39
use SilverStripe\Security\PermissionProvider;
40
use SilverStripe\Security\SecurityToken;
41
use SilverStripe\View\Requirements;
42
use SilverStripe\ORM\Versioning\Versioned;
43
44
/**
45
 * AssetAdmin is the 'file store' section of the CMS.
46
 * It provides an interface for manipulating the File and Folder objects in the system.
47
 */
48
class AssetAdmin extends LeftAndMain implements PermissionProvider
49
{
50
    private static $url_segment = 'assets';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_segment is not used and could be removed.

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

Loading history...
51
52
    private static $url_rule = '/$Action/$ID';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_rule is not used and could be removed.

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

Loading history...
53
54
    private static $menu_title = 'Files';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $menu_title is not used and could be removed.

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

Loading history...
55
56
    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...
57
58
    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...
59
        // Legacy redirect for SS3-style detail view
60
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
61
        // Pass all URLs to the index, for React to unpack
62
        'show/$FolderID/edit/$FileID' => 'index',
63
        // API access points with structured data
64
        'POST api/createFolder' => 'apiCreateFolder',
65
        'POST api/createFile' => 'apiCreateFile',
66
        'GET api/readFolder' => 'apiReadFolder',
67
        'PUT api/updateFolder' => 'apiUpdateFolder',
68
        'DELETE api/delete' => 'apiDelete',
69
        'GET api/search' => 'apiSearch',
70
        'GET api/history' => 'apiHistory'
71
    ];
72
73
    /**
74
     * Amount of results showing on a single page.
75
     *
76
     * @config
77
     * @var int
78
     */
79
    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...
80
81
    /**
82
     * @config
83
     * @see Upload->allowedMaxFileSize
84
     * @var int
85
     */
86
    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...
87
88
    /**
89
     * @config
90
     *
91
     * @var int
92
     */
93
    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...
94
95
    /**
96
     * @var array
97
     */
98
    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...
99
        'legacyRedirectForEditView',
100
        'apiCreateFolder',
101
        'apiCreateFile',
102
        'apiReadFolder',
103
        'apiUpdateFolder',
104
        'apiHistory',
105
        'apiDelete',
106
        'apiSearch',
107
        'fileEditForm',
108
        'fileHistoryForm',
109
        'addToCampaignForm',
110
        'fileInsertForm',
111
        'schema',
112
        'fileSelectForm',
113
    );
114
115
    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...
116
117
    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...
118
119
    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...
120
121
    /**
122
     * Set up the controller
123
     */
124
    public function init()
125
    {
126
        parent::init();
127
128
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
129
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
130
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
131
132
        CMSBatchActionHandler::register('delete', DeleteAssets::class, Folder::class);
133
    }
134
135
    public function getClientConfig()
136
    {
137
        $baseLink = $this->Link();
138
        return array_merge(parent::getClientConfig(), [
139
            'reactRouter' => true,
140
            'createFileEndpoint' => [
141
                'url' => Controller::join_links($baseLink, 'api/createFile'),
142
                'method' => 'post',
143
                'payloadFormat' => 'urlencoded',
144
            ],
145
            'createFolderEndpoint' => [
146
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
147
                'method' => 'post',
148
                'payloadFormat' => 'urlencoded',
149
            ],
150
            'readFolderEndpoint' => [
151
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
152
                'method' => 'get',
153
                'responseFormat' => 'json',
154
            ],
155
            'searchEndpoint' => [
156
                'url' => Controller::join_links($baseLink, 'api/search'),
157
                'method' => 'get',
158
                'responseFormat' => 'json',
159
            ],
160
            'updateFolderEndpoint' => [
161
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
162
                'method' => 'put',
163
                'payloadFormat' => 'urlencoded',
164
            ],
165
            'deleteEndpoint' => [
166
                'url' => Controller::join_links($baseLink, 'api/delete'),
167
                'method' => 'delete',
168
                'payloadFormat' => 'urlencoded',
169
            ],
170
            'historyEndpoint' => [
171
                'url' => Controller::join_links($baseLink, 'api/history'),
172
                'method' => 'get',
173
                'responseFormat' => 'json'
174
            ],
175
            'limit' => $this->config()->page_length,
176
            'form' => [
177
                'fileEditForm' => [
178
                    'schemaUrl' => $this->Link('schema/fileEditForm')
179
                ],
180
                'fileInsertForm' => [
181
                    'schemaUrl' => $this->Link('schema/fileInsertForm')
182
                ],
183
                'fileSelectForm' => [
184
                    'schemaUrl' => $this->Link('schema/fileSelectForm')
185
                ],
186
                'addToCampaignForm' => [
187
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
188
                ],
189
                'fileHistoryForm' => [
190
                    'schemaUrl' => $this->Link('schema/fileHistoryForm')
191
                ]
192
            ],
193
        ]);
194
    }
195
196
    /**
197
     * Fetches a collection of files by ParentID.
198
     *
199
     * @param HTTPRequest $request
200
     * @return HTTPResponse
201
     */
202
    public function apiReadFolder(HTTPRequest $request)
203
    {
204
        $params = $request->requestVars();
205
        $items = array();
206
        $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...
207
        $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...
208
209
        if (!isset($params['id']) && !strlen($params['id'])) {
210
            $this->httpError(400);
211
        }
212
213
        $folderID = (int)$params['id'];
214
        /** @var Folder $folder */
215
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
216
217
        if (!$folder) {
218
            $this->httpError(400);
219
        }
220
221
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
222
        $files = $this->getList()->filter('ParentID', $folderID);
223
224
        if ($files) {
225
            /** @var File $file */
226
            foreach ($files as $file) {
227
                if (!$file->canView()) {
228
                    continue;
229
                }
230
231
                $items[] = $this->getObjectFromData($file);
232
            }
233
        }
234
235
        // Build parents (for breadcrumbs)
236
        $parents = [];
237
        $next = $folder->Parent();
238
        while ($next && $next->exists()) {
239
            array_unshift($parents, [
240
                'id' => $next->ID,
241
                'title' => $next->getTitle(),
242
                'filename' => $next->getFilename(),
243
            ]);
244
            if ($next->ParentID) {
245
                $next = $next->Parent();
246
            } else {
247
                break;
248
            }
249
        }
250
251
        // Build response
252
        $response = new HTTPResponse();
253
        $response->addHeader('Content-Type', 'application/json');
254
        $response->setBody(json_encode([
255
            'files' => $items,
256
            'title' => $folder->getTitle(),
257
            'count' => count($items),
258
            'parents' => $parents,
259
            'parent' => $parents ? $parents[count($parents) - 1] : null,
260
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
261
            'folderID' => $folderID,
262
            'canEdit' => $folder->canEdit(),
263
            'canDelete' => $folder->canArchive(),
264
        ]));
265
266
        return $response;
267
    }
268
269
    /**
270
     * @param HTTPRequest $request
271
     *
272
     * @return HTTPResponse
273
     */
274
    public function apiSearch(HTTPRequest $request)
275
    {
276
        $params = $request->getVars();
277
        $list = $this->getList($params);
278
279
        $response = new HTTPResponse();
280
        $response->addHeader('Content-Type', 'application/json');
281
        $response->setBody(json_encode([
282
            // Serialisation
283
            "files" => array_map(function ($file) {
284
                return $this->getObjectFromData($file);
285
            }, $list->toArray()),
286
            "count" => $list->count(),
287
        ]));
288
289
        return $response;
290
    }
291
292
    /**
293
     * @param HTTPRequest $request
294
     *
295
     * @return HTTPResponse
296
     */
297
    public function apiDelete(HTTPRequest $request)
298
    {
299
        parse_str($request->getBody(), $vars);
300
301
        // CSRF check
302
        $token = SecurityToken::inst();
303 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...
304
            return new HTTPResponse(null, 400);
305
        }
306
307 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...
308
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
309
                ->addHeader('Content-Type', 'application/json');
310
        }
311
312
        $fileIds = $vars['ids'];
313
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
314
315 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...
316
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
317
                ->addHeader('Content-Type', 'application/json');
318
        }
319
320
        if (!min(array_map(function (File $file) {
321
            return $file->canArchive();
322
        }, $files))) {
323
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
324
                ->addHeader('Content-Type', 'application/json');
325
        }
326
327
        /** @var File $file */
328
        foreach ($files as $file) {
329
            $file->doArchive();
330
        }
331
332
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
333
            ->addHeader('Content-Type', 'application/json');
334
    }
335
336
    /**
337
     * Creates a single file based on a form-urlencoded upload.
338
     *
339
     * @param HTTPRequest $request
340
     * @return HTTPRequest|HTTPResponse
341
     */
342
    public function apiCreateFile(HTTPRequest $request)
343
    {
344
        $data = $request->postVars();
345
        $upload = $this->getUpload();
346
347
        // CSRF check
348
        $token = SecurityToken::inst();
349 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...
350
            return new HTTPResponse(null, 400);
351
        }
352
353
        // Check parent record
354
        /** @var Folder $parentRecord */
355
        $parentRecord = null;
356
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
357
            $parentRecord = Folder::get()->byID($data['ParentID']);
358
        }
359
        $data['Parent'] = $parentRecord;
360
361
        $tmpFile = $request->postVar('Upload');
362
        if (!$upload->validate($tmpFile)) {
363
            $result = ['message' => null];
364
            $errors = $upload->getErrors();
365
            if ($message = array_shift($errors)) {
366
                $result['message'] = [
367
                    'type' => 'error',
368
                    'value' => $message,
369
                ];
370
            }
371
            return (new HTTPResponse(json_encode($result), 400))
372
                ->addHeader('Content-Type', 'application/json');
373
        }
374
375
        // TODO Allow batch uploads
376
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
377
        /** @var File $file */
378
        $file = Injector::inst()->create($fileClass);
379
380
        // check canCreate permissions
381 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...
382
            $result = ['message' => [
383
                'type' => 'error',
384
                'value' => _t(
385
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
386
                    'You do not have permission to add files'
387
                )
388
            ]];
389
            return (new HTTPResponse(json_encode($result), 403))
390
                ->addHeader('Content-Type', 'application/json');
391
        }
392
393
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
394 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...
395
            $result = ['message' => [
396
                'type' => 'error',
397
                'value' => _t(
398
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
399
                    'Failed to load file'
400
                )
401
            ]];
402
            return (new HTTPResponse(json_encode($result), 400))
403
                ->addHeader('Content-Type', 'application/json');
404
        }
405
406
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
407
        $file->write();
408
409
        $result = [$this->getObjectFromData($file)];
410
411
        return (new HTTPResponse(json_encode($result)))
412
            ->addHeader('Content-Type', 'application/json');
413
    }
414
415
    /**
416
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
417
     *
418
     * @param HTTPRequest $request
419
     * @return HTTPResponse
420
     */
421
    public function apiHistory(HTTPRequest $request)
422
    {
423
        // CSRF check not required as the GET request has no side effects.
424
        $fileId = $request->getVar('fileId');
425
426
        if (!$fileId || !is_numeric($fileId)) {
427
            return new HTTPResponse(null, 400);
428
        }
429
430
        $class = File::class;
431
        $file = DataObject::get($class)->byID($fileId);
432
433
        if (!$file) {
434
            return new HTTPResponse(null, 404);
435
        }
436
437
        if (!$file->canView()) {
438
            return new HTTPResponse(null, 403);
439
        }
440
441
        $versions = Versioned::get_all_versions($class, $fileId)
442
            ->limit($this->config()->max_history_entries)
443
            ->sort('Version', 'DESC');
444
445
        $output = array();
446
        $next = array();
447
        $prev = null;
448
449
        // swap the order so we can get the version number to compare against.
450
        // i.e version 3 needs to know version 2 is the previous version
451
        $copy = $versions->map('Version', 'Version')->toArray();
452
        foreach (array_reverse($copy) as $k => $v) {
453
            if ($prev) {
454
                $next[$v] = $prev;
455
            }
456
457
            $prev = $v;
458
        }
459
460
        $_cachedMembers = array();
461
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
        return $this->getAbstractFileForm($id, 'fileEditForm');
749
    }
750
751
    /**
752
     * Get file edit form
753
     *
754
     * @return Form
755
     */
756 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...
757
    {
758
        // Get ID either from posted back value, or url parameter
759
        $request = $this->getRequest();
760
        $id = $request->param('ID') ?: $request->postVar('ID');
761
        return $this->getFileEditForm($id);
762
    }
763
764
    /**
765
     * The form is used to generate a form schema,
766
     * as well as an intermediary object to process data through API endpoints.
767
     * Since it's used directly on API endpoints, it does not have any form actions.
768
     * It handles both {@link File} and {@link Folder} records.
769
     *
770
     * @param int $id
771
     * @return Form
772
     */
773
    public function getFileInsertForm($id)
774
    {
775
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT ]);
776
    }
777
778
    /**
779
     * Get file insert form
780
     *
781
     * @return Form
782
     */
783 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...
784
    {
785
        // Get ID either from posted back value, or url parameter
786
        $request = $this->getRequest();
787
        $id = $request->param('ID') ?: $request->postVar('ID');
788
        return $this->getFileInsertForm($id);
789
    }
790
791
    /**
792
     * Abstract method for generating a form for a file
793
     *
794
     * @param int $id Record ID
795
     * @param string $name Form name
796
     * @param array $context Form context
797
     * @return Form
798
     */
799
    protected function getAbstractFileForm($id, $name, $context = [])
800
    {
801
        /** @var File $file */
802
        $file = $this->getList()->byID($id);
803
804 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...
805
            $this->httpError(403, _t(
806
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
807
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
808
                '',
809
                ['ObjectTitle' => $file->i18n_singular_name()]
810
            ));
811
            return null;
812
        }
813
814
        // Pass to form factory
815
        $augmentedContext = array_merge($context, ['Record' => $file]);
816
        $scaffolder = $this->getFormFactory($file);
817
        $form = $scaffolder->getForm($this, $name, $augmentedContext);
818
819
        // Configure form to respond to validation errors with form schema
820
        // if requested via react.
821
        $form->setValidationResponseCallback(function () use ($form, $file, $name) {
822
            $schemaId = Controller::join_links(
823
                $this->Link('schema'),
824
                $name,
825
                $file->isInDB() ? $file->ID : ''
826
            );
827
            return $this->getSchemaResponse($form, $schemaId);
828
        });
829
830
        return $form;
831
    }
832
833
    /**
834
     * Get form for selecting a file
835
     *
836
     * @return Form
837
     */
838
    public function fileSelectForm()
839
    {
840
        // Get ID either from posted back value, or url parameter
841
        $request = $this->getRequest();
842
        $id = $request->param('ID') ?: $request->postVar('ID');
843
        return $this->getFileSelectForm($id);
844
    }
845
846
    /**
847
     * Get form for selecting a file
848
     *
849
     * @param int $id ID of the record being selected
850
     * @return Form
851
     */
852
    public function getFileSelectForm($id)
853
    {
854
        return $this->getAbstractFileForm($id, 'fileSelectForm', [ 'Type' => AssetFormFactory::TYPE_SELECT ]);
855
    }
856
857
    /**
858
     * @param array $context
859
     * @return Form
860
     * @throws InvalidArgumentException
861
     */
862
    public function getFileHistoryForm($context)
863
    {
864
        // Check context
865
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
866
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
867
        }
868
        $id = $context['RecordID'];
869
        $versionId = $context['RecordVersion'];
870
        if (!$id || !$versionId) {
871
            return $this->httpError(404);
872
        }
873
874
        /** @var File $file */
875
        $file = Versioned::get_version(File::class, $id, $versionId);
876
        if (!$file) {
877
            return $this->httpError(404);
878
        }
879
880 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...
881
            $this->httpError(403, _t(
882
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
883
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
884
                '',
885
                ['ObjectTitle' => $file->i18n_singular_name()]
886
            ));
887
            return null;
888
        }
889
890
        $effectiveContext = array_merge($context, ['Record' => $file]);
891
        /** @var FormFactory $scaffolder */
892
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
893
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
894
895
        // Configure form to respond to validation errors with form schema
896
        // if requested via react.
897 View Code Duplication
        $form->setValidationResponseCallback(function () 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...
898
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
899
            return $this->getSchemaResponse($form, $schemaId);
900
        });
901
902
903
        return $form;
904
    }
905
906
    /**
907
     * Gets a JSON schema representing the current edit form.
908
     *
909
     * WARNING: Experimental API.
910
     *
911
     * @param HTTPRequest $request
912
     * @return HTTPResponse
913
     */
914
    public function schema($request)
915
    {
916
        $formName = $request->param('FormName');
917
        if ($formName !== 'fileHistoryForm') {
918
            return parent::schema($request);
919
        }
920
921
        // Get schema for history form
922
        // @todo Eventually all form scaffolding will be based on context rather than record ID
923
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
924
        $itemID = $request->param('ItemID');
925
        $version = $request->param('OtherItemID');
926
        $form = $this->getFileHistoryForm([
927
            'RecordID' => $itemID,
928
            'RecordVersion' => $version,
929
        ]);
930
931
        // Respond with this schema
932
        $response = $this->getResponse();
933
        $response->addHeader('Content-Type', 'application/json');
934
        $response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
0 ignored issues
show
Bug introduced by
It seems like $form defined by $this->getFileHistoryFor...dVersion' => $version)) on line 926 can be null; however, SilverStripe\Admin\LeftAndMain::getSchemaForForm() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
935
        return $response;
936
    }
937
938
    /**
939
     * Get file history form
940
     *
941
     * @return Form
942
     */
943
    public function fileHistoryForm()
944
    {
945
        $request = $this->getRequest();
946
        $id = $request->param('ID') ?: $request->postVar('ID');
947
        $version = $request->param('OtherID') ?: $request->postVar('Version');
948
        $form = $this->getFileHistoryForm([
949
            'RecordID' => $id,
950
            'RecordVersion' => $version,
951
        ]);
952
        return $form;
953
    }
954
955
    /**
956
     * @param array $data
957
     * @param Form $form
958
     * @return HTTPResponse
959
     */
960
    public function save($data, $form)
961
    {
962
        return $this->saveOrPublish($data, $form, false);
963
    }
964
965
    /**
966
     * @param array $data
967
     * @param Form $form
968
     * @return HTTPResponse
969
     */
970
    public function publish($data, $form)
971
    {
972
        return $this->saveOrPublish($data, $form, true);
973
    }
974
975
    /**
976
     * Update thisrecord
977
     *
978
     * @param array $data
979
     * @param Form $form
980
     * @param bool $doPublish
981
     * @return HTTPResponse
982
     */
983
    protected function saveOrPublish($data, $form, $doPublish = false)
984
    {
985 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...
986
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
987
                ->addHeader('Content-Type', 'application/json');
988
        }
989
990
        $id = (int) $data['ID'];
991
        /** @var File $record */
992
        $record = $this->getList()->filter('ID', $id)->first();
993
994 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...
995
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
996
                ->addHeader('Content-Type', 'application/json');
997
        }
998
999
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
1000
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1001
                ->addHeader('Content-Type', 'application/json');
1002
        }
1003
1004
        $form->saveInto($record);
1005
        $record->write();
1006
1007
        // Publish this record and owned objects
1008
        if ($doPublish) {
1009
            $record->publishRecursive();
1010
        }
1011
1012
        // Return the record data in the same response as the schema to save a postback
1013
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->exists() ? $record->ID : '');
1014
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id), $schemaId);
0 ignored issues
show
Bug introduced by
It seems like $this->getFileEditForm($id) can be null; however, getSchemaForForm() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1015
        $schemaData['record'] = $this->getObjectFromData($record);
1016
        $response = new HTTPResponse(Convert::raw2json($schemaData));
1017
        $response->addHeader('Content-Type', 'application/json');
1018
        return $response;
1019
    }
1020
1021
    public function unpublish($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1022
    {
1023 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...
1024
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1025
                ->addHeader('Content-Type', 'application/json');
1026
        }
1027
1028
        $id = (int) $data['ID'];
1029
        /** @var File $record */
1030
        $record = $this->getList()->filter('ID', $id)->first();
1031
1032
        if (!$record) {
1033
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1034
                ->addHeader('Content-Type', 'application/json');
1035
        }
1036
1037
        if (!$record->canUnpublish()) {
1038
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1039
                ->addHeader('Content-Type', 'application/json');
1040
        }
1041
1042
        $record->doUnpublish();
1043
1044
        // Return the record data in the same response as the schema to save a postback
1045
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->exists() ? $record->ID : '');
1046
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id), $schemaId);
0 ignored issues
show
Bug introduced by
It seems like $this->getFileEditForm($id) can be null; however, getSchemaForForm() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1047
        $schemaData['record'] = $this->getObjectFromData($record);
1048
        $response = new HTTPResponse(Convert::raw2json($schemaData));
1049
        $response->addHeader('Content-Type', 'application/json');
1050
        return $response;
1051
    }
1052
1053
    /**
1054
     * @param File $file
1055
     *
1056
     * @return array
1057
     */
1058
    public function getObjectFromData(File $file)
1059
    {
1060
        $object = array(
1061
            'id' => $file->ID,
1062
            'created' => $file->Created,
1063
            'lastUpdated' => $file->LastEdited,
1064
            'owner' => null,
1065
            'parent' => null,
1066
            'title' => $file->Title,
1067
            'exists' => $file->exists(), // Broken file check
1068
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1069
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
1070
            'name' => $file->Name,
1071
            'filename' => $file->Filename,
1072
            '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...
1073
            'size' => $file->Size,
1074
            'url' => $file->AbsoluteURL,
1075
            'published' => $file->isPublished(),
1076
            'modified' => $file->isModifiedOnDraft(),
1077
            'draft' => $file->isOnDraftOnly(),
1078
            'canEdit' => $file->canEdit(),
1079
            'canDelete' => $file->canArchive(),
1080
        );
1081
1082
        /** @var Member $owner */
1083
        $owner = $file->Owner();
1084
1085
        if ($owner) {
1086
            $object['owner'] = array(
1087
                'id' => $owner->ID,
1088
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1089
            );
1090
        }
1091
1092
        /** @var Folder $parent */
1093
        $parent = $file->Parent();
1094
1095
        if ($parent) {
1096
            $object['parent'] = array(
1097
                'id' => $parent->ID,
1098
                'title' => $parent->Title,
1099
                'filename' => $parent->Filename,
1100
            );
1101
        }
1102
1103
        /** @var File $file */
1104
        if ($file->getIsImage()) {
1105
            // Small thumbnail
1106
            $smallWidth = UploadField::config()->get('thumbnail_width');
1107
            $smallHeight = UploadField::config()->get('thumbnail_height');
1108
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1109
            if ($smallThumbnail && $smallThumbnail->exists()) {
1110
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1111
            }
1112
1113
            // Large thumbnail
1114
            $width = $this->config()->get('thumbnail_width');
1115
            $height = $this->config()->get('thumbnail_height');
1116
            $thumbnail = $file->FitMax($width, $height);
1117
            if ($thumbnail && $thumbnail->exists()) {
1118
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1119
            }
1120
            $object['dimensions']['width'] = $file->Width;
1121
            $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...
1122
        }
1123
1124
        return $object;
1125
    }
1126
1127
    /**
1128
     * Returns the files and subfolders contained in the currently selected folder,
1129
     * defaulting to the root node. Doubles as search results, if any search parameters
1130
     * are set through {@link SearchForm()}.
1131
     *
1132
     * @param array $params Unsanitised request parameters
1133
     * @return DataList
1134
     */
1135
    protected function getList($params = array())
1136
    {
1137
        $context = $this->getSearchContext();
1138
1139
        // Overwrite name filter to search both Name and Title attributes
1140
        $context->removeFilterByName('Name');
1141
1142
        // Lazy loaded list. Allows adding new filters through SearchContext.
1143
        /** @var DataList $list */
1144
        $list = $context->getResults($params);
1145
1146
        // Re-add previously removed "Name" filter as combined filter
1147
        // TODO Replace with composite SearchFilter once that API exists
1148
        if (!empty($params['Name'])) {
1149
            $list = $list->filterAny(array(
1150
                'Name:PartialMatch' => $params['Name'],
1151
                'Title:PartialMatch' => $params['Name']
1152
            ));
1153
        }
1154
1155
        // Optionally limit search to a folder (non-recursive)
1156
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
1157
            $list = $list->filter('ParentID', $params['ParentID']);
1158
        }
1159
1160
        // Date filtering
1161 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...
1162
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
1163
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
1164
        }
1165 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...
1166
            $toDate = new DateField(null, null, $params['CreatedTo']);
1167
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
1168
        }
1169
1170
        // Categories
1171
        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...
1172
            $extensions = File::config()->app_categories[$filters['AppCategory']];
1173
            $list = $list->filter('Name:PartialMatch', $extensions);
1174
        }
1175
1176
        // Sort folders first
1177
        $list = $list->sort(
1178
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
1179
        );
1180
1181
        // Pagination
1182
        if (isset($filters['page']) && isset($filters['limit'])) {
1183
            $page = $filters['page'];
1184
            $limit = $filters['limit'];
1185
            $offset = ($page - 1) * $limit;
1186
            $list = $list->limit($limit, $offset);
1187
        }
1188
1189
        // Access checks
1190
        $list = $list->filterByCallback(function (File $file) {
1191
            return $file->canView();
1192
        });
1193
1194
        return $list;
1195
    }
1196
1197
    /**
1198
     * Action handler for adding pages to a campaign
1199
     *
1200
     * @param array $data
1201
     * @param Form $form
1202
     * @return DBHTMLText|HTTPResponse
1203
     */
1204
    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...
1205
    {
1206
        $id = $data['ID'];
1207
        $record = $this->getList()->byID($id);
1208
1209
        $handler = AddToCampaignHandler::create($this, $record);
1210
        $results = $handler->addToCampaign($record, $data['Campaign']);
1211
        if (!isset($results)) {
1212
            return null;
1213
        }
1214
        $request = $this->getRequest();
1215
        if ($request->getHeader('X-Formschema-Request')) {
1216
            $data = $this->getSchemaForForm($handler->Form($record));
1217
            $data['message'] = $results;
1218
1219
            $response = new HTTPResponse(Convert::raw2json($data));
1220
            $response->addHeader('Content-Type', 'application/json');
1221
            return $response;
1222
        }
1223
        return $results;
1224
    }
1225
1226
    /**
1227
     * Url handler for add to campaign form
1228
     *
1229
     * @param HTTPRequest $request
1230
     * @return Form
1231
     */
1232
    public function addToCampaignForm($request)
1233
    {
1234
        // Get ID either from posted back value, or url parameter
1235
        $id = $request->param('ID') ?: $request->postVar('ID');
1236
        return $this->getAddToCampaignForm($id);
1237
    }
1238
1239
    /**
1240
     * @param int $id
1241
     * @return Form
1242
     */
1243
    public function getAddToCampaignForm($id)
1244
    {
1245
        // Get record-specific fields
1246
        $record = $this->getList()->byID($id);
1247
1248
        if (!$record) {
1249
            $this->httpError(404, _t(
1250
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1251
                'That {Type} couldn\'t be found',
1252
                '',
1253
                ['Type' => File::singleton()->i18n_singular_name()]
1254
            ));
1255
            return null;
1256
        }
1257 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...
1258
            $this->httpError(403, _t(
1259
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1260
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1261
                '',
1262
                ['ObjectTitle' => $record->i18n_singular_name()]
1263
            ));
1264
            return null;
1265
        }
1266
1267
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1268
        $form = $handler->Form($record);
1269
1270 View Code Duplication
        $form->setValidationResponseCallback(function () 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...
1271
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1272
            return $this->getSchemaResponse($form, $schemaId);
1273
        });
1274
1275
        return $form;
1276
    }
1277
1278
    /**
1279
     * @return Upload
1280
     */
1281
    protected function getUpload()
1282
    {
1283
        $upload = Upload::create();
1284
        $upload->getValidator()->setAllowedExtensions(
1285
            // filter out '' since this would be a regex problem on JS end
1286
            array_filter(File::config()->get('allowed_extensions'))
1287
        );
1288
1289
        return $upload;
1290
    }
1291
}
1292