Completed
Pull Request — master (#339)
by
unknown
01:50
created

AssetAdmin::publish()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
216
217
        if (!isset($params['id']) && !strlen($params['id'])) {
218
            $this->httpError(400);
219
        }
220
221
        $folderID = (int)$params['id'];
222
        /** @var Folder $folder */
223
        $folder = $folderID ? Folder::get()->byID($folderID) : Folder::singleton();
224
225
        if (!$folder) {
226
            $this->httpError(400);
227
        }
228
229
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
230
        $files = $this->getList()->filter('ParentID', $folderID);
231
232
        if ($files) {
233
            /** @var File $file */
234
            foreach ($files as $file) {
235
                if (!$file->canView()) {
236
                    continue;
237
                }
238
239
                $items[] = $this->getObjectFromData($file);
240
            }
241
        }
242
243
        // Build parents (for breadcrumbs)
244
        $parents = [];
245
        $next = $folder->Parent();
246
        while ($next && $next->exists()) {
247
            array_unshift($parents, [
248
                'id' => $next->ID,
249
                'title' => $next->getTitle(),
250
                'filename' => $next->getFilename(),
251
            ]);
252
            if ($next->ParentID) {
253
                $next = $next->Parent();
254
            } else {
255
                break;
256
            }
257
        }
258
259
        $column = 'title';
260
        $direction = 'asc';
261
        if (isset($params['sort'])) {
262
            list($column, $direction) = explode(',', $params['sort']);
263
        }
264
        $multiplier = ($direction === 'asc') ? 1 : -1;
265
266
        usort($items, function ($a, $b) use ($column, $multiplier) {
267
            if (!isset($a[$column]) || !isset($b[$column])) {
268
                return 0;
269
            }
270
            if ($a['type'] === 'folder' && $b['type'] !== 'folder') {
271
                return -1;
272
            }
273
            if ($b['type'] === 'folder' && $a['type'] !== 'folder') {
274
                return 1;
275
            }
276
            $numeric = (is_numeric($a[$column]) && is_numeric($b[$column]));
277
            $fieldA = ($numeric) ? floatval($a[$column]) : strtolower($a[$column]);
278
            $fieldB = ($numeric) ? floatval($b[$column]) : strtolower($b[$column]);
279
280
            if ($fieldA < $fieldB) {
281
                return $multiplier * -1;
282
            }
283
284
            if ($fieldA > $fieldB) {
285
                return $multiplier;
286
            }
287
288
            return 0;
289
        });
290
291
        $page = (isset($params['page'])) ? $params['page'] : 0;
292
        $limit = (isset($params['limit'])) ? $params['limit'] : $this->config()->page_length;
293
        $filteredItems = array_slice($items, $page * $limit, $limit);
294
295
        // Build response
296
        $response = new HTTPResponse();
297
        $response->addHeader('Content-Type', 'application/json');
298
        $response->setBody(json_encode([
299
            'files' => $filteredItems,
300
            'title' => $folder->getTitle(),
301
            'count' => count($items),
302
            'parents' => $parents,
303
            'parent' => $parents ? $parents[count($parents) - 1] : null,
304
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
305
            'folderID' => $folderID,
306
            'canEdit' => $folder->canEdit(),
307
            'canDelete' => $folder->canArchive(),
308
        ]));
309
310
        return $response;
311
    }
312
313
    /**
314
     * @param HTTPRequest $request
315
     *
316
     * @return HTTPResponse
317
     */
318
    public function apiSearch(HTTPRequest $request)
319
    {
320
        $params = $request->getVars();
321
        $list = $this->getList($params);
322
323
        $response = new HTTPResponse();
324
        $response->addHeader('Content-Type', 'application/json');
325
        $response->setBody(json_encode([
326
            // Serialisation
327
            "files" => array_map(function ($file) {
328
                return $this->getObjectFromData($file);
329
            }, $list->toArray()),
330
            "count" => $list->count(),
331
        ]));
332
333
        return $response;
334
    }
335
336
    /**
337
     * @param HTTPRequest $request
338
     *
339
     * @return HTTPResponse
340
     */
341
    public function apiDelete(HTTPRequest $request)
