Completed
Pull Request — master (#327)
by
unknown
02:00
created

AssetAdmin::apiReadFolder()   F

Complexity

Conditions 28
Paths 768

Size

Total Lines 102
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 102
rs 2.1804
cc 28
eloc 64
nc 768
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

Loading history...
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
57
        // Legacy redirect for SS3-style detail view
58
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
59
        // Pass all URLs to the index, for React to unpack
60
        'show/$FolderID/edit/$FileID' => 'index',
61
        // API access points with structured data
62
        'POST api/createFolder' => 'apiCreateFolder',
63
        'POST api/createFile' => 'apiCreateFile',
64
        'GET api/readFolder' => 'apiReadFolder',
65
        'PUT api/updateFolder' => 'apiUpdateFolder',
66
        'DELETE api/delete' => 'apiDelete',
67
        'GET api/search' => 'apiSearch',
68
        'GET api/history' => 'apiHistory'
69
    ];
70
71
    /**
72
     * Amount of results showing on a single page.
73
     *
74
     * @config
75
     * @var int
76
     */
77
    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...
78
79
    /**
80
     * @config
81
     * @see Upload->allowedMaxFileSize
82
     * @var int
83
     */
84
    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...
85
86
    /**
87
     * @config
88
     *
89
     * @var int
90
     */
91
    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...
92
93
    /**
94
     * @var array
95
     */
96
    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...
97
        'legacyRedirectForEditView',
98
        'apiCreateFolder',
99
        'apiCreateFile',
100
        'apiReadFolder',
101
        'apiUpdateFolder',
102
        'apiHistory',
103
        'apiDelete',
104
        'apiSearch',
105
        'FileEditForm',
106
        'FileHistoryForm',
107
        'AddToCampaignForm',
108
        'FileInsertForm',
109
        'schema',
110
    );
111
112
    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...
113
114
    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...
115
116
    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...
117
118
    /**
119
     * Set up the controller
120
     */
121
    public function init()
122
    {
123
        parent::init();
124
125
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
126
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
127
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
128
129
        CMSBatchActionHandler::register(
130
            'delete',
131
            'SilverStripe\AssetAdmin\BatchAction\DeleteAssets',
132
            'SilverStripe\\Assets\\Folder'
133
        );
134
    }
135
136
    public function getClientConfig()
137
    {
138
        $baseLink = $this->Link();
139
        return array_merge( parent::getClientConfig(), [
140
            'reactRouter' => true,
141
            'createFileEndpoint' => [
142
                'url' => Controller::join_links($baseLink, 'api/createFile'),
143
                'method' => 'post',
144
                'payloadFormat' => 'urlencoded',
145
            ],
146
            'createFolderEndpoint' => [
147
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
148
                'method' => 'post',
149
                'payloadFormat' => 'urlencoded',
150
            ],
151
            'readFolderEndpoint' => [
152
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
153
                'method' => 'get',
154
                'responseFormat' => 'json',
155
            ],
156
            'searchEndpoint' => [
157
                'url' => Controller::join_links($baseLink, 'api/search'),
158
                'method' => 'get',
159
                'responseFormat' => 'json',
160
            ],
161
            'updateFolderEndpoint' => [
162
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
163
                'method' => 'put',
164
                'payloadFormat' => 'urlencoded',
165
            ],
166
            'deleteEndpoint' => [
167
                'url' => Controller::join_links($baseLink, 'api/delete'),
168
                'method' => 'delete',
169
                'payloadFormat' => 'urlencoded',
170
            ],
171
            'historyEndpoint' => [
172
                'url' => Controller::join_links($baseLink, 'api/history'),
173
                'method' => 'get',
174
                'responseFormat' => 'json'
175
            ],
176
            'limit' => $this->config()->page_length,
177
            'form' => [
178
                'FileEditForm' => [
179
                    'schemaUrl' => $this->Link('schema/FileEditForm')
180
                ],
181
                'FileInsertForm' => [
182
                    'schemaUrl' => $this->Link('schema/FileInsertForm')
183
                ],
184
                'AddToCampaignForm' => [
185
                    'schemaUrl' => $this->Link('schema/AddToCampaignForm')
186
                ],
187
                'FileHistoryForm' => [
188
                    'schemaUrl' => $this->Link('schema/FileHistoryForm')
189
                ]
190
            ],
191
        ]);
192
    }
193
194
    /**
195
     * Fetches a collection of files by ParentID.
196
     *
197
     * @param HTTPRequest $request
198
     * @return HTTPResponse
199
     */
200
    public function apiReadFolder(HTTPRequest $request)
