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

AssetAdmin::apiCreateFolder()   C

Complexity

Conditions 12
Paths 97

Size

Total Lines 58
Code Lines 31

Duplication

Lines 3
Ratio 5.17 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 3
loc 58
rs 6.5331
cc 12
eloc 31
nc 97
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\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\Injector\Injector;
25
use SilverStripe\Forms\CheckboxField;
26
use SilverStripe\Forms\DateField;
27
use SilverStripe\Forms\DropdownField;
28
use SilverStripe\Forms\FieldGroup;
29
use SilverStripe\Forms\Form;
30
use SilverStripe\Forms\FormFactory;
31
use SilverStripe\Forms\HeaderField;
32
use SilverStripe\ORM\ArrayList;
33
use SilverStripe\ORM\DataList;
34
use SilverStripe\ORM\DataObject;
35
use SilverStripe\ORM\FieldType\DBHTMLText;
36
use SilverStripe\ORM\Search\SearchContext;
37
use SilverStripe\ORM\ValidationResult;
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
use Exception;
44
45
/**
46
 * AssetAdmin is the 'file store' section of the CMS.
47
 * It provides an interface for manipulating the File and Folder objects in the system.
48
 */
49
class AssetAdmin extends LeftAndMain implements PermissionProvider
50
{
51
    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...
52
53
    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...
54
55
    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...
56
57
    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...
58
59
    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...
60
        // Legacy redirect for SS3-style detail view
61
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
62
        // Pass all URLs to the index, for React to unpack
63
        'show/$FolderID/edit/$FileID' => 'index',
64
        // API access points with structured data
65
        'POST api/createFolder' => 'apiCreateFolder',
66
        'POST api/createFile' => 'apiCreateFile',
67
        'POST api/uploadFile' => 'apiUploadFile',
68
        'GET api/readFolder' => 'apiReadFolder',
69
        'PUT api/updateFolder' => 'apiUpdateFolder',
70
        'DELETE api/delete' => 'apiDelete',
71
        'GET api/search' => 'apiSearch',
72
        'GET api/history' => 'apiHistory'
73
    ];
74
75
    /**
76
     * Amount of results showing on a single page.
77
     *
78
     * @config
79
     * @var int
80
     */
81
    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...
82
83
    /**
84
     * @config
85
     * @see Upload->allowedMaxFileSize
86
     * @var int
87
     */
88
    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...
89
90
    /**
91
     * @config
92
     *
93
     * @var int
94
     */
95
    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...
96
97
    /**
98
     * @var array
99
     */
100
    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...
101
        'legacyRedirectForEditView',
102
        'apiCreateFolder',
103
        'apiCreateFile',
104
        'apiUploadFile',
105
        'apiReadFolder',
106
        'apiUpdateFolder',
107
        'apiHistory',
108
        'apiDelete',
109
        'apiSearch',
110
        'fileEditForm',
111
        'fileHistoryForm',
112
        'addToCampaignForm',
113
        'fileInsertForm',
114
        'schema',
115
        'fileSelectForm',
116
    );
117
118
    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...
119
120
    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...
121
122
    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...
123
124
    /**
125
     * Set up the controller
126
     */
127
    public function init()
128
    {
129
        parent::init();
130
131
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
132
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
133
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
134
135
        CMSBatchActionHandler::register('delete', DeleteAssets::class, Folder::class);
136
    }
137
138
    public function getClientConfig()
139
    {
140
        $baseLink = $this->Link();
141
        return array_merge(parent::getClientConfig(), [
142
            'reactRouter' => true,
143
            'createFileEndpoint' => [
144
                'url' => Controller::join_links($baseLink, 'api/createFile'),
145
                'method' => 'post',
146
                'payloadFormat' => 'urlencoded',
147
            ],
148
            'createFolderEndpoint' => [
149
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
150
                'method' => 'post',
151
                'payloadFormat' => 'urlencoded',
152
            ],
153
            'readFolderEndpoint' => [
154
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
155
                'method' => 'get',
156
                'responseFormat' => 'json',
157
            ],
158
            'searchEndpoint' => [
159
                'url' => Controller::join_links($baseLink, 'api/search'),
160
                'method' => 'get',
161
                'responseFormat' => 'json',
162
            ],
163
            'updateFolderEndpoint' => [
164
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
165
                'method' => 'put',
166
                'payloadFormat' => 'urlencoded',
167
            ],
168
            'deleteEndpoint' => [
169
                'url' => Controller::join_links($baseLink, 'api/delete'),
170
                'method' => 'delete',
171
                'payloadFormat' => 'urlencoded',
172
            ],
173
            'uploadFileEndpoint' => [
174
                'url' => Controller::join_links($baseLink, 'api/uploadFile'),
175
                'method' => 'post',
176
                'payloadFormat' => 'urlencoded',
177
            ],
178
            'historyEndpoint' => [
179
                'url' => Controller::join_links($baseLink, 'api/history'),
180
                'method' => 'get',
181
                'responseFormat' => 'json',
182
            ],
183
            'limit' => $this->config()->page_length,
184
            'form' => [
185
                'fileEditForm' => [
186
                    'schemaUrl' => $this->Link('schema/fileEditForm')
187
                ],
188
                'fileInsertForm' => [
189
                    'schemaUrl' => $this->Link('schema/fileInsertForm')
190
                ],
191
                'fileSelectForm' => [
192
                    'schemaUrl' => $this->Link('schema/fileSelectForm')
193
                ],
194
                'addToCampaignForm' => [
195
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
196
                ],
197
                'fileHistoryForm' => [
198
                    'schemaUrl' => $this->Link('schema/fileHistoryForm')
199
                ]
200
            ],
201
        ]);
202
    }
