Completed
Pull Request — master (#337)
by
unknown
01:48
created

AssetAdmin::getSortObjectFromData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

Loading history...
55
56
    private static $tree_class = 'SilverStripe\\Assets\\Folder';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $tree_class is not used and could be removed.

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

Loading history...
57
58
    private static $url_handlers = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $url_handlers is not used and could be removed.

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

Loading history...
59
        // Legacy redirect for SS3-style detail view
60
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
61
        // Pass all URLs to the index, for React to unpack
62
        'show/$FolderID/edit/$FileID' => 'index',
63
        // API access points with structured data
64
        'POST api/createFolder' => 'apiCreateFolder',
65
        'POST api/createFile' => 'apiCreateFile',
66
        'GET api/readFolder' => 'apiReadFolder',
67
        'PUT api/updateFolder' => 'apiUpdateFolder',
68
        'DELETE api/delete' => 'apiDelete',
69
        'GET api/search' => 'apiSearch',
70
        'GET api/history' => 'apiHistory'
71
    ];
72
73
    /**
74
     * Amount of results showing on a single page.
75
     *
76
     * @config
77
     * @var int
78
     */
79
    private static $page_length = 15;
0 ignored issues
show
Unused Code introduced by
The property $page_length is not used and could be removed.

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

Loading history...
80
81
    /**
82
     * @config
83
     * @see Upload->allowedMaxFileSize
84
     * @var int
85
     */
86
    private static $allowed_max_file_size;
0 ignored issues
show
Unused Code introduced by
The property $allowed_max_file_size is not used and could be removed.

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

Loading history...
87
88
    /**
89
     * @config
90
     *
91
     * @var int
92
     */
93
    private static $max_history_entries = 100;
0 ignored issues
show
Unused Code introduced by
The property $max_history_entries is not used and could be removed.

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

Loading history...
94
95
    /**
96
     * @var array
97
     */
98
    private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

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

Loading history...
99
        'legacyRedirectForEditView',
100
        'apiCreateFolder',
101
        'apiCreateFile',
102
        'apiReadFolder',
103
        'apiUpdateFolder',
104
        'apiHistory',
105
        'apiDelete',
106
        'apiSearch',
107
        'fileEditForm',
108
        'fileHistoryForm',
109
        'addToCampaignForm',
110
        'fileInsertForm',
111
        'schema',
112
        'fileSelectForm',
113
    );
114
115
    private static $required_permission_codes = 'CMS_ACCESS_AssetAdmin';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
Unused Code introduced by
The property $required_permission_codes is not used and could be removed.

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

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

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

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

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

Loading history...
120
121
    /**
122
     * Set up the controller
123
     */
124
    public function init()
125
    {
126
        parent::init();
127
128
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
129
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
130
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
131
132
        CMSBatchActionHandler::register('delete', DeleteAssets::class, Folder::class);
133
    }
134
135
    public function getClientConfig()
136
    {
137
        $baseLink = $this->Link();
138
        return array_merge(parent::getClientConfig(), [
139
            'reactRouter' => true,
140
            'createFileEndpoint' => [
141
                'url' => Controller::join_links($baseLink, 'api/createFile'),
142
                'method' => 'post',
143
                'payloadFormat' => 'urlencoded',
144
            ],
145
            'createFolderEndpoint' => [
146
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
147
                'method' => 'post',
148
                'payloadFormat' => 'urlencoded',
149
            ],
150
            'readFolderEndpoint' => [
151
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
152
                'method' => 'get',
153
                'responseFormat' => 'json',
154
            ],
155
            'searchEndpoint' => [
156
                'url' => Controller::join_links($baseLink, 'api/search'),
157
                'method' => 'get',
158
                'responseFormat' => 'json',
159
            ],
160
            'updateFolderEndpoint' => [
161
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
162
                'method' => 'put',
163
                'payloadFormat' => 'urlencoded',
164
            ],
165
            'deleteEndpoint' => [
166
                'url' => Controller::join_links($baseLink, 'api/delete'),
167
                'method' => 'delete',
168
                'payloadFormat' => 'urlencoded',
169
            ],
170
            'historyEndpoint' => [
171
                'url' => Controller::join_links($baseLink, 'api/history'),
172
                'method' => 'get',
173
                'responseFormat' => 'json'
174
            ],
175
            'limit' => $this->config()->page_length,
176
            'form' => [
177
                'fileEditForm' => [
178
                    'schemaUrl' => $this->Link('schema/fileEditForm')
179
                ],
180
                'fileInsertForm' => [
181
                    'schemaUrl' => $this->Link('schema/fileInsertForm')
182
                ],
183
                'fileSelectForm' => [
184
                    'schemaUrl' => $this->Link('schema/fileSelectForm')
185
                ],
186
                'addToCampaignForm' => [
187
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
188
                ],
189
                'fileHistoryForm' => [
190
                    'schemaUrl' => $this->Link('schema/fileHistoryForm')
191
                ]
192
            ],
193
        ]);
194
    }