201
    {
202
        $params = $request->requestVars();
203
        $items = array();
204
        $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...
205
        $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...
206
207
        if (!isset($params['id']) && !strlen($params['id'])) {
208
            $this->httpError(400);
209
        }
210
211
        $folderID = (int)$params['id'];
212
        /** @var Folder $folder */
213
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
214
215
        if (!$folder) {
216
            $this->httpError(400);
217
        }
218
219
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
220
        $files = $this->getList()->filter('ParentID', $folderID);
221
222
        if ($files) {
223
            /** @var File $file */
224
            foreach ($files as $file) {
225
                if (!$file->canView()) {
226
                    continue;
227
                }
228
229
                $items[] = $this->getObjectFromData($file);
230
            }
231
        }
232
233
        // Build parents (for breadcrumbs)
234
        $parents = [];
235
        $next = $folder->Parent();
236
        while($next && $next->exists()) {
237
            array_unshift($parents, [
238
                'id' => $next->ID,
239
                'title' => $next->getTitle(),
240
                'filename' => $next->getFilename(),
241
            ]);
242
            if($next->ParentID) {
243
                $next = $next->Parent();
244
            } else {
245
                break;
246
            }
247
        }
248
    
249
        $column = 'title';
250
        $direction = 'asc';
251
        if (isset($params['sort'])) {
252
            list($column, $direction) = explode(',', $params['sort']);
253
        }
254
        $multiplier = ($direction === 'asc') ? 1 : -1;
255
        
256
        usort($items, function($a, $b) use ($column, $multiplier) {
257
            if (!isset($a[$column]) || !isset($b[$column])) {
258
                return 0;
259
            }
260
            if ($a['type'] === 'folder' && $b['type'] !== 'folder') {
261
                return -1;
262
            }
263
            if ($b['type'] === 'folder' && $a['type'] !== 'folder') {
264
                return 1;
265
            }
266
            $numeric = (is_numeric($a[$column]) && is_numeric($b[$column]));
267
            $fieldA = ($numeric) ? floatval($a[$column]) : strtolower($a[$column]);
268
            $fieldB = ($numeric) ? floatval($b[$column]) : strtolower($b[$column]);
269
270
            if ($fieldA < $fieldB) {
271
                return $multiplier * -1;
272
            }
273
274
            if ($fieldA > $fieldB) {
275
                return $multiplier;
276
            }
277
278
            return 0;
279
        });
280
        
281
        $page = (isset($params['page'])) ? $params['page'] : 0;
282
        $limit = (isset($params['limit'])) ? $params['limit'] : $this->config()->page_length;
283
        $filteredItems = array_slice($items, $page * $limit, $limit);
284
285
        // Build response
286
        $response = new HTTPResponse();
287
        $response->addHeader('Content-Type', 'application/json');
288
        $response->setBody(json_encode([
289
            'files' => $filteredItems,
290
            'title' => $folder->getTitle(),
291
            'count' => count($items),
292
            'parents' => $parents,
293
            'parent' => $parents ? $parents[count($parents) - 1] : null,
294
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
295
            'folderID' => $folderID,
296
            'canEdit' => $folder->canEdit(),
297
            'canDelete' => $folder->canArchive(),
298
        ]));
299
300
        return $response;
301
    }
302
303
    /**
304
     * @param HTTPRequest $request
305
     *
306
     * @return HTTPResponse
307
     */
308
    public function apiSearch(HTTPRequest $request)
309
    {
310
        $params = $request->getVars();
311
        $list = $this->getList($params);
312
313
        $response = new HTTPResponse();
314
        $response->addHeader('Content-Type', 'application/json');
315
        $response->setBody(json_encode([
316
            // Serialisation
317
            "files" => array_map(function($file) {
318
                return $this->getObjectFromData($file);
319
            }, $list->toArray()),
320
            "count" => $list->count(),
321
        ]));
322
323
        return $response;
324
    }
325
326
    /**
327
     * @param HTTPRequest $request
328
     *
329
     * @return HTTPResponse
330
     */
331
    public function apiDelete(HTTPRequest $request)
332
    {
333
        parse_str($request->getBody(), $vars);
334
335
        // CSRF check
336
        $token = SecurityToken::inst();
337 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...
338
            return new HTTPResponse(null, 400);
339
        }
340
341 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...
342
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
343
                ->addHeader('Content-Type', 'application/json');
344
        }
345
346
        $fileIds = $vars['ids'];
347
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
348
349 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...
350
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
351
                ->addHeader('Content-Type', 'application/json');
352
        }