203
204
    /**
205
     * Fetches a collection of files by ParentID.
206
     *
207
     * @param HTTPRequest $request
208
     * @return HTTPResponse
209
     */
210
    public function apiReadFolder(HTTPRequest $request)
211
    {
212
        $params = $request->requestVars();
213
        $items = array();
214
        $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...
215
        $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...
216
217
        if (!isset($params['id']) && !strlen($params['id'])) {
218
            $this->httpError(400);
219
        }
220
221
        $folderID = (int)$params['id'];
222
        /** @var Folder $folder */
223
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
224
225
        if (!$folder) {
226
            $this->httpError(400);
227
        }
228
229
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
230
        $files = $this->getList()->filter('ParentID', $folderID);
231
232
        if ($files) {
233
            /** @var File $file */
234
            foreach ($files as $file) {
235
                if (!$file->canView()) {
236
                    continue;
237
                }
238
239
                $items[] = $this->getObjectFromData($file);
240
            }
241
        }
242
243
        // Build parents (for breadcrumbs)
244
        $parents = [];
245
        $next = $folder->Parent();
246
        while ($next && $next->exists()) {
247
            array_unshift($parents, [
248
                'id' => $next->ID,
249
                'title' => $next->getTitle(),
250
                'filename' => $next->getFilename(),
251
            ]);
252
            if ($next->ParentID) {
253
                $next = $next->Parent();
254
            } else {
255
                break;
256
            }
257
        }
258
259
        $column = 'title';
260
        $direction = 'asc';
261
        if (isset($params['sort'])) {
262
            list($column, $direction) = explode(',', $params['sort']);
263
        }
264
        $multiplier = ($direction === 'asc') ? 1 : -1;
265
266
        usort($items, function ($a, $b) use ($column, $multiplier) {
267
            if (!isset($a[$column]) || !isset($b[$column])) {
268
                return 0;
269
            }
270
            if ($a['type'] === 'folder' && $b['type'] !== 'folder') {
271
                return -1;
272
            }
273
            if ($b['type'] === 'folder' && $a['type'] !== 'folder') {
274
                return 1;
275
            }
276
            $numeric = (is_numeric($a[$column]) && is_numeric($b[$column]));
277
            $fieldA = ($numeric) ? floatval($a[$column]) : strtolower($a[$column]);
278
            $fieldB = ($numeric) ? floatval($b[$column]) : strtolower($b[$column]);
279
280
            if ($fieldA < $fieldB) {
281
                return $multiplier * -1;
282
            }
283
284
            if ($fieldA > $fieldB) {
285
                return $multiplier;
286
            }
287
288
            return 0;
289
        });
290
291
        $page = (isset($params['page'])) ? $params['page'] : 0;
292
        $limit = (isset($params['limit'])) ? $params['limit'] : $this->config()->page_length;
293
        $filteredItems = array_slice($items, $page * $limit, $limit);
294
295
        // Build response
296
        $response = new HTTPResponse();
297
        $response->addHeader('Content-Type', 'application/json');
298
        $response->setBody(json_encode([
299
            'files' => $filteredItems,
300
            'title' => $folder->getTitle(),
301
            'count' => count($items),
302
            'parents' => $parents,
303
            'parent' => $parents ? $parents[count($parents) - 1] : null,
304
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
305
            'folderID' => $folderID,
306
            'canEdit' => $folder->canEdit(),
307
            'canDelete' => $folder->canArchive(),
308
        ]));
309
310
        return $response;
311
    }
312
313
    /**
314
     * @param HTTPRequest $request
315
     *
316
     * @return HTTPResponse
317
     */
318
    public function apiSearch(HTTPRequest $request)
319
    {
320
        $params = $request->getVars();
321
        $list = $this->getList($params);
322
323
        $response = new HTTPResponse();
324
        $response->addHeader('Content-Type', 'application/json');
325
        $response->setBody(json_encode([
326
            // Serialisation
327
            "files" => array_map(function ($file) {
328
                return $this->getObjectFromData($file);
329
            }, $list->toArray()),
330
            "count" => $list->count(),
331
        ]));
332
333
        return $response;
334
    }
335
336
    /**
337
     * @param HTTPRequest $request
338
     *
339
     * @return HTTPResponse
340
     */
341
    public function apiDelete(HTTPRequest $request)
342
    {
343
        parse_str($request->getBody(), $vars);
344
345
        // CSRF check
346
        $token = SecurityToken::inst();
347 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...
348
            return new HTTPResponse(null, 400);
349
        }
350
351 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...
352
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
353
                ->addHeader('Content-Type', 'application/json');