195
196
    /**
197
     * Fetches a collection of files by ParentID.
198
     *
199
     * @param HTTPRequest $request
200
     * @return HTTPResponse
201
     */
202
    public function apiReadFolder(HTTPRequest $request)
203
    {
204
        $params = $request->requestVars();
205
        $items = array();
206
        $parentId = null;
0 ignored issues
show
Unused Code introduced by
$parentId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
207
        $folderID = null;
0 ignored issues
show
Unused Code introduced by
$folderID is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
208
209
        if (!isset($params['id']) && !strlen($params['id'])) {
210
            $this->httpError(400);
211
        }
212
213
        $folderID = (int)$params['id'];
214
        /** @var Folder $folder */
215
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
216
217
        if (!$folder) {
218
            $this->httpError(400);
219
        }
220
221
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
222
        $files = $this->getList()->filter('ParentID', $folderID);
223
224
        if ($files) {
225
            /** @var File $file */
226
            foreach ($files as $file) {
227
                if (!$file->canView()) {
228
                    continue;
229
                }
230
231
                $items[] = $this->getSortObjectFromData($file);
232
            }
233
        }
234
235
        // Build parents (for breadcrumbs)
236
        $parents = [];
237
        $next = $folder->Parent();
238
        while ($next && $next->exists()) {
239
            array_unshift($parents, [
240
                'id' => $next->ID,
241
                'title' => $next->getTitle(),
242
                'filename' => $next->getFilename(),
243
            ]);
244
            if ($next->ParentID) {
245
                $next = $next->Parent();
246
            } else {
247
                break;
248
            }
249
        }
250
251
        // Sort the files
252
        $column = 'title';
253
        $direction = 'asc';
254
        if (isset($params['sort'])) {
255
            list($column, $direction) = explode(',', $params['sort']);
256
        }
257
        $multiplier = ($direction === 'asc') ? 1 : -1;
258
259
        usort($items, function ($a, $b) use ($column, $multiplier) {
260
            if (!isset($a[$column]) || !isset($b[$column])) {
261
                return 0;
262
            }
263
            if ($a['type'] === 'folder' && $b['type'] !== 'folder') {
264
                return -1;
265
            }
266
            if ($b['type'] === 'folder' && $a['type'] !== 'folder') {
267
                return 1;
268
            }
269
            $numeric = (is_numeric($a[$column]) && is_numeric($b[$column]));
270
            $fieldA = ($numeric) ? floatval($a[$column]) : strtolower($a[$column]);
271
            $fieldB = ($numeric) ? floatval($b[$column]) : strtolower($b[$column]);
272
273
            if ($fieldA < $fieldB) {
274
                return $multiplier * -1;
275
            }
276
277
            if ($fieldA > $fieldB) {
278
                return $multiplier;
279
            }
280
281
            return 0;
282
        });
283
284
        // Pageination
285
        $page = (isset($params['page'])) ? $params['page'] : 0;
286
        $limit = (isset($params['limit'])) ? $params['limit'] : $this->config()->page_length;
287
        $toDisplay = array_slice($items, $page * $limit, $limit);
288
289
        // Get the full details about the files to be displayed
290
        $filteredItems = array_map(function ($item) {
291
                return $this->getObjectFromData($item['file']);
292
        }, $toDisplay);
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 apiSearch(HTTPRequest $request)
318
    {
319
        $params = $request->getVars();
320
        $list = $this->getList($params);
321
322
        $response = new HTTPResponse();
323
        $response->addHeader('Content-Type', 'application/json');
324
        $response->setBody(json_encode([
325
            // Serialisation
326
            "files" => array_map(function ($file) {
327
                return $this->getObjectFromData($file);
328
            }, $list->toArray()),
329
            "count" => $list->count(),
330
        ]));
331
332
        return $response;
333
    }
334
335
    /**
336
     * @param HTTPRequest $request
337
     *
338
     * @return HTTPResponse
339
     */
340
    public function apiDelete(HTTPRequest $request)
341
    {
342
        parse_str($request->getBody(), $vars);
343
344
        // CSRF check
345
        $token = SecurityToken::inst();
346 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...
347
            return new HTTPResponse(null, 400);
348
        }
349
350 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...
351
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
352
                ->addHeader('Content-Type', 'application/json');
353
        }
354
355
        $fileIds = $vars['ids'];
356
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
357
358 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...
359
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
360
                ->addHeader('Content-Type', 'application/json');
361
        }