353
354
        if (!min(array_map(function (File $file) {
355
            return $file->canArchive();
356
        }, $files))) {
357
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
358
                ->addHeader('Content-Type', 'application/json');
359
        }
360
361
        /** @var File $file */
362
        foreach ($files as $file) {
363
            $file->doArchive();
364
        }
365
366
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
367
            ->addHeader('Content-Type', 'application/json');
368
    }
369
370
    /**
371
     * Creates a single file based on a form-urlencoded upload.
372
     *
373
     * @param HTTPRequest $request
374
     * @return HTTPRequest|HTTPResponse
375
     */
376
    public function apiCreateFile(HTTPRequest $request)
377
    {
378
        $data = $request->postVars();
379
        $upload = $this->getUpload();
380
381
        // CSRF check
382
        $token = SecurityToken::inst();
383 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...
384
            return new HTTPResponse(null, 400);
385
        }
386
387
        // Check parent record
388
        /** @var Folder $parentRecord */
389
        $parentRecord = null;
390
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
391
            $parentRecord = Folder::get()->byID($data['ParentID']);
392
        }
393
        $data['Parent'] = $parentRecord;
394
395
        $tmpFile = $request->postVar('Upload');
396
        if(!$upload->validate($tmpFile)) {
397
            $result = ['message' => null];
398
            $errors = $upload->getErrors();
399
            if ($message = array_shift($errors)) {
400
                $result['message'] = [
401
                    'type' => 'error',
402
                    'value' => $message,
403
                ];
404
            }
405
            return (new HTTPResponse(json_encode($result), 400))
406
                ->addHeader('Content-Type', 'application/json');
407
        }
408
409
        // TODO Allow batch uploads
410
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
411
        /** @var File $file */
412
        $file = Injector::inst()->create($fileClass);
413
414
        // check canCreate permissions
415 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...
416
            $result = ['message' => [
417
                'type' => 'error',
418
                'value' => _t(
419
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
420
                    'You do not have permission to add files'
421
                )
422
            ]];
423
            return (new HTTPResponse(json_encode($result), 403))
424
                ->addHeader('Content-Type', 'application/json');
425
        }
426
427
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
428 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...
429
            $result = ['message' => [
430
                'type' => 'error',
431
                'value' => _t(
432
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
433
                    'Failed to load file'
434
                )
435
            ]];
436
            return (new HTTPResponse(json_encode($result), 400))
437
                ->addHeader('Content-Type', 'application/json');
438
        }
439
440
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
441
        $file->write();
442
443
        $result = [$this->getObjectFromData($file)];
444
445
        return (new HTTPResponse(json_encode($result)))
446
            ->addHeader('Content-Type', 'application/json');
447
    }
448
449
    /**
450
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
451
     *
452
     * @param HTTPRequest
453
     *
454
     * @return HTTPResponse
455
     */
456
    public function apiHistory(HTTPRequest $request)
457
    {
458
        // CSRF check not required as the GET request has no side effects.
459
        $fileId = $request->getVar('fileId');
460
461
        if(!$fileId || !is_numeric($fileId)) {
462
            return new HTTPResponse(null, 400);
463
        }
464
465
        $class = File::class;
466
        $file = DataObject::get($class)->byId($fileId);
467
468
        if(!$file) {
469
            return new HTTPResponse(null, 404);
470
        }
471
472
        if(!$file->canView()) {
473
            return new HTTPResponse(null, 403);
474
        }
475
476
        $versions = Versioned::get_all_versions($class, $fileId)
477
            ->limit($this->config()->max_history_entries)
478
            ->sort('Version', 'DESC');
479
480
        $output = array();
481
        $next = array();
482
        $prev = null;
483
484
        // swap the order so we can get the version number to compare against.
485
        // i.e version 3 needs to know version 2 is the previous version
486
        $copy = $versions->map('Version', 'Version')->toArray();
487
        foreach(array_reverse($copy) as $k => $v) {
488
            if($prev) {
489
                $next[$v] = $prev;
490
            }
491
492
            $prev = $v;
493
        }
494
495
        $_cachedMembers = array();
496
497
        foreach($versions as $version) {
498
            $author = null;
499
500
            if($version->AuthorID) {
501
                if(!isset($_cachedMembers[$version->AuthorID])) {
502
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
503
                        ->byId($version->AuthorID);
504
                }
505
506
                $author = $_cachedMembers[$version->AuthorID];
507
            }
508
509
            if($version->canView()) {
510
                $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...
511
512
                if(isset($next[$version->Version])) {
513
                    $summary = $version->humanizedChanges(
514
                        $version->Version,
515
                        $next[$version->Version]
516
                    );
517
518
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
519
                    // generic message
520
                    if(!$summary) {
521
                        $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.SAVEDFILE', "Saved file");
522
                    }
523
                } else {
524
                    $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UPLOADEDFILE', "Uploaded file");
525
                }
526
527
                $output[] = array(
528
                    'versionid' => $version->Version,
529
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
530
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
531
                    'status' => ($version->WasPublished) ? _t('File.PUBLISHED', 'Published') : '',
532
                    'author' => ($author)
533
                        ? $author->Name
534
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNKNOWN', "Unknown"),
535
                    'summary' => ($summary) ? $summary : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NOSUMMARY', "No summary available")
536
                );
537
            }
538
        }
539
540
        return
541
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
542
543
    }
