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

AssetAdmin::breadcrumbs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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