354
        }
355
356
        $fileIds = $vars['ids'];
357
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
358
359 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...
360
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
361
                ->addHeader('Content-Type', 'application/json');
362
        }
363
364
        if (!min(array_map(function (File $file) {
365
            return $file->canArchive();
366
        }, $files))) {
367
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
368
                ->addHeader('Content-Type', 'application/json');
369
        }
370
371
        /** @var File $file */
372
        foreach ($files as $file) {
373
            $file->doArchive();
374
        }
375
376
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
377
            ->addHeader('Content-Type', 'application/json');
378
    }
379
380
    /**
381
     * Creates a single file based on a form-urlencoded upload.
382
     *
383
     * @param HTTPRequest $request
384
     * @return HTTPRequest|HTTPResponse
385
     */
386
    public function apiCreateFile(HTTPRequest $request)
387
    {
388
        $data = $request->postVars();
389
        $upload = $this->getUpload();
390
391
        // CSRF check
392
        $token = SecurityToken::inst();
393 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...
394
            return new HTTPResponse(null, 400);
395
        }
396
397
        // Check parent record
398
        /** @var Folder $parentRecord */
399
        $parentRecord = null;
400 View Code Duplication
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
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...
401
            $parentRecord = Folder::get()->byID($data['ParentID']);
402
        }
403
        $data['Parent'] = $parentRecord;
404
405
        $tmpFile = $request->postVar('Upload');
406 View Code Duplication
        if (!$upload->validate($tmpFile)) {
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...
407
            $result = ['message' => null];
408
            $errors = $upload->getErrors();
409
            if ($message = array_shift($errors)) {
410
                $result['message'] = [
411
                    'type' => 'error',
412
                    'value' => $message,
413
                ];
414
            }
415
            return (new HTTPResponse(json_encode($result), 400))
416
                ->addHeader('Content-Type', 'application/json');
417
        }
418
419
        // TODO Allow batch uploads
420
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
421
        /** @var File $file */
422
        $file = Injector::inst()->create($fileClass);
423
424
        // check canCreate permissions
425 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...
426
            $result = ['message' => [
427
                'type' => 'error',
428
                'value' => _t(
429
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
430
                    'You do not have permission to add files'
431
                )
432
            ]];
433
            return (new HTTPResponse(json_encode($result), 403))
434
                ->addHeader('Content-Type', 'application/json');
435
        }
436
437
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
438 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...
439
            $result = ['message' => [
440
                'type' => 'error',
441
                'value' => _t(
442
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
443
                    'Failed to load file'
444
                )
445
            ]];
446
            return (new HTTPResponse(json_encode($result), 400))
447
                ->addHeader('Content-Type', 'application/json');
448
        }
449
450
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
451
        $file->write();
452
453
        $result = [$this->getObjectFromData($file)];
454
455
        return (new HTTPResponse(json_encode($result)))
456
            ->addHeader('Content-Type', 'application/json');
457
    }
458
459
    /**
460
     * Upload a new asset for a pre-existing record. Returns the asset tuple.
461
     *
462
     * @param HTTPRequest $request
463
     * @return HTTPRequest|HTTPResponse
464
     */
465
    public function apiUploadFile(HTTPRequest $request)
466
    {
467
        $data = $request->postVars();
468
        $upload = $this->getUpload();
469
470
        // CSRF check
471
        $token = SecurityToken::inst();
472 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...
473
            return new HTTPResponse(null, 400);
474
        }
475
476
        // Check parent record
477
        /** @var Folder $parentRecord */
478
        $parentRecord = null;
479 View Code Duplication
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
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...
480
            $parentRecord = Folder::get()->byID($data['ParentID']);
481
        }
482
483
        $tmpFile = $data['Upload'];
484 View Code Duplication
        if (!$upload->validate($tmpFile)) {
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...
485
            $result = ['message' => null];
486
            $errors = $upload->getErrors();
487
            if ($message = array_shift($errors)) {
488
                $result['message'] = [
489
                    'type' => 'error',
490
                    'value' => $message,
491
                ];
492
            }
493
            return (new HTTPResponse(json_encode($result), 400))
494
                ->addHeader('Content-Type', 'application/json');
495
        }
496
497
        $folder = $parentRecord ? $parentRecord->getFilename() : '/';
498
499
        try {
500
            $tuple = $upload->load($tmpFile, $folder);
501
        } catch (Exception $e) {
502
            $result = [
503
                'message' => [
504
                    'type' => 'error',
505
                    'value' => $e->getMessage(),
506
                ]
507
            ];
508
            return (new HTTPResponse(json_encode($result), 400))
509
                ->addHeader('Content-Type', 'application/json');
510
        }
511
512
        if ($upload->isError()) {
513
            $result['message'] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
514
                'type' => 'error',
515
                'value' => implode(' ' . PHP_EOL, $upload->getErrors()),
516
            ];
517
            return (new HTTPResponse(json_encode($result), 400))
518
                ->addHeader('Content-Type', 'application/json');
519
        }
520
521
        $tuple['Name'] = basename($tuple['Filename']);
522
        return (new HTTPResponse(json_encode($tuple)))
523
            ->addHeader('Content-Type', 'application/json');