544
545
546
    /**
547
     * Creates a single folder, within an optional parent folder.
548
     *
549
     * @param HTTPRequest $request
550
     * @return HTTPRequest|HTTPResponse
551
     */
552
    public function apiCreateFolder(HTTPRequest $request)
553
    {
554
        $data = $request->postVars();
555
556
        $class = 'SilverStripe\\Assets\\Folder';
557
558
        // CSRF check
559
        $token = SecurityToken::inst();
560 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...
561
            return new HTTPResponse(null, 400);
562
        }
563
564
        // check addchildren permissions
565
        /** @var Folder $parentRecord */
566
        $parentRecord = null;
567
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
568
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
569
        }
570
        $data['Parent'] = $parentRecord;
571
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
572
573
        // Build filename
574
        $baseFilename = isset($data['Name'])
575
            ? basename($data['Name'])
576
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
577
578
        if ($parentRecord && $parentRecord->ID) {
579
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
580
        }
581
582
        // Ensure name is unique
583
        $nameGenerator = $this->getNameGenerator($baseFilename);
584
        $filename = null;
585
        foreach ($nameGenerator as $filename) {
586
            if (! File::find($filename)) {
587
                break;
588
            }
589
        }
590
        $data['Name'] = basename($filename);
591
592
        // Create record
593
        /** @var Folder $record */
594
        $record = Injector::inst()->create($class);
595
596
        // check create permissions
597
        if (!$record->canCreate(null, $data)) {
598
            return (new HTTPResponse(null, 403))
599
                ->addHeader('Content-Type', 'application/json');
600
        }
601
602
        $record->ParentID = $data['ParentID'];
603
        $record->Name = $record->Title = basename($data['Name']);
604
        $record->write();
605
606
        $result = $this->getObjectFromData($record);
607
608
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
609
    }
610
611
    /**
612
     * Redirects 3.x style detail links to new 4.x style routing.
613
     *
614
     * @param HTTPRequest $request
615
     */
616
    public function legacyRedirectForEditView($request)
617
    {
618
        $fileID = $request->param('FileID');
619
        /** @var File $file */
620
        $file = File::get()->byID($fileID);
621
        $link = $this->getFileEditLink($file) ?: $this->Link();
622
        $this->redirect($link);
623
    }
624
625
    /**
626
     * Given a file return the CMS link to edit it
627
     *
628
     * @param File $file
629
     * @return string
630
     */
631
    public function getFileEditLink($file)
632
    {
633
        if(!$file || !$file->isInDB()) {
634
            return null;
635
        }
636
637
        return Controller::join_links(
638
            $this->Link('show'),
639
            $file->ParentID,
640
            'edit',
641
            $file->ID
642
        );
643
    }
644
645
    /**
646
     * Get the search context from {@link File}, used to create the search form
647
     * as well as power the /search API endpoint.
648
     *
649
     * @return SearchContext
650
     */
651
    public function getSearchContext()
652
    {
653
        $context = File::singleton()->getDefaultSearchContext();
654
655
        // Customize fields
656
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
657
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
658
        ->setConfig('showcalendar', true);
659
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
660
        ->setConfig('showcalendar', true);
661
        $dateGroup = FieldGroup::create(
662
            $dateHeader,
663
            $dateFrom,
664
            $dateTo
665
        );
666
        $context->addField($dateGroup);
667
        /** @skipUpgrade */
668
        $appCategories = array(
669
            'archive' => _t(
670
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
671
                'Archive'
672
            ),
673
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
674
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
675
            'flash' => _t(
676
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
677
                'Flash',
678
                'The fileformat'
679
            ),
680
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
681
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
682
        );
683
        $context->addField(
684
            $typeDropdown = new DropdownField(
685
                'AppCategory',
686
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
687
                $appCategories
688
            )
689
        );
690
691
        $typeDropdown->setEmptyString(' ');
692
693
        $currentfolderLabel = _t(
694
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
695
            'Limit to current folder?'
696
        );
697
        $context->addField(
698
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
699
        );
700
        $context->getFields()->removeByName('Title');
701
702
        return $context;
703
    }
