Completed
Pull Request — master (#330)
by
unknown
01:57
created

AssetAdmin::saveOrPublish()   D

Complexity

Conditions 9
Paths 7

Size

Total Lines 44
Code Lines 23

Duplication

Lines 8
Ratio 18.18 %

Importance

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