362
363
        if (!min(array_map(function (File $file) {
364
            return $file->canArchive();
365
        }, $files))) {
366
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
367
                ->addHeader('Content-Type', 'application/json');
368
        }
369
370
        /** @var File $file */
371
        foreach ($files as $file) {
372
            $file->doArchive();
373
        }
374
375
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
376
            ->addHeader('Content-Type', 'application/json');
377
    }
378
379
    /**
380
     * Creates a single file based on a form-urlencoded upload.
381
     *
382
     * @param HTTPRequest $request
383
     * @return HTTPRequest|HTTPResponse
384
     */
385
    public function apiCreateFile(HTTPRequest $request)
386
    {
387
        $data = $request->postVars();
388
        $upload = $this->getUpload();
389
390
        // CSRF check
391
        $token = SecurityToken::inst();
392 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...
393
            return new HTTPResponse(null, 400);
394
        }
395
396
        // Check parent record
397
        /** @var Folder $parentRecord */
398
        $parentRecord = null;
399
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
400
            $parentRecord = Folder::get()->byID($data['ParentID']);
401
        }
402
        $data['Parent'] = $parentRecord;
403
404
        $tmpFile = $request->postVar('Upload');
405
        if (!$upload->validate($tmpFile)) {
406
            $result = ['message' => null];
407
            $errors = $upload->getErrors();
408
            if ($message = array_shift($errors)) {
409
                $result['message'] = [
410
                    'type' => 'error',
411
                    'value' => $message,
412
                ];
413
            }
414
            return (new HTTPResponse(json_encode($result), 400))
415
                ->addHeader('Content-Type', 'application/json');
416
        }
417
418
        // TODO Allow batch uploads
419
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
420
        /** @var File $file */
421
        $file = Injector::inst()->create($fileClass);
422
423
        // check canCreate permissions
424 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...
425
            $result = ['message' => [
426
                'type' => 'error',
427
                'value' => _t(
428
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
429
                    'You do not have permission to add files'
430
                )
431
            ]];
432
            return (new HTTPResponse(json_encode($result), 403))
433
                ->addHeader('Content-Type', 'application/json');
434
        }
435
436
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
437 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...
438
            $result = ['message' => [
439
                'type' => 'error',
440
                'value' => _t(
441
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
442
                    'Failed to load file'
443
                )
444
            ]];
445
            return (new HTTPResponse(json_encode($result), 400))
446
                ->addHeader('Content-Type', 'application/json');
447
        }
448
449
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
450
        $file->write();
451
452
        $result = [$this->getObjectFromData($file)];
453
454
        return (new HTTPResponse(json_encode($result)))
455
            ->addHeader('Content-Type', 'application/json');
456
    }
457
458
    /**
459
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
460
     *
461
     * @param HTTPRequest $request
462
     * @return HTTPResponse
463
     */
464
    public function apiHistory(HTTPRequest $request)
465
    {
466
        // CSRF check not required as the GET request has no side effects.
467
        $fileId = $request->getVar('fileId');
468
469
        if (!$fileId || !is_numeric($fileId)) {
470
            return new HTTPResponse(null, 400);
471
        }
472
473
        $class = File::class;
474
        $file = DataObject::get($class)->byID($fileId);
475
476
        if (!$file) {
477
            return new HTTPResponse(null, 404);
478
        }
479
480
        if (!$file->canView()) {
481
            return new HTTPResponse(null, 403);
482
        }
483
484
        $versions = Versioned::get_all_versions($class, $fileId)
485
            ->limit($this->config()->max_history_entries)
486
            ->sort('Version', 'DESC');
487
488
        $output = array();
489
        $next = array();
490
        $prev = null;
491
492
        // swap the order so we can get the version number to compare against.
493
        // i.e version 3 needs to know version 2 is the previous version
494
        $copy = $versions->map('Version', 'Version')->toArray();
495
        foreach (array_reverse($copy) as $k => $v) {
496
            if ($prev) {
497
                $next[$v] = $prev;
498
            }
499
500
            $prev = $v;
501
        }
502
503
        $_cachedMembers = array();
504
505
        /** @var File $version */
506
        foreach ($versions as $version) {
507
            $author = null;
508
509
            if ($version->AuthorID) {
510
                if (!isset($_cachedMembers[$version->AuthorID])) {
511
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
512
                        ->byID($version->AuthorID);
513
                }
514
515
                $author = $_cachedMembers[$version->AuthorID];
516
            }
517
518
            if ($version->canView()) {
519
                if (isset($next[$version->Version])) {
520
                    $summary = $version->humanizedChanges(
521
                        $version->Version,
522
                        $next[$version->Version]
523
                    );
524
525
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
526
                    // generic message
527
                    if (!$summary) {
528
                        $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.SAVEDFILE', "Saved file");
529
                    }
530
                } else {
531
                    $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UPLOADEDFILE', "Uploaded file");
532
                }
533
534
                $output[] = array(
535
                    'versionid' => $version->Version,
536
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
537
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
538
                    'status' => ($version->WasPublished) ? _t('File.PUBLISHED', 'Published') : '',
539
                    'author' => ($author)
540
                        ? $author->Name
541
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNKNOWN', "Unknown"),
542
                    'summary' => ($summary)
543
                        ? $summary
544
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NOSUMMARY', "No summary available")
545
                );
546
            }
547
        }
548
549
        return
550
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
551
    }