704
705
    /**
706
     * Get an asset renamer for the given filename.
707
     *
708
     * @param  string             $filename Path name
709
     * @return AssetNameGenerator
710
     */
711
    protected function getNameGenerator($filename)
712
    {
713
        return Injector::inst()
714
            ->createWithArgs('AssetNameGenerator', array($filename));
715
    }
716
717
    /**
718
     * @todo Implement on client
719
     *
720
     * @param bool $unlinked
721
     * @return ArrayList
722
     */
723
    public function breadcrumbs($unlinked = false)
724
    {
725
        return null;
726
    }
727
728
729
    /**
730
     * Don't include class namespace in auto-generated CSS class
731
     */
732
    public function baseCSSClasses()
733
    {
734
        return 'AssetAdmin LeftAndMain';
735
    }
736
737
    public function providePermissions()
738
    {
739
        return array(
740
            "CMS_ACCESS_AssetAdmin" => array(
741
                '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...
742
                    'title' => static::menu_title()
743
                )),
744
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
745
            )
746
        );
747
    }
748
749
    /**
750
     * Build a form scaffolder for this model
751
     *
752
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
753
     *
754
     * @param File $file
755
     * @return FormFactory
756
     */
757
    public function getFormFactory(File $file)
758
    {
759
        // Get service name based on file class
760
        $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...
761
        if ($file instanceof Folder) {
762
            $name = FolderFormFactory::class;
763
        } elseif ($file instanceof Image) {
764
            $name = ImageFormFactory::class;
765
        } else {
766
            $name = FileFormFactory::class;
767
        }
768
        return Injector::inst()->get($name);
769
    }
770
771
    /**
772
     * The form is used to generate a form schema,
773
     * as well as an intermediary object to process data through API endpoints.
774
     * Since it's used directly on API endpoints, it does not have any form actions.
775
     * It handles both {@link File} and {@link Folder} records.
776
     *
777
     * @param int $id
778
     * @return Form
779
     */
780
    public function getFileEditForm($id)
781
    {
782
        /** @var File $file */
783
        $file = $this->getList()->byID($id);
784
785 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...
786
            $this->httpError(403, _t(
787
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
788
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
789
                '',
790
                ['ObjectTitle' => $file->i18n_singular_name()]
791
            ));
792
            return null;
793
        }
794
795
        $scaffolder = $this->getFormFactory($file);
796
        $form = $scaffolder->getForm($this, 'FileEditForm', [
797
            'Record' => $file
798
        ]);
799
800
        // Configure form to respond to validation errors with form schema
801
        // if requested via react.
802 View Code Duplication
        $form->setValidationResponseCallback(function() use ($form, $file) {
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...
803
            $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $file->exists() ? $file->ID : '');
804
            return $this->getSchemaResponse($form, $schemaId);
805
        });
806
807
        return $form;
808
    }
809
810
    /**
811
     * Get file edit form
812
     *
813
     * @return Form
814
     */
815 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...
816
    {
817
        // Get ID either from posted back value, or url parameter
818
        $request = $this->getRequest();
819
        $id = $request->param('ID') ?: $request->postVar('ID');
820
        return $this->getFileEditForm($id);
821
    }
822
823
    /**
824
     * The form is used to generate a form schema,
825
     * as well as an intermediary object to process data through API endpoints.
826
     * Since it's used directly on API endpoints, it does not have any form actions.
827
     * It handles both {@link File} and {@link Folder} records.
828
     *
829
     * @param int $id
830
     * @return Form
831
     */
832
    public function getFileInsertForm($id)
833
    {
834
        /** @var File $file */
835
        $file = $this->getList()->byID($id);
836
837 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...
838
            $this->httpError(403, _t(
839
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
840
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
841
                '',
842
                ['ObjectTitle' => $file->i18n_singular_name()]
843
            ));
844
            return null;
845
        }
846
847
        $scaffolder = $this->getFormFactory($file);
848
        $form = $scaffolder->getForm($this, 'FileInsertForm', [
849
            'Record' => $file,
850
            'Type' => 'insert',
851
        ]);
852
853
        return $form;
854
    }
855
856
    /**
857
     * Get file insert form
858
     *
859
     * @return Form
860
     */
