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

AssetAdmin::getFileSearchform()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
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\DateField;
27
use SilverStripe\Forms\Form;
28
use SilverStripe\Forms\FormFactory;
29
use SilverStripe\ORM\ArrayList;
30
use SilverStripe\ORM\DataList;
31
use SilverStripe\ORM\DataObject;
32
use SilverStripe\ORM\FieldType\DBHTMLText;
33
use SilverStripe\ORM\ValidationResult;
34
use SilverStripe\Security\Member;
35
use SilverStripe\Security\PermissionProvider;
36
use SilverStripe\Security\SecurityToken;
37
use SilverStripe\View\Requirements;
38
use SilverStripe\ORM\Versioning\Versioned;
39
use Exception;
40
41
/**
42
 * AssetAdmin is the 'file store' section of the CMS.
43
 * It provides an interface for manipulating the File and Folder objects in the system.
44
 */
45
class AssetAdmin extends LeftAndMain implements PermissionProvider
46
{
47
    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...
48
49
    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...
50
51
    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...
52
53
    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...
54
55
    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...
56
        // Legacy redirect for SS3-style detail view
57
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
58
        // Pass all URLs to the index, for React to unpack
59
        'show/$FolderID/edit/$FileID' => 'index',
60
        // API access points with structured data
61
        'POST api/createFolder' => 'apiCreateFolder',
62
        'POST api/createFile' => 'apiCreateFile',
63
        'POST api/uploadFile' => 'apiUploadFile',
64
        'GET api/readFolder' => 'apiReadFolder',
65
        'PUT api/updateFolder' => 'apiUpdateFolder',
66
        'DELETE api/delete' => 'apiDelete',
67
        'GET api/history' => 'apiHistory'
68
    ];
69
70
    /**
71
     * Amount of results showing on a single page.
72
     *
73
     * @config
74
     * @var int
75
     */
76
    private static $page_length = 50;
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...
77
78
    /**
79
     * @config
80
     * @see Upload->allowedMaxFileSize
81
     * @var int
82
     */
83
    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...
84
85
    /**
86
     * @config
87
     *
88
     * @var int
89
     */
90
    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...
91
92
    /**
93
     * @var array
94
     */
95
    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...
96
        'legacyRedirectForEditView',
97
        'apiCreateFolder',
98
        'apiCreateFile',
99
        'apiUploadFile',
100
        'apiReadFolder',
101
        'apiUpdateFolder',
102
        'apiHistory',
103
        'apiDelete',
104
        'fileEditForm',
105
        'fileHistoryForm',
106
        'addToCampaignForm',
107
        'fileInsertForm',
108
        'schema',
109
        'fileSelectForm',
110
        'fileSearchForm',
111
    );
112
113
    private static $required_permission_codes = 'CMS_ACCESS_AssetAdmin';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $required_permission_codes is not used and could be removed.

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

Loading history...
114
115
    private static $thumbnail_width = 400;
0 ignored issues
show
Unused Code introduced by
The property $thumbnail_width is not used and could be removed.

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

Loading history...
116
117
    private static $thumbnail_height = 300;
0 ignored issues
show
Unused Code introduced by
The property $thumbnail_height is not used and could be removed.

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

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