552
553
554
    /**
555
     * Creates a single folder, within an optional parent folder.
556
     *
557
     * @param HTTPRequest $request
558
     * @return HTTPRequest|HTTPResponse
559
     */
560
    public function apiCreateFolder(HTTPRequest $request)
561
    {
562
        $data = $request->postVars();
563
564
        $class = 'SilverStripe\\Assets\\Folder';
565
566
        // CSRF check
567
        $token = SecurityToken::inst();
568 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...
569
            return new HTTPResponse(null, 400);
570
        }
571
572
        // check addchildren permissions
573
        /** @var Folder $parentRecord */
574
        $parentRecord = null;
575
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
576
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
577
        }
578
        $data['Parent'] = $parentRecord;
579
        $data['ParentID'] = $parentRecord ? (int)$parentRecord->ID : 0;
580
581
        // Build filename
582
        $baseFilename = isset($data['Name'])
583
            ? basename($data['Name'])
584
            : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NEWFOLDER', "NewFolder");
585
586
        if ($parentRecord && $parentRecord->ID) {
587
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
588
        }
589
590
        // Ensure name is unique
591
        $nameGenerator = $this->getNameGenerator($baseFilename);
592
        $filename = null;
593
        foreach ($nameGenerator as $filename) {
594
            if (! File::find($filename)) {
595
                break;
596
            }
597
        }
598
        $data['Name'] = basename($filename);
599
600
        // Create record
601
        /** @var Folder $record */
602
        $record = Injector::inst()->create($class);
603
604
        // check create permissions
605
        if (!$record->canCreate(null, $data)) {
606
            return (new HTTPResponse(null, 403))
607
                ->addHeader('Content-Type', 'application/json');
608
        }
609
610
        $record->ParentID = $data['ParentID'];
611
        $record->Name = $record->Title = basename($data['Name']);
612
        $record->write();
613
614
        $result = $this->getObjectFromData($record);
615
616
        return (new HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
617
    }
618
619
    /**
620
     * Redirects 3.x style detail links to new 4.x style routing.
621
     *
622
     * @param HTTPRequest $request
623
     */
624
    public function legacyRedirectForEditView($request)
625
    {
626
        $fileID = $request->param('FileID');
627
        /** @var File $file */
628
        $file = File::get()->byID($fileID);
629
        $link = $this->getFileEditLink($file) ?: $this->Link();
630
        $this->redirect($link);
631
    }
632
633
    /**
634
     * Given a file return the CMS link to edit it
635
     *
636
     * @param File $file
637
     * @return string
638
     */
639
    public function getFileEditLink($file)
640
    {
641
        if (!$file || !$file->isInDB()) {
642
            return null;
643
        }
644
645
        return Controller::join_links(
646
            $this->Link('show'),
647
            $file->ParentID,
648
            'edit',
649
            $file->ID
650
        );
651
    }
652
653
    /**
654
     * Get the search context from {@link File}, used to create the search form
655
     * as well as power the /search API endpoint.
656
     *
657
     * @return SearchContext
658
     */
659
    public function getSearchContext()
660
    {
661
        $context = File::singleton()->getDefaultSearchContext();
662
663
        // Customize fields
664
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
665
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
666
        ->setConfig('showcalendar', true);
667
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
668
        ->setConfig('showcalendar', true);
669
        $dateGroup = FieldGroup::create(
670
            $dateHeader,
671
            $dateFrom,
672
            $dateTo
673
        );
674
        $context->addField($dateGroup);
675
        /** @skipUpgrade */
676
        $appCategories = array(
677
            'archive' => _t(
678
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryArchive',
679
                'Archive'
680
            ),
681
            'audio' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryAudio', 'Audio'),
682
            'document' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryDocument', 'Document'),
683
            'flash' => _t(
684
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryFlash',
685
                'Flash',
686
                'The fileformat'
687
            ),
688
            'image' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryImage', 'Image'),
689
            'video' => _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.AppCategoryVideo', 'Video'),
690
        );
691
        $context->addField(
692
            $typeDropdown = new DropdownField(
693
                'AppCategory',
694
                _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.Filetype', 'File type'),
695
                $appCategories
696
            )
697
        );
698
699
        $typeDropdown->setEmptyString(' ');
700
701
        $currentfolderLabel = _t(
702
            'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CurrentFolderOnly',
703
            'Limit to current folder?'
704
        );
705
        $context->addField(
706
            new CheckboxField('CurrentFolderOnly', $currentfolderLabel)
707
        );
708
        $context->getFields()->removeByName('Title');
709
710
        return $context;
711
    }