861 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...
862
    {
863
        // Get ID either from posted back value, or url parameter
864
        $request = $this->getRequest();
865
        $id = $request->param('ID') ?: $request->postVar('ID');
866
        return $this->getFileInsertForm($id);
867
    }
868
869
    /**
870
     * @param array $context
871
     * @return Form
872
     * @throws InvalidArgumentException
873
     */
874
    public function getFileHistoryForm($context)
875
    {
876
        // Check context
877
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
878
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
879
        }
880
        $id = $context['RecordID'];
881
        $versionId = $context['RecordVersion'];
882
        if(!$id || !$versionId) {
883
            return $this->httpError(404);
884
        }
885
886
        /** @var File $file */
887
        $file = Versioned::get_version(File::class, $id, $versionId);
888
        if (!$file) {
889
            return $this->httpError(404);
890
        }
891
892 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...
893
            $this->httpError(403, _t(
894
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
895
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
896
                '',
897
                ['ObjectTitle' => $file->i18n_singular_name()]
898
            ));
899
            return null;
900
        }
901
902
        $effectiveContext = array_merge($context, ['Record' => $file]);
903
        /** @var FormFactory $scaffolder */
904
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
905
        $form = $scaffolder->getForm($this, 'FileHistoryForm', $effectiveContext);
906
907
        // Configure form to respond to validation errors with form schema
908
        // if requested via react.
909 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...
910
            $schemaId = Controller::join_links($this->Link('schema/FileHistoryForm'), $id, $versionId);
911
            return $this->getSchemaResponse($form, $schemaId);
912
        });
913
914
915
        return $form;
916
    }
917
918
    /**
919
	 * Gets a JSON schema representing the current edit form.
920
	 *
921
	 * WARNING: Experimental API.
922
	 *
923
	 * @param HTTPRequest $request
924
	 * @return HTTPResponse
925
	 */
926
	public function schema($request) {
927
		$formName = $request->param('FormName');
928
        if ($formName !== 'FileHistoryForm') {
929
            return parent::schema($request);
930
        }
931
932
        // Get schema for history form
933
        // @todo Eventually all form scaffolding will be based on context rather than record ID
934
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
935
		$itemID = $request->param('ItemID');
936
        $version = $request->param('OtherItemID');
937
        $form = $this->getFileHistoryForm([
938
            'RecordID' => $itemID,
939
            'RecordVersion' => $version,
940
        ]);
941
942
        // Respond with this schema
943
		$response = $this->getResponse();
944
        $response->addHeader('Content-Type', 'application/json');
945
        $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 937 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...
946
        return $response;
947
	}
948
949
    /**
950
     * Get file history form
951
     *
952
     * @return Form
953
     */
954
    public function FileHistoryForm()
955
    {
956
        $request = $this->getRequest();
957
        $id = $request->param('ID') ?: $request->postVar('ID');
958
        $version = $request->param('OtherID') ?: $request->postVar('Version');
959
        $form = $this->getFileHistoryForm([
960
            'RecordID' => $id,
961
            'RecordVersion' => $version,
962
        ]);
963
        return $form;
964
    }
965
966
    /**
967
     * @param array $data
968
     * @param Form $form
969
     * @return HTTPResponse
970
     */
971
    public function save($data, $form)
972
    {
973
        return $this->saveOrPublish($data, $form, false);
974
    }
975
976
    /**
977
     * @param array $data
978
     * @param Form $form
979
     * @return HTTPResponse
980
     */
981
    public function publish($data, $form)
982
    {
983
        return $this->saveOrPublish($data, $form, true);
984
    }
985
986
    /**
987
     * Update thisrecord
988
     *
989
     * @param array $data
990
     * @param Form $form
991
     * @param bool $doPublish
992
     * @return HTTPResponse
993
     */
994
    protected function saveOrPublish($data, $form, $doPublish = false)
995
    {
996 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...
997
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
998
                ->addHeader('Content-Type', 'application/json');
999
        }
1000
1001
        $id = (int) $data['ID'];
1002
        /** @var File $record */
1003
        $record = $this->getList()->filter('ID', $id)->first();
1004
1005 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...
1006
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1007
                ->addHeader('Content-Type', 'application/json');
1008
        }
1009
1010
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
1011
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1012
                ->addHeader('Content-Type', 'application/json');
1013
        }
1014
1015
        $form->saveInto($record);
1016
        $record->write();
1017
1018
        // Publish this record and owned objects
1019
        if ($doPublish) {
1020
            $record->publishRecursive();
1021
        }
1022
1023
        // Return the record data in the same response as the schema to save a postback