524
    }
525
526
    /**
527
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
528
     *
529
     * @param HTTPRequest $request
530
     * @return HTTPResponse
531
     */
532
    public function apiHistory(HTTPRequest $request)
533
    {
534
        // CSRF check not required as the GET request has no side effects.
535
        $fileId = $request->getVar('fileId');
536
537
        if (!$fileId || !is_numeric($fileId)) {
538
            return new HTTPResponse(null, 400);
539
        }
540
541
        $class = File::class;
542
        $file = DataObject::get($class)->byID($fileId);
543
544
        if (!$file) {
545
            return new HTTPResponse(null, 404);
546
        }
547
548
        if (!$file->canView()) {
549
            return new HTTPResponse(null, 403);
550
        }
551
552
        $versions = Versioned::get_all_versions($class, $fileId)
553
            ->limit($this->config()->max_history_entries)
554
            ->sort('Version', 'DESC');
555
556
        $output = array();
557
        $next = array();
558
        $prev = null;
559
560
        // swap the order so we can get the version number to compare against.
561
        // i.e version 3 needs to know version 2 is the previous version
562
        $copy = $versions->map('Version', 'Version')->toArray();
563
        foreach (array_reverse($copy) as $k => $v) {
564
            if ($prev) {
565
                $next[$v] = $prev;
566
            }
567
568
            $prev = $v;
569
        }
570
571
        $_cachedMembers = array();
572
573
        /** @var File $version */
574
        foreach ($versions as $version) {
575
            $author = null;
576
577
            if ($version->AuthorID) {
578
                if (!isset($_cachedMembers[$version->AuthorID])) {
579
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
580
                        ->byID($version->AuthorID);
581
                }
582
583
                $author = $_cachedMembers[$version->AuthorID];
584
            }
585
586
            if ($version->canView()) {
587
                if (isset($next[$version->Version])) {
588
                    $summary = $version->humanizedChanges(
589
                        $version->Version,
590
                        $next[$version->Version]
591
                    );
592
593
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
594
                    // generic message
595
                    if (!$summary) {
596
                        $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.SAVEDFILE', "Saved file");
597
                    }
598
                } else {
599
                    $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UPLOADEDFILE', "Uploaded file");
600
                }
601
602
                $output[] = array(
603
                    'versionid' => $version->Version,
604
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
605
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
606
                    'status' => ($version->WasPublished) ? _t('File.PUBLISHED', 'Published') : '',
607
                    'author' => ($author)
608
                        ? $author->Name
609
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNKNOWN', "Unknown"),
610
                    'summary' => ($summary)
611
                        ? $summary
612
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NOSUMMARY', "No summary available")
613
                );
614
            }
615
        }
616
617
        return
618
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
619
    }
620
621
622
    /**
623
     * Creates a single folder, within an optional parent folder.
624
     *
625
     * @param HTTPRequest $request
626
     * @return HTTPRequest|HTTPResponse
627
     */
628
    public function apiCreateFolder(HTTPRequest $request)
629
    {
630
        $data = $request->postVars();
631
632
        $class = 'SilverStripe\\Assets\\Folder';
633
634
        // CSRF check
635
        $token = SecurityToken::inst();
636 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...
637
            return new HTTPResponse(null, 400);
638
        }
639
640
        // check addchildren permissions
641
        /** @var Folder $parentRecord */
642
        $parentRecord = null;
643
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
644
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
645
        }
646
        $data['Parent'] = $parentRecord;
647
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
648
649
        // Build filename
650
        $baseFilename = isset($data['Name'])
651
            ? basename($data['Name'])
652
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
653
654
        if ($parentRecord && $parentRecord->ID) {
655
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
656
        }
657
658
        // Ensure name is unique
659
        $nameGenerator = $this->getNameGenerator($baseFilename);
660
        $filename = null;
661
        foreach ($nameGenerator as $filename) {
662
            if (! File::find($filename)) {
663
                break;
664
            }
665
        }
666
        $data['Name'] = basename($filename);
667
668
        // Create record
669
        /** @var Folder $record */
670
        $record = Injector::inst()->create($class);
671
672
        // check create permissions
673
        if (!$record->canCreate(null, $data)) {
674
            return (new HTTPResponse(null, 403))
675
                ->addHeader('Content-Type', 'application/json');
676
        }
677
678
        $record->ParentID = $data['ParentID'];
679
        $record->Name = $record->Title = basename($data['Name']);
680
        $record->write();
681
682
        $result = $this->getObjectFromData($record);
683
684
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
685
    }
686
687
    /**
688
     * Redirects 3.x style detail links to new 4.x style routing.
689
     *
690
     * @param HTTPRequest $request
691
     */
692
    public function legacyRedirectForEditView($request)
693
    {
694
        $fileID = $request->param('FileID');
695
        /** @var File $file */
696
        $file = File::get()->byID($fileID);
697
        $link = $this->getFileEditLink($file) ?: $this->Link();
698
        $this->redirect($link);
699
    }
700
701
    /**
702
     * Given a file return the CMS link to edit it
703
     *
704
     * @param File $file
705
     * @return string
706
     */