712
713
    /**
714
     * Get an asset renamer for the given filename.
715
     *
716
     * @param  string             $filename Path name
717
     * @return AssetNameGenerator
718
     */
719
    protected function getNameGenerator($filename)
720
    {
721
        return Injector::inst()
722
            ->createWithArgs('AssetNameGenerator', array($filename));
723
    }
724
725
    /**
726
     * @todo Implement on client
727
     *
728
     * @param bool $unlinked
729
     * @return ArrayList
730
     */
731
    public function breadcrumbs($unlinked = false)
732
    {
733
        return null;
734
    }
735
736
737
    /**
738
     * Don't include class namespace in auto-generated CSS class
739
     */
740
    public function baseCSSClasses()
741
    {
742
        return 'AssetAdmin LeftAndMain';
743
    }
744
745
    public function providePermissions()
746
    {
747
        return array(
748
            "CMS_ACCESS_AssetAdmin" => array(
749
                '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...
750
                    'title' => static::menu_title()
751
                )),
752
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
753
            )
754
        );
755
    }
756
757
    /**
758
     * Build a form scaffolder for this model
759
     *
760
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
761
     *
762
     * @param File $file
763
     * @return FormFactory
764
     */
765
    public function getFormFactory(File $file)
766
    {
767
        // Get service name based on file class
768
        $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...
769
        if ($file instanceof Folder) {
770
            $name = FolderFormFactory::class;
771
        } elseif ($file instanceof Image) {
772
            $name = ImageFormFactory::class;
773
        } else {
774
            $name = FileFormFactory::class;
775
        }
776
        return Injector::inst()->get($name);
777
    }
778
779
    /**
780
     * The form is used to generate a form schema,
781
     * as well as an intermediary object to process data through API endpoints.
782
     * Since it's used directly on API endpoints, it does not have any form actions.
783
     * It handles both {@link File} and {@link Folder} records.
784
     *
785
     * @param int $id
786
     * @return Form
787
     */
788
    public function getFileEditForm($id)
789
    {
790
        return $this->getAbstractFileForm($id, 'fileEditForm');
791
    }
792
793
    /**
794
     * Get file edit form
795
     *
796
     * @return Form
797
     */
798 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...
799
    {
800
        // Get ID either from posted back value, or url parameter
801
        $request = $this->getRequest();
802
        $id = $request->param('ID') ?: $request->postVar('ID');
803
        return $this->getFileEditForm($id);
804
    }
805
806
    /**
807
     * The form is used to generate a form schema,
808
     * as well as an intermediary object to process data through API endpoints.
809
     * Since it's used directly on API endpoints, it does not have any form actions.
810
     * It handles both {@link File} and {@link Folder} records.
811
     *
812
     * @param int $id
813
     * @return Form
814
     */
815
    public function getFileInsertForm($id)
816
    {
817
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT ]);
818
    }
819
820
    /**
821
     * Get file insert form
822
     *
823
     * @return Form
824
     */
825 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...
826
    {
827
        // Get ID either from posted back value, or url parameter
828
        $request = $this->getRequest();
829
        $id = $request->param('ID') ?: $request->postVar('ID');
830
        return $this->getFileInsertForm($id);
831
    }
832
833
    /**
834
     * Abstract method for generating a form for a file
835
     *
836
     * @param int $id Record ID
837
     * @param string $name Form name
838
     * @param array $context Form context
839
     * @return Form
840
     */
841
    protected function getAbstractFileForm($id, $name, $context = [])
842
    {
843
        /** @var File $file */
844
        $file = $this->getList()->byID($id);
845
846 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...
847
            $this->httpError(403, _t(
848
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
849
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
850
                '',
851
                ['ObjectTitle' => $file->i18n_singular_name()]
852
            ));
853
            return null;
854
        }
855
856
        // Pass to form factory
857
        $augmentedContext = array_merge($context, ['Record' => $file]);
858
        $scaffolder = $this->getFormFactory($file);
859
        $form = $scaffolder->getForm($this, $name, $augmentedContext);
860
861
        // Configure form to respond to validation errors with form schema
862
        // if requested via react.
863
        $form->setValidationResponseCallback(function (ValidationResult $error) use ($form, $id, $name) {
864
            $schemaId = Controller::join_links($this->Link('schema'), $name, $id);
865
            return $this->getSchemaResponse($schemaId, $form, $error);
866
        });