1024
        $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $record->exists() ? $record->ID : '');
1025
        $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...
1026
        $schemaData['record'] = $this->getObjectFromData($record);
1027
        $response = new HTTPResponse(Convert::raw2json($schemaData));
1028
        $response->addHeader('Content-Type', 'application/json');
1029
        return $response;
1030
    }
1031
1032
    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...
1033
    {
1034 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...
1035
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1036
                ->addHeader('Content-Type', 'application/json');
1037
        }
1038
1039
        $id = (int) $data['ID'];
1040
        /** @var File $record */
1041
        $record = $this->getList()->filter('ID', $id)->first();
1042
1043
        if (!$record) {
1044
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1045
                ->addHeader('Content-Type', 'application/json');
1046
        }
1047
1048
        if (!$record->canUnpublish()) {
1049
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1050
                ->addHeader('Content-Type', 'application/json');
1051
        }
1052
1053
        $record->doUnpublish();
1054
1055
        // Return the record data in the same response as the schema to save a postback
1056
        $schemaId = Controller::join_links($this->Link('schema/FileEditForm'), $record->exists() ? $record->ID : '');
1057
        $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...
1058
        $schemaData['record'] = $this->getObjectFromData($record);
1059
        $response = new HTTPResponse(Convert::raw2json($schemaData));
1060
        $response->addHeader('Content-Type', 'application/json');
1061
        return $response;
1062
    }
1063
1064
    /**
1065
     * @param File $file
1066
     *
1067
     * @return array
1068
     */
1069
    public function getObjectFromData(File $file)
1070
    {
1071
        $object = array(
1072
            'id' => $file->ID,
1073
            'created' => $file->Created,
1074
            'lastUpdated' => $file->LastEdited,
1075
            'owner' => null,
1076
            'parent' => null,
1077
            'title' => $file->Title,
1078
            'exists' => $file->exists(), // Broken file check
1079
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1080
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
1081
            'name' => $file->Name,
1082
            'filename' => $file->Filename,
1083
            '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...
1084
            'size' => $file->AbsoluteSize,
1085
            'url' => $file->AbsoluteURL,
1086
            'published' => $file->isPublished(),
1087
            'modified' => $file->isModifiedOnDraft(),
1088
            'draft' => $file->isOnDraftOnly(),
1089
            'canEdit' => $file->canEdit(),
1090
            'canDelete' => $file->canArchive(),
1091
        );
1092
1093
        /** @var Member $owner */
1094
        $owner = $file->Owner();
1095
1096
        if ($owner) {
1097
            $object['owner'] = array(
1098
                'id' => $owner->ID,
1099
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1100
            );
1101
        }
1102
1103
        /** @var Folder $parent */
1104
        $parent = $file->Parent();
1105
1106
        if ($parent) {
1107
            $object['parent'] = array(
1108
                'id' => $parent->ID,
1109
                'title' => $parent->Title,
1110
                'filename' => $parent->Filename,
1111
            );
1112
        }
1113
1114
        /** @var File $file */
1115
        if ($file->getIsImage()) {
1116
            // Small thumbnail
1117
            $smallWidth = UploadField::config()->get('thumbnail_width');
1118
            $smallHeight = UploadField::config()->get('thumbnail_height');
1119
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1120
            if ($smallThumbnail && $smallThumbnail->exists()) {
1121
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1122
            }
1123
1124
            // Large thumbnail
1125
            $width = $this->config()->get('thumbnail_width');
1126
            $height = $this->config()->get('thumbnail_height');
1127
            $thumbnail = $file->FitMax($width, $height);
1128
            if ($thumbnail && $thumbnail->exists()) {
1129
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1130
            }
1131
            $object['dimensions']['width'] = $file->Width;
1132
            $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...
1133
        } else {
1134
            $object['thumbnail'] = $file->PreviewLink();
1135
        }
1136
1137
        return $object;
1138
    }
1139
1140
    /**
1141
     * Returns the files and subfolders contained in the currently selected folder,
1142
     * defaulting to the root node. Doubles as search results, if any search parameters
1143
     * are set through {@link SearchForm()}.
1144
     *
1145
     * @param array $params Unsanitised request parameters
1146
     * @return DataList
1147
     */
1148
    protected function getList($params = array())