707
    public function getFileEditLink($file)
708
    {
709
        if (!$file || !$file->isInDB()) {
710
            return null;
711
        }
712
713
        return Controller::join_links(
714
            $this->Link('show'),
715
            $file->ParentID,
716
            'edit',
717
            $file->ID
718
        );
719
    }
720
721
    /**
722
     * Get the search context from {@link File}, used to create the search form
723
     * as well as power the /search API endpoint.
724
     *
725
     * @return SearchContext
726
     */
727
    public function getSearchContext()
728
    {
729
        $context = File::singleton()->getDefaultSearchContext();
730
731
        // Customize fields
732
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
733
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
734
        ->setConfig('showcalendar', true);
735
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
736
        ->setConfig('showcalendar', true);
737
        $dateGroup = FieldGroup::create(
738
            $dateHeader,
739
            $dateFrom,
740
            $dateTo
741
        );
742
        $context->addField($dateGroup);
743
        /** @skipUpgrade */
744
        $appCategories = array(
745
            'archive' => _t(
746
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
747
                'Archive'
748
            ),
749
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
750
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
751
            'flash' => _t(
752
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
753
                'Flash',
754
                'The fileformat'
755
            ),
756
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
757
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
758
        );
759
        $context->addField(
760
            $typeDropdown = new DropdownField(
761
                'AppCategory',
762
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
763
                $appCategories
764
            )
765
        );
766
767
        $typeDropdown->setEmptyString(' ');
768
769
        $currentfolderLabel = _t(
770
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
771
            'Limit to current folder?'
772
        );
773
        $context->addField(
774
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
775
        );
776
        $context->getFields()->removeByName('Title');
777
778
        return $context;
779
    }
780
781
    /**
782
     * Get an asset renamer for the given filename.
783
     *
784
     * @param  string             $filename Path name
785
     * @return AssetNameGenerator
786
     */
787
    protected function getNameGenerator($filename)
788
    {
789
        return Injector::inst()
790
            ->createWithArgs('AssetNameGenerator', array($filename));
791
    }
792
793
    /**
794
     * @todo Implement on client
795
     *
796
     * @param bool $unlinked
797
     * @return ArrayList
798
     */
799
    public function breadcrumbs($unlinked = false)
800
    {
801
        return null;
802
    }
803
804
805
    /**
806
     * Don't include class namespace in auto-generated CSS class
807
     */
808
    public function baseCSSClasses()
809
    {
810
        return 'AssetAdmin LeftAndMain';
811
    }
812
813
    public function providePermissions()
814
    {
815
        return array(
816
            "CMS_ACCESS_AssetAdmin" => array(
817
                '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...
818
                    'title' => static::menu_title()
819
                )),
820
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
821
            )
822
        );
823
    }
824
825
    /**
826
     * Build a form scaffolder for this model
827
     *
828
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
829
     *
830
     * @param File $file
831
     * @return FormFactory
832
     */
833
    public function getFormFactory(File $file)
834
    {
835
        // Get service name based on file class
836
        $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...
837
        if ($file instanceof Folder) {
838
            $name = FolderFormFactory::class;
839
        } elseif ($file instanceof Image) {
840
            $name = ImageFormFactory::class;
841
        } else {
842
            $name = FileFormFactory::class;
843
        }
844
        return Injector::inst()->get($name);
845
    }
846
847
    /**
848
     * The form is used to generate a form schema,
849
     * as well as an intermediary object to process data through API endpoints.
850
     * Since it's used directly on API endpoints, it does not have any form actions.
851
     * It handles both {@link File} and {@link Folder} records.
852
     *
853
     * @param int $id
854
     * @return Form
855
     */
856
    public function getFileEditForm($id)
857
    {
858
        return $this->getAbstractFileForm($id, 'fileEditForm');
859
    }
860
861
    /**
862
     * Get file edit form
863
     *
864
     * @return Form
865
     */
866 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...
867
    {
868
        // Get ID either from posted back value, or url parameter
869
        $request = $this->getRequest();
870
        $id = $request->param('ID') ?: $request->postVar('ID');
871
        return $this->getFileEditForm($id);
872
    }
873
874
    /**
875
     * The form is used to generate a form schema,
876
     * as well as an intermediary object to process data through API endpoints.
877
     * Since it's used directly on API endpoints, it does not have any form actions.
878
     * It handles both {@link File} and {@link Folder} records.
879
     *
880
     * @param int $id
881
     * @return Form
882
     */
883
    public function getFileInsertForm($id)
884
    {
885
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT ]);
886
    }
887
888
    /**
889
     * Get file insert form
890
     *
891
     * @return Form
892
     */
893 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...
894
    {
895
        // Get ID either from posted back value, or url parameter
896
        $request = $this->getRequest();
897
        $id = $request->param('ID') ?: $request->postVar('ID');
898
        return $this->getFileInsertForm($id);
899
    }
900
901
    /**
902
     * Abstract method for generating a form for a file
903
     *
904
     * @param int $id Record ID
905
     * @param string $name Form name
906
     * @param array $context Form context
907
     * @return Form
908
     */
909
    protected function getAbstractFileForm($id, $name, $context = [])