867
868
        return $form;
869
    }
870
871
    /**
872
     * Get form for selecting a file
873
     *
874
     * @return Form
875
     */
876
    public function fileSelectForm()
877
    {
878
        // Get ID either from posted back value, or url parameter
879
        $request = $this->getRequest();
880
        $id = $request->param('ID') ?: $request->postVar('ID');
881
        return $this->getFileSelectForm($id);
882
    }
883
884
    /**
885
     * Get form for selecting a file
886
     *
887
     * @param int $id ID of the record being selected
888
     * @return Form
889
     */
890
    public function getFileSelectForm($id)
891
    {
892
        return $this->getAbstractFileForm($id, 'fileSelectForm', [ 'Type' => AssetFormFactory::TYPE_SELECT ]);
893
    }
894
895
    /**
896
     * @param array $context
897
     * @return Form
898
     * @throws InvalidArgumentException
899
     */
900
    public function getFileHistoryForm($context)
901
    {
902
        // Check context
903
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
904
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
905
        }
906
        $id = $context['RecordID'];
907
        $versionId = $context['RecordVersion'];
908
        if (!$id || !$versionId) {
909
            return $this->httpError(404);
910
        }
911
912
        /** @var File $file */
913
        $file = Versioned::get_version(File::class, $id, $versionId);
914
        if (!$file) {
915
            return $this->httpError(404);
916
        }
917
918 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...
919
            $this->httpError(403, _t(
920
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
921
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
922
                '',
923
                ['ObjectTitle' => $file->i18n_singular_name()]
924
            ));
925
            return null;
926
        }
927
928
        $effectiveContext = array_merge($context, ['Record' => $file]);
929
        /** @var FormFactory $scaffolder */
930
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
931
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
932
933
        // Configure form to respond to validation errors with form schema
934
        // if requested via react.
935 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...
936
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
937
            return $this->getSchemaResponse($schemaId, $form, $errors);
938
        });
939
940
        return $form;
941
    }
942
943
    /**
944
     * Gets a JSON schema representing the current edit form.
945
     *
946
     * WARNING: Experimental API.
947
     *
948
     * @param HTTPRequest $request
949
     * @return HTTPResponse
950
     */
951
    public function schema($request)
952
    {
953
        $formName = $request->param('FormName');
954
        if ($formName !== 'fileHistoryForm') {
955
            return parent::schema($request);
956
        }
957
958
        // Get schema for history form
959
        // @todo Eventually all form scaffolding will be based on context rather than record ID
960
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
961
        $itemID = $request->param('ItemID');
962
        $version = $request->param('OtherItemID');
963
        $form = $this->getFileHistoryForm([
964
            'RecordID' => $itemID,
965
            'RecordVersion' => $version,
966
        ]);
967
968
        // Respond with this schema
969
        $response = $this->getResponse();
970
        $response->addHeader('Content-Type', 'application/json');
971
        $schemaID = $this->getRequest()->getURL();
972
        return $this->getSchemaResponse($schemaID, $form);
973
    }
974
975
    /**
976
     * Get file history form
977
     *
978
     * @return Form
979
     */
980
    public function fileHistoryForm()
981
    {
982
        $request = $this->getRequest();
983
        $id = $request->param('ID') ?: $request->postVar('ID');
984
        $version = $request->param('OtherID') ?: $request->postVar('Version');
985
        $form = $this->getFileHistoryForm([
986
            'RecordID' => $id,
987
            'RecordVersion' => $version,
988
        ]);
989
        return $form;
990
    }
991
992
    /**
993
     * @param array $data
994
     * @param Form $form
995
     * @return HTTPResponse
996
     */
997
    public function save($data, $form)
998
    {
999
        return $this->saveOrPublish($data, $form, false);
1000
    }
1001
1002
    /**
1003
     * @param array $data
1004
     * @param Form $form
1005
     * @return HTTPResponse
1006
     */
1007
    public function publish($data, $form)
1008
    {
1009
        return $this->saveOrPublish($data, $form, true);
1010
    }
1011
1012
    /**
1013
     * Update thisrecord
1014
     *
1015
     * @param array $data
1016
     * @param Form $form
1017
     * @param bool $doPublish
1018
     * @return HTTPResponse
1019
     */
1020
    protected function saveOrPublish($data, $form, $doPublish = false)
1021
    {
1022 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...
1023
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1024
                ->addHeader('Content-Type', 'application/json');
1025
        }
1026
1027
        $id = (int) $data['ID'];
1028
        /** @var File $record */
1029
        $record = $this->getList()->filter('ID', $id)->first();
1030
1031 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...
1032
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1033
                ->addHeader('Content-Type', 'application/json');
1034
        }