1149
    {
1150
        $context = $this->getSearchContext();
1151
1152
        // Overwrite name filter to search both Name and Title attributes
1153
        $context->removeFilterByName('Name');
1154
1155
        // Lazy loaded list. Allows adding new filters through SearchContext.
1156
        /** @var DataList $list */
1157
        $list = $context->getResults($params);
1158
1159
        // Re-add previously removed "Name" filter as combined filter
1160
        // TODO Replace with composite SearchFilter once that API exists
1161
        if(!empty($params['Name'])) {
1162
            $list = $list->filterAny(array(
1163
                'Name:PartialMatch' => $params['Name'],
1164
                'Title:PartialMatch' => $params['Name']
1165
            ));
1166
        }
1167
1168
        // Optionally limit search to a folder (non-recursive)
1169
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
1170
            $list = $list->filter('ParentID', $params['ParentID']);
1171
        }
1172
1173
        // Date filtering
1174 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...
1175
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
1176
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
1177
        }
1178 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...
1179
            $toDate = new DateField(null, null, $params['CreatedTo']);
1180
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
1181
        }
1182
1183
        // Categories
1184
        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...
1185
            $extensions = File::config()->app_categories[$filters['AppCategory']];
1186
            $list = $list->filter('Name:PartialMatch', $extensions);
1187
        }
1188
1189
        // Sort folders first
1190
        $list = $list->sort(
1191
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
1192
        );
1193
1194
        // Pagination
1195
        if (isset($filters['page']) && isset($filters['limit'])) {
1196
            $page = $filters['page'];
1197
            $limit = $filters['limit'];
1198
            $offset = ($page - 1) * $limit;
1199
            $list = $list->limit($limit, $offset);
1200
        }
1201
1202
        // Access checks
1203
        $list = $list->filterByCallback(function(File $file) {
1204
            return $file->canView();
1205
        });
1206
1207
        return $list;
1208
    }
1209
1210
    /**
1211
     * Action handler for adding pages to a campaign
1212
     *
1213
     * @param array $data
1214
     * @param Form $form
1215
     * @return DBHTMLText|HTTPResponse
1216
     */
1217
    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...
1218
    {
1219
        $id = $data['ID'];
1220
        $record = $this->getList()->byID($id);
1221
1222
        $handler = AddToCampaignHandler::create($this, $record);
1223
        $results = $handler->addToCampaign($record, $data['Campaign']);
1224
        if (!isset($results)) {
1225
            return null;
1226
        }
1227
        $request = $this->getRequest();
1228
        if($request->getHeader('X-Formschema-Request')) {
1229
            $data = $this->getSchemaForForm($handler->Form($record));
1230
            $data['message'] = $results;
1231
1232
            $response = new HTTPResponse(Convert::raw2json($data));
1233
            $response->addHeader('Content-Type', 'application/json');
1234
            return $response;
1235
        }
1236
        return $results;
1237
    }
1238
1239
    /**
1240
     * Url handler for add to campaign form
1241
     *
1242
     * @param HTTPRequest $request
1243
     * @return Form
1244
     */
1245
    public function AddToCampaignForm($request)
1246
    {
1247
        // Get ID either from posted back value, or url parameter
1248
        $id = $request->param('ID') ?: $request->postVar('ID');
1249
        return $this->getAddToCampaignForm($id);
1250
    }
1251
1252
    /**
1253
     * @param int $id
1254
     * @return Form
1255
     */
1256
    public function getAddToCampaignForm($id)
1257
    {
1258
        // Get record-specific fields
1259
        $record = $this->getList()->byID($id);
1260
1261
        if (!$record) {
1262
            $this->httpError(404, _t(
1263
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1264
                'That {Type} couldn\'t be found',
1265
                '',
1266
                ['Type' => File::singleton()->i18n_singular_name()]
1267
            ));
1268
            return null;
1269
        }
1270 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...
1271
            $this->httpError(403, _t(
1272
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1273
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1274
                '',
1275
                ['ObjectTitle' => $record->i18n_singular_name()]
1276
            ));
1277
            return null;
1278
        }
1279
1280
        $handler = AddToCampaignHandler::create($this, $record);
1281
        $form = $handler->Form($record);
1282
1283 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...
1284
            $schemaId = Controller::join_links($this->Link('schema/AddToCampaignForm'), $id);
1285
            return $this->getSchemaResponse($form, $schemaId);
1286
        });
1287
1288
        return $form;
1289
    }
1290
1291
    /**
1292
     * @return Upload
1293
     */
1294
    protected function getUpload()
1295
    {
1296
        $upload = Upload::create();
1297
        $upload->getValidator()->setAllowedExtensions(
1298
            // filter out '' since this would be a regex problem on JS end
1299
            array_filter(File::config()->get('allowed_extensions'))
1300
        );
1301
1302
        return $upload;
1303
    }
1304
}
1305