910
    {
911
        /** @var File $file */
912
        $file = $this->getList()->byID($id);
913
914 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...
915
            $this->httpError(403, _t(
916
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
917
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
918
                '',
919
                ['ObjectTitle' => $file->i18n_singular_name()]
920
            ));
921
            return null;
922
        }
923
924
        // Pass to form factory
925
        $augmentedContext = array_merge($context, ['Record' => $file]);
926
        $scaffolder = $this->getFormFactory($file);
927
        $form = $scaffolder->getForm($this, $name, $augmentedContext);
928
929
        // Configure form to respond to validation errors with form schema
930
        // if requested via react.
931
        $form->setValidationResponseCallback(function (ValidationResult $error) use ($form, $id, $name) {
932
            $schemaId = Controller::join_links($this->Link('schema'), $name, $id);
933
            return $this->getSchemaResponse($schemaId, $form, $error);
934
        });
935
936
        return $form;
937
    }
938
939
    /**
940
     * Get form for selecting a file
941
     *
942
     * @return Form
943
     */
944
    public function fileSelectForm()
945
    {
946
        // Get ID either from posted back value, or url parameter
947
        $request = $this->getRequest();
948
        $id = $request->param('ID') ?: $request->postVar('ID');
949
        return $this->getFileSelectForm($id);
950
    }
951
952
    /**
953
     * Get form for selecting a file
954
     *
955
     * @param int $id ID of the record being selected
956
     * @return Form
957
     */
958
    public function getFileSelectForm($id)
959
    {
960
        return $this->getAbstractFileForm($id, 'fileSelectForm', [ 'Type' => AssetFormFactory::TYPE_SELECT ]);
961
    }
962
963
    /**
964
     * @param array $context
965
     * @return Form
966
     * @throws InvalidArgumentException
967
     */
968
    public function getFileHistoryForm($context)
969
    {
970
        // Check context
971
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
972
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
973
        }
974
        $id = $context['RecordID'];
975
        $versionId = $context['RecordVersion'];
976
        if (!$id || !$versionId) {
977
            return $this->httpError(404);
978
        }
979
980
        /** @var File $file */
981
        $file = Versioned::get_version(File::class, $id, $versionId);
982
        if (!$file) {
983
            return $this->httpError(404);
984
        }
985
986 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...
987
            $this->httpError(403, _t(
988
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
989
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
990
                '',
991
                ['ObjectTitle' => $file->i18n_singular_name()]
992
            ));
993
            return null;
994
        }
995
996
        $effectiveContext = array_merge($context, ['Record' => $file]);
997
        /** @var FormFactory $scaffolder */
998
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
999
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
1000
1001
        // Configure form to respond to validation errors with form schema
1002
        // if requested via react.
1003 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id, $versionId) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1004
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
1005
            return $this->getSchemaResponse($schemaId, $form, $errors);
1006
        });
1007
1008
        return $form;
1009
    }
1010
1011
    /**
1012
     * Gets a JSON schema representing the current edit form.
1013
     *
1014
     * WARNING: Experimental API.
1015
     *
1016
     * @param HTTPRequest $request
1017
     * @return HTTPResponse
1018
     */
1019
    public function schema($request)
1020
    {
1021
        $formName = $request->param('FormName');
1022
        if ($formName !== 'fileHistoryForm') {
1023
            return parent::schema($request);
1024
        }
1025
1026
        // Get schema for history form
1027
        // @todo Eventually all form scaffolding will be based on context rather than record ID
1028
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
1029
        $itemID = $request->param('ItemID');
1030
        $version = $request->param('OtherItemID');
1031
        $form = $this->getFileHistoryForm([
1032
            'RecordID' => $itemID,
1033
            'RecordVersion' => $version,
1034
        ]);
1035
1036
        // Respond with this schema
1037
        $response = $this->getResponse();
1038
        $response->addHeader('Content-Type', 'application/json');
1039
        $schemaID = $this->getRequest()->getURL();
1040
        return $this->getSchemaResponse($schemaID, $form);
1041
    }
1042
1043
    /**
1044
     * Get file history form
1045
     *
1046
     * @return Form
1047
     */
1048
    public function fileHistoryForm()
1049
    {
1050
        $request = $this->getRequest();
1051
        $id = $request->param('ID') ?: $request->postVar('ID');
1052
        $version = $request->param('OtherID') ?: $request->postVar('Version');
1053
        $form = $this->getFileHistoryForm([
1054
            'RecordID' => $id,
1055
            'RecordVersion' => $version,
1056
        ]);
1057
        return $form;
1058
    }
1059
1060
    /**
1061
     * @param array $data
1062
     * @param Form $form
1063
     * @return HTTPResponse
1064
     */
1065
    public function save($data, $form)
1066
    {
1067
        return $this->saveOrPublish($data, $form, false);
1068
    }
1069
1070
    /**
1071
     * @param array $data
1072
     * @param Form $form
1073
     * @return HTTPResponse
1074
     */
1075
    public function publish($data, $form)
1076
    {
1077
        return $this->saveOrPublish($data, $form, true);
1078
    }