1035
1036
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
1037
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1038
                ->addHeader('Content-Type', 'application/json');
1039
        }
1040
1041
        $form->saveInto($record);
1042
        $record->write();
1043
1044
        // Publish this record and owned objects
1045
        if ($doPublish) {
1046
            $record->publishRecursive();
1047
        }
1048
1049
        // Note: Force return of schema / state in success result
1050
        return $this->getRecordUpdatedResponse($record, $form);
1051
    }
1052
1053
    public function unpublish($data, $form)
1054
    {
1055 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...
1056
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1057
                ->addHeader('Content-Type', 'application/json');
1058
        }
1059
1060
        $id = (int) $data['ID'];
1061
        /** @var File $record */
1062
        $record = $this->getList()->filter('ID', $id)->first();
1063
1064
        if (!$record) {
1065
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1066
                ->addHeader('Content-Type', 'application/json');
1067
        }
1068
1069
        if (!$record->canUnpublish()) {
1070
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1071
                ->addHeader('Content-Type', 'application/json');
1072
        }
1073
1074
        $record->doUnpublish();
1075
        return $this->getRecordUpdatedResponse($record, $form);
1076
    }
1077
1078
    /**
1079
     * @param File $file
1080
     *
1081
     * @return array
1082
     */
1083
    protected function getSortObjectFromData(File $file)
1084
    {
1085
        return [
1086
            'file' => $file,
1087
            'created' => $file->Created,
1088
            'title' => $file->Title,
1089
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1090
        ];
1091
    }
1092
    
1093
    /**
1094
     * @param File $file
1095
     *
1096
     * @return array
1097
     */
1098
    public function getObjectFromData(File $file)
1099
    {
1100
        $object = array(
1101
            'id' => $file->ID,
1102
            'created' => $file->Created,
1103
            'lastUpdated' => $file->LastEdited,
1104
            'owner' => null,
1105
            'parent' => null,
1106
            'title' => $file->Title,
1107
            'exists' => $file->exists(), // Broken file check
1108
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1109
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
1110
            'name' => $file->Name,
1111
            'filename' => $file->Filename,
1112
            '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...
1113
            'size' => $file->AbsoluteSize,
1114
            'url' => $file->AbsoluteURL,
1115
            'published' => $file->isPublished(),
1116
            'modified' => $file->isModifiedOnDraft(),
1117
            'draft' => $file->isOnDraftOnly(),
1118
            'canEdit' => $file->canEdit(),
1119
            'canDelete' => $file->canArchive(),
1120
        );
1121
1122
        /** @var Member $owner */
1123
        $owner = $file->Owner();
1124
1125
        if ($owner) {
1126
            $object['owner'] = array(
1127
                'id' => $owner->ID,
1128
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1129
            );
1130
        }
1131
1132
        /** @var Folder $parent */
1133
        $parent = $file->Parent();
1134
1135
        if ($parent) {
1136
            $object['parent'] = array(
1137
                'id' => $parent->ID,
1138
                'title' => $parent->Title,
1139
                'filename' => $parent->Filename,
1140
            );
1141
        }
1142
1143
        /** @var File $file */
1144
        if ($file->getIsImage()) {
1145
            // Small thumbnail
1146
            $smallWidth = UploadField::config()->get('thumbnail_width');
1147
            $smallHeight = UploadField::config()->get('thumbnail_height');
1148
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1149
            if ($smallThumbnail && $smallThumbnail->exists()) {
1150
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1151
            }
1152
1153
            // Large thumbnail
1154
            $width = $this->config()->get('thumbnail_width');
1155
            $height = $this->config()->get('thumbnail_height');
1156
            $thumbnail = $file->FitMax($width, $height);
1157
            if ($thumbnail && $thumbnail->exists()) {
1158
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1159
            }
1160
            $object['dimensions']['width'] = $file->Width;
1161
            $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...
1162
        } else {
1163
            $object['thumbnail'] = $file->PreviewLink();
1164
        }
1165
1166
        return $object;
1167
    }
1168
1169
    /**
1170
     * Returns the files and subfolders contained in the currently selected folder,
1171
     * defaulting to the root node. Doubles as search results, if any search parameters
1172
     * are set through {@link SearchForm()}.
1173
     *
1174
     * @param array $params Unsanitised request parameters
1175
     * @return DataList
1176
     */
1177
    protected function getList($params = array())