342
    {
343
        parse_str($request->getBody(), $vars);
344
345
        // CSRF check
346
        $token = SecurityToken::inst();
347 View Code Duplication
        if (empty($vars[$token->getName()]) || !$token->check($vars[$token->getName()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
348
            return new HTTPResponse(null, 400);
349
        }
350
351 View Code Duplication
        if (!isset($vars['ids']) || !$vars['ids']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
352
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
353
                ->addHeader('Content-Type', 'application/json');
354
        }
355
356
        $fileIds = $vars['ids'];
357
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
358
359 View Code Duplication
        if (!count($files)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
360
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
361
                ->addHeader('Content-Type', 'application/json');
362
        }
363
364
        if (!min(array_map(function (File $file) {
365
            return $file->canArchive();
366
        }, $files))) {
367
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
368
                ->addHeader('Content-Type', 'application/json');
369
        }
370
371
        /** @var File $file */
372
        foreach ($files as $file) {
373
            $file->doArchive();
374
        }
375
376
        return (new HTTPResponse(json_encode(['status' => 'file was deleted'])))
377
            ->addHeader('Content-Type', 'application/json');
378
    }
379
380
    /**
381
     * Creates a single file based on a form-urlencoded upload.
382
     *
383
     * @param HTTPRequest $request
384
     * @return HTTPRequest|HTTPResponse
385
     */
386
    public function apiCreateFile(HTTPRequest $request)
387
    {
388
        $data = $request->postVars();
389
        $upload = $this->getUpload();
390
391
        // CSRF check
392
        $token = SecurityToken::inst();
393 View Code Duplication
        if (empty($data[$token->getName()]) || !$token->check($data[$token->getName()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
394
            return new HTTPResponse(null, 400);
395
        }
396
397
        // Check parent record
398
        /** @var Folder $parentRecord */
399
        $parentRecord = null;
400 View Code Duplication
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
401
            $parentRecord = Folder::get()->byID($data['ParentID']);
402
        }
403
        $data['Parent'] = $parentRecord;
404
405
        $tmpFile = $request->postVar('Upload');
406 View Code Duplication
        if (!$upload->validate($tmpFile)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
407
            $result = ['message' => null];
408
            $errors = $upload->getErrors();
409
            if ($message = array_shift($errors)) {
410
                $result['message'] = [
411
                    'type' => 'error',
412
                    'value' => $message,
413
                ];
414
            }
415
            return (new HTTPResponse(json_encode($result), 400))
416
                ->addHeader('Content-Type', 'application/json');
417
        }
418
419
        // TODO Allow batch uploads
420
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
421
        /** @var File $file */
422
        $file = Injector::inst()->create($fileClass);
423
424
        // check canCreate permissions
425 View Code Duplication
        if (!$file->canCreate(null, $data)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
426
            $result = ['message' => [
427
                'type' => 'error',
428
                'value' => _t(
429
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
430
                    'You do not have permission to add files'
431
                )
432
            ]];
433
            return (new HTTPResponse(json_encode($result), 403))
434
                ->addHeader('Content-Type', 'application/json');
435
        }
436
437
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
438 View Code Duplication
        if (!$uploadResult) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
439
            $result = ['message' => [
440
                'type' => 'error',
441
                'value' => _t(
442
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
443
                    'Failed to load file'
444
                )
445
            ]];
446
            return (new HTTPResponse(json_encode($result), 400))
447
                ->addHeader('Content-Type', 'application/json');
448
        }
449
450
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
451
        $file->write();
452
453
        $result = [$this->getObjectFromData($file)];
454
455
        return (new HTTPResponse(json_encode($result)))
456
            ->addHeader('Content-Type', 'application/json');
457
    }
458
    
459
    /**
460
     * Creates a single file based on a form-urlencoded upload.
461
     *
462
     * @param HTTPRequest $request
463
     * @return HTTPRequest|HTTPResponse
464
     */
465
    public function apiUploadFile(HTTPRequest $request) {
466
        $data = $request->postVars();
467
        $upload = $this->getUpload();
468
        
469
        // CSRF check
470
        $token = SecurityToken::inst();
471 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...
472
            return new HTTPResponse(null, 400);
473
        }
474
    
475
        // Check parent record
476
        /** @var Folder $parentRecord */
477
        $parentRecord = null;
478 View Code Duplication
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
479
            $parentRecord = Folder::get()->byID($data['ParentID']);
480
        }
481
    
482
        $tmpFile = $data['Upload'];
483 View Code Duplication
        if(!$upload->validate($tmpFile)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
484
            $result = ['message' => null];
485
            $errors = $upload->getErrors();
486
            if ($message = array_shift($errors)) {
487
                $result['message'] = [
488
                    'type' => 'error',
489
                    'value' => $message,
490
                ];
491
            }
492
            return (new HTTPResponse(json_encode($result), 400))
493
                ->addHeader('Content-Type', 'application/json');
494
        }
495
    
496
        $folder = $parentRecord ? $parentRecord->getFilename() : '/';
497
    
498
        try {
499
            $tuple = $upload->load($tmpFile, $folder);
500
        } catch (Exception $e) {
501
            $result = [
502
                'message' => [
503
                    'type' => 'error',
504
                    'value' => $e->getMessage(),
505
                ]
506
            ];
507
            return (new HTTPResponse(json_encode($result), 400))
508
                ->addHeader('Content-Type', 'application/json');
509
        }
510
        
511
        if ($upload->isError()) {
512
            $result['message'] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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

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

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

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