1079
1080
    /**
1081
     * Update thisrecord
1082
     *
1083
     * @param array $data
1084
     * @param Form $form
1085
     * @param bool $doPublish
1086
     * @return HTTPResponse
1087
     */
1088
    protected function saveOrPublish($data, $form, $doPublish = false)
1089
    {
1090 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...
1091
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1092
                ->addHeader('Content-Type', 'application/json');
1093
        }
1094
1095
        $id = (int) $data['ID'];
1096
        /** @var File $record */
1097
        $record = DataObject::get_by_id(File::class, $id);
1098
1099 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...
1100
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1101
                ->addHeader('Content-Type', 'application/json');
1102
        }
1103
1104
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
1105
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1106
                ->addHeader('Content-Type', 'application/json');
1107
        }
1108
1109
        // check File extension
1110
        $extension = File::get_file_extension($data['FileFilename']);
1111
        $newClass = File::get_class_for_file_extension($extension);
1112
        // if the class has changed, cast it to the proper class
1113
        if ($record->getClassName() !== $newClass) {
1114
            $record = $record->newClassInstance($newClass);
1115
1116
            // update the allowed category for the new file extension
1117
            $category = File::get_app_category($extension);
1118
            $record->File->setAllowedCategories($category);
1119
        }
1120
1121
        $form->saveInto($record);
1122
        $record->write();
1123
1124
        // Publish this record and owned objects
1125
        if ($doPublish) {
1126
            $record->publishRecursive();
1127
        }
1128
1129
        // Note: Force return of schema / state in success result
1130
        return $this->getRecordUpdatedResponse($record, $form);
0 ignored issues
show
Compatibility introduced by
$record of type object<SilverStripe\ORM\DataObject> is not a sub-type of object<SilverStripe\Assets\File>. It seems like you assume a child class of the class SilverStripe\ORM\DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1131
    }
1132
1133
    public function unpublish($data, $form)
1134
    {
1135 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...
1136
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1137
                ->addHeader('Content-Type', 'application/json');
1138
        }
1139
1140
        $id = (int) $data['ID'];
1141
        /** @var File $record */
1142
        $record = DataObject::get_by_id(File::class, $id);
1143
1144
        if (!$record) {
1145
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1146
                ->addHeader('Content-Type', 'application/json');
1147
        }
1148
1149
        if (!$record->canUnpublish()) {
1150
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1151
                ->addHeader('Content-Type', 'application/json');
1152
        }
1153
1154
        $record->doUnpublish();
1155
        return $this->getRecordUpdatedResponse($record, $form);
1156
    }
1157
1158
    /**
1159
     * @param File $file
1160
     *
1161
     * @return array
1162
     */
1163
    public function getObjectFromData(File $file)
1164
    {
1165
        $object = array(
1166
            'id' => $file->ID,
1167
            'created' => $file->Created,
1168
            'lastUpdated' => $file->LastEdited,
1169
            'owner' => null,
1170
            'parent' => null,
1171
            'title' => $file->Title,
1172
            'exists' => $file->exists(), // Broken file check
1173
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1174
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
1175
            'name' => $file->Name,
1176
            'filename' => $file->Filename,
1177
            '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...
1178
            'size' => $file->AbsoluteSize,
1179
            'url' => $file->AbsoluteURL,
1180
            'published' => $file->isPublished(),
1181
            'modified' => $file->isModifiedOnDraft(),
1182
            'draft' => $file->isOnDraftOnly(),
1183
            'canEdit' => $file->canEdit(),
1184
            'canDelete' => $file->canArchive(),
1185
        );
1186
1187
        /** @var Member $owner */
1188
        $owner = $file->Owner();
1189
1190
        if ($owner) {
1191
            $object['owner'] = array(
1192
                'id' => $owner->ID,
1193
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1194
            );
1195
        }
1196
1197
        /** @var Folder $parent */
1198
        $parent = $file->Parent();
1199
1200
        if ($parent) {
1201
            $object['parent'] = array(
1202
                'id' => $parent->ID,
1203
                'title' => $parent->Title,
1204
                'filename' => $parent->Filename,
1205
            );
1206
        }
1207
1208
        /** @var File $file */
1209
        if ($file->getIsImage()) {
1210
            // Small thumbnail
1211
            $smallWidth = UploadField::config()->get('thumbnail_width');
1212
            $smallHeight = UploadField::config()->get('thumbnail_height');
1213
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1214
            if ($smallThumbnail && $smallThumbnail->exists()) {
1215
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1216
            }
1217
1218
            // Large thumbnail
1219
            $width = $this->config()->get('thumbnail_width');
1220
            $height = $this->config()->get('thumbnail_height');
1221
            $thumbnail = $file->FitMax($width, $height);
1222
            if ($thumbnail && $thumbnail->exists()) {
1223
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1224
            }
1225
            $object['dimensions']['width'] = $file->Width;
1226
            $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...
1227
        } else {
1228
            $object['thumbnail'] = $file->PreviewLink();
1229
        }
1230
1231
        return $object;
1232
    }
1233
1234
    /**
1235
     * Returns the files and subfolders contained in the currently selected folder,
1236
     * defaulting to the root node. Doubles as search results, if any search parameters
1237
     * are set through {@link SearchForm()}.
1238
     *
1239
     * @param array $params Unsanitised request parameters
1240
     * @return DataList
1241
     */