1178
    {
1179
        $context = $this->getSearchContext();
1180
1181
        // Overwrite name filter to search both Name and Title attributes
1182
        $context->removeFilterByName('Name');
1183
1184
        // Lazy loaded list. Allows adding new filters through SearchContext.
1185
        /** @var DataList $list */
1186
        $list = $context->getResults($params);
1187
1188
        // Re-add previously removed "Name" filter as combined filter
1189
        // TODO Replace with composite SearchFilter once that API exists
1190
        if (!empty($params['Name'])) {
1191
            $list = $list->filterAny(array(
1192
                'Name:PartialMatch' => $params['Name'],
1193
                'Title:PartialMatch' => $params['Name']
1194
            ));
1195
        }
1196
1197
        // Optionally limit search to a folder (non-recursive)
1198
        if (!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
1199
            $list = $list->filter('ParentID', $params['ParentID']);
1200
        }
1201
1202
        // Date filtering
1203 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...
1204
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
1205
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
1206
        }
1207 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...
1208
            $toDate = new DateField(null, null, $params['CreatedTo']);
1209
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
1210
        }
1211
1212
        // Categories
1213
        if (!empty($filters['AppCategory']) && !empty(File::config()->app_categories[$filters['AppCategory']])) {
0 ignored issues
show
Bug introduced by
The variable $filters seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
1214
            $extensions = File::config()->app_categories[$filters['AppCategory']];
1215
            $list = $list->filter('Name:PartialMatch', $extensions);
1216
        }
1217
1218
        // Sort folders first
1219
        $list = $list->sort(
1220
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
1221
        );
1222
1223
        // Pagination
1224
        if (isset($filters['page']) && isset($filters['limit'])) {
1225
            $page = $filters['page'];
1226
            $limit = $filters['limit'];
1227
            $offset = ($page - 1) * $limit;
1228
            $list = $list->limit($limit, $offset);
1229
        }
1230
1231
        // Access checks
1232
        $list = $list->filterByCallback(function (File $file) {
1233
            return $file->canView();
1234
        });
1235
1236
        return $list;
1237
    }
1238
1239
    /**
1240
     * Action handler for adding pages to a campaign
1241
     *
1242
     * @param array $data
1243
     * @param Form $form
1244
     * @return DBHTMLText|HTTPResponse
1245
     */
1246
    public function addtocampaign($data, $form)
1247
    {
1248
        $id = $data['ID'];
1249
        $record = $this->getList()->byID($id);
1250
1251
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1252
        $results = $handler->addToCampaign($record, $data['Campaign']);
1253
        if (!isset($results)) {
1254
            return null;
1255
        }
1256
1257
        // Send extra "message" data with schema response
1258
        $extraData = ['message' => $results];
1259
        $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1260
        return $this->getSchemaResponse($schemaId, $form, null, $extraData);
1261
    }
1262
1263
    /**
1264
     * Url handler for add to campaign form
1265
     *
1266
     * @param HTTPRequest $request
1267
     * @return Form
1268
     */
1269
    public function addToCampaignForm($request)
1270
    {
1271
        // Get ID either from posted back value, or url parameter
1272
        $id = $request->param('ID') ?: $request->postVar('ID');
1273
        return $this->getAddToCampaignForm($id);
1274
    }
1275
1276
    /**
1277
     * @param int $id
1278
     * @return Form
1279
     */
1280
    public function getAddToCampaignForm($id)
1281
    {
1282
        // Get record-specific fields
1283
        $record = $this->getList()->byID($id);
1284
1285
        if (!$record) {
1286
            $this->httpError(404, _t(
1287
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1288
                'That {Type} couldn\'t be found',
1289
                '',
1290
                ['Type' => File::singleton()->i18n_singular_name()]
1291
            ));
1292
            return null;
1293
        }
1294 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...
1295
            $this->httpError(403, _t(
1296
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1297
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1298
                '',
1299
                ['ObjectTitle' => $record->i18n_singular_name()]
1300
            ));
1301
            return null;
1302
        }
1303
1304
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1305
        $form = $handler->Form($record);
1306
1307 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...
1308
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1309
            return $this->getSchemaResponse($schemaId, $form, $errors);
1310
        });
1311
1312
        return $form;
1313
    }
1314
1315
    /**
1316
     * @return Upload
1317
     */
1318
    protected function getUpload()
1319
    {
1320
        $upload = Upload::create();
1321
        $upload->getValidator()->setAllowedExtensions(
1322
            // filter out '' since this would be a regex problem on JS end
1323
            array_filter(File::config()->get('allowed_extensions'))
1324
        );
1325
1326
        return $upload;
1327
    }
1328
1329
    /**
1330
     * Get response for successfully updated record
1331
     *
1332
     * @param File $record
1333
     * @param Form $form
1334
     * @return HTTPResponse
1335
     */
1336
    protected function getRecordUpdatedResponse($record, $form)
1337
    {
1338
        // Return the record data in the same response as the schema to save a postback
1339
        $schemaData = ['record' => $this->getObjectFromData($record)];
1340
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1341
        return $this->getSchemaResponse($schemaId, $form, null, $schemaData);
1342
    }
1343
}
1344