1242
    protected function getList($params = array())
1243
    {
1244
        $context = $this->getSearchContext();
1245
1246
        // Overwrite name filter to search both Name and Title attributes
1247
        $context->removeFilterByName('Name');
1248
1249
        // Lazy loaded list. Allows adding new filters through SearchContext.
1250
        /** @var DataList $list */
1251
        $list = $context->getResults($params);
1252
1253
        // Re-add previously removed "Name" filter as combined filter
1254
        // TODO Replace with composite SearchFilter once that API exists
1255
        if (!empty($params['Name'])) {
1256
            $list = $list->filterAny(array(
1257
                'Name:PartialMatch' => $params['Name'],
1258
                'Title:PartialMatch' => $params['Name']
1259
            ));
1260
        }
1261
1262
        // Optionally limit search to a folder (non-recursive)
1263
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
1264
            $list = $list->filter('ParentID', $params['ParentID']);
1265
        }
1266
1267
        // Date filtering
1268 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...
1269
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
1270
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
1271
        }
1272 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...
1273
            $toDate = new DateField(null, null, $params['CreatedTo']);
1274
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
1275
        }
1276
1277
        // Categories
1278
        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...
1279
            $extensions = File::config()->app_categories[$filters['AppCategory']];
1280
            $list = $list->filter('Name:PartialMatch', $extensions);
1281
        }
1282
1283
        // Sort folders first
1284
        $list = $list->sort(
1285
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
1286
        );
1287
1288
        // Pagination
1289
        if (isset($filters['page']) && isset($filters['limit'])) {
1290
            $page = $filters['page'];
1291
            $limit = $filters['limit'];
1292
            $offset = ($page - 1) * $limit;
1293
            $list = $list->limit($limit, $offset);
1294
        }
1295
1296
        // Access checks
1297
        $list = $list->filterByCallback(function (File $file) {
1298
            return $file->canView();
1299
        });
1300
1301
        return $list;
1302
    }
1303
1304
    /**
1305
     * Action handler for adding pages to a campaign
1306
     *
1307
     * @param array $data
1308
     * @param Form $form
1309
     * @return DBHTMLText|HTTPResponse
1310
     */
1311
    public function addtocampaign($data, $form)
1312
    {
1313
        $id = $data['ID'];
1314
        $record = $this->getList()->byID($id);
1315
1316
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1317
        $results = $handler->addToCampaign($record, $data['Campaign']);
1318
        if (!isset($results)) {
1319
            return null;
1320
        }
1321
1322
        // Send extra "message" data with schema response
1323
        $extraData = ['message' => $results];
1324
        $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1325
        return $this->getSchemaResponse($schemaId, $form, null, $extraData);
1326
    }
1327
1328
    /**
1329
     * Url handler for add to campaign form
1330
     *
1331
     * @param HTTPRequest $request
1332
     * @return Form
1333
     */
1334
    public function addToCampaignForm($request)
1335
    {
1336
        // Get ID either from posted back value, or url parameter
1337
        $id = $request->param('ID') ?: $request->postVar('ID');
1338
        return $this->getAddToCampaignForm($id);
1339
    }
1340
1341
    /**
1342
     * @param int $id
1343
     * @return Form
1344
     */
1345
    public function getAddToCampaignForm($id)
1346
    {
1347
        // Get record-specific fields
1348
        $record = $this->getList()->byID($id);
1349
1350
        if (!$record) {
1351
            $this->httpError(404, _t(
1352
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1353
                'That {Type} couldn\'t be found',
1354
                '',
1355
                ['Type' => File::singleton()->i18n_singular_name()]
1356
            ));
1357
            return null;
1358
        }
1359 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...
1360
            $this->httpError(403, _t(
1361
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1362
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1363
                '',
1364
                ['ObjectTitle' => $record->i18n_singular_name()]
1365
            ));
1366
            return null;
1367
        }
1368
1369
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1370
        $form = $handler->Form($record);
1371
1372 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1373
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1374
            return $this->getSchemaResponse($schemaId, $form, $errors);
1375
        });
1376
1377
        return $form;
1378
    }
1379
1380
    /**
1381
     * @return Upload
1382
     */
1383
    protected function getUpload()
1384
    {
1385
        $upload = Upload::create();
1386
        $upload->getValidator()->setAllowedExtensions(
1387
            // filter out '' since this would be a regex problem on JS end
1388
            array_filter(File::config()->get('allowed_extensions'))
1389
        );
1390
1391
        return $upload;
1392
    }
1393
1394
    /**
1395
     * Get response for successfully updated record
1396
     *
1397
     * @param File $record
1398
     * @param Form $form
1399
     * @return HTTPResponse
1400
     */
1401
    protected function getRecordUpdatedResponse($record, $form)
1402
    {
1403
        // Return the record data in the same response as the schema to save a postback
1404
        $schemaData = ['record' => $this->getObjectFromData($record)];
1405
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1406
        return $this->getSchemaResponse($schemaId, $form, null, $schemaData);
1407
    }
1408
}
1409