Completed
Pull Request — master (#499)
by
unknown
02:45
created

AssetAdmin::publish()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

Loading history...
55
56
    private static $menu_icon_class = 'font-icon-image';
0 ignored issues
show
Unused Code introduced by
The property $menu_icon_class is not used and could be removed.

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

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

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

Loading history...
59
60
    private static $url_handlers = [
0 ignored issues
show
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...
61
        // Legacy redirect for SS3-style detail view
62
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
63
        // Pass all URLs to the index, for React to unpack
64
        'show/$FolderID/edit/$FileID' => 'index',
65
        // API access points with structured data
66
        'POST api/createFile' => 'apiCreateFile',
67
        'POST api/uploadFile' => 'apiUploadFile',
68
        'GET api/history' => 'apiHistory',
69
        'fileEditForm/$ID' => 'fileEditForm',
70
        'fileInsertForm/$ID' => 'fileInsertForm',
71
        'fileUpdateForm/$ID' => 'fileUpdateForm',
72
        'fileEditorLinkForm/$ID' => 'fileEditorLinkForm',
73
        'fileUpdateEditorLinkForm/$ID' => 'fileUpdateEditorLinkForm',
74
        'fileHistoryForm/$ID/$VersionID' => 'fileHistoryForm',
75
        'folderCreateForm/$ParentID' => 'folderCreateForm',
76
    ];
77
78
    /**
79
     * Amount of results showing on a single page.
80
     *
81
     * @config
82
     * @var int
83
     */
84
    private static $page_length = 50;
0 ignored issues
show
Unused Code introduced by
The property $page_length is not used and could be removed.

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

Loading history...
85
86
    /**
87
     * @config
88
     * @see Upload->allowedMaxFileSize
89
     * @var int
90
     */
91
    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...
92
93
    /**
94
     * @config
95
     *
96
     * @var int
97
     */
98
    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...
99
100
    /**
101
     * @var array
102
     */
103
    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...
104
        'legacyRedirectForEditView',
105
        'apiCreateFile',
106
        'apiUploadFile',
107
        'apiHistory',
108
        'folderCreateForm',
109
        'fileEditForm',
110
        'fileHistoryForm',
111
        'addToCampaignForm',
112
        'fileInsertForm',
113
        'fileUpdateForm',
114
        'fileEditorLinkForm',
115
        'fileUpdateEditorLinkForm',
116
        'schema',
117
        'fileSelectForm',
118
        'fileSearchForm',
119
    );
120
121
    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...
122
123
    /**
124
     * Retina thumbnail image (native size: 176)
125
     *
126
     * @config
127
     * @var int
128
     */
129
    private static $thumbnail_width = 352;
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...
130
131
    /**
132
     * Retina thumbnail height (native size: 132)
133
     *
134
     * @config
135
     * @var int
136
     */
137
    private static $thumbnail_height = 264;
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...
138
139
    /**
140
     * Safely limit max inline thumbnail size to 200kb
141
     *
142
     * @config
143
     * @var int
144
     */
145
    private static $max_thumbnail_bytes = 200000;
0 ignored issues
show
Unused Code introduced by
The property $max_thumbnail_bytes 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...
146
147
    /**
148
     * Set up the controller
149
     */
150
    public function init()
151
    {
152
        parent::init();
153
154
        $module = ModuleLoader::getModule('silverstripe/asset-admin');
155
        Requirements::add_i18n_javascript($module->getResourcePath('client/lang'), false, true);
156
        Requirements::javascript($module->getResourcePath("client/dist/js/bundle.js"));
157
        Requirements::css($module->getResourcePath("client/dist/styles/bundle.css"));
158
159
        CMSBatchActionHandler::register('delete', DeleteAssets::class, Folder::class);
160
    }
161
162
    public function getClientConfig()
163
    {
164
        $baseLink = $this->Link();
165
        return array_merge(parent::getClientConfig(), [
166
            'reactRouter' => true,
167
            'createFileEndpoint' => [
168
                'url' => Controller::join_links($baseLink, 'api/createFile'),
169
                'method' => 'post',
170
                'payloadFormat' => 'urlencoded',
171
            ],
172
            'uploadFileEndpoint' => [
173
                'url' => Controller::join_links($baseLink, 'api/uploadFile'),
174
                'method' => 'post',
175
                'payloadFormat' => 'urlencoded',
176
            ],
177
            'historyEndpoint' => [
178
                'url' => Controller::join_links($baseLink, 'api/history'),
179
                'method' => 'get',
180
                'responseFormat' => 'json',
181
            ],
182
            'limit' => $this->config()->page_length,
183
            'form' => [
184
                'fileEditForm' => [
185
                    'schemaUrl' => $this->Link('schema/fileEditForm')
186
                ],
187
                'fileInsertForm' => [
188
                    'schemaUrl' => $this->Link('schema/fileInsertForm')
189
                ],
190
                'fileUpdateForm' => [
191
                    'schemaUrl' => $this->Link('schema/fileUpdateForm')
192
                ],
193
                'remoteEditForm' => [
194
                    'schemaUrl' => LeftAndMain::singleton()
195
                        ->Link('Modals/remoteEditFormSchema'),
196
                ],
197
                'remoteCreateForm' => [
198
                    'schemaUrl' => LeftAndMain::singleton()
199
                        ->Link('methodSchema/Modals/remoteCreateForm')
200
                ],
201
                'fileSelectForm' => [
202
                    'schemaUrl' => $this->Link('schema/fileSelectForm')
203
                ],
204
                'addToCampaignForm' => [
205
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
206
                ],
207
                'fileHistoryForm' => [
208
                    'schemaUrl' => $this->Link('schema/fileHistoryForm')
209
                ],
210
                'fileSearchForm' => [
211
                    'schemaUrl' => $this->Link('schema/fileSearchForm')
212
                ],
213
                'folderCreateForm' => [
214
                    'schemaUrl' => $this->Link('schema/folderCreateForm')
215
                ],
216
                'fileEditorLinkForm' => [
217
                    'schemaUrl' => $this->Link('schema/fileEditorLinkForm'),
218
                ],
219
                'fileUpdateEditorLinkForm' => [
220
                    'schemaUrl' => $this->Link('schema/fileUpdateEditorLinkForm'),
221
                ],
222
            ],
223
        ]);
224
    }
225
226
    /**
227
     * Creates a single file based on a form-urlencoded upload.
228
     *
229
     * @param HTTPRequest $request
230
     * @return HTTPRequest|HTTPResponse
231
     */
232
    public function apiCreateFile(HTTPRequest $request)
233
    {
234
        $data = $request->postVars();
235
236
        // When creating new files, rename on conflict
237
        $upload = $this->getUpload();
238
        $upload->setReplaceFile(false);
239
240
        // CSRF check
241
        $token = SecurityToken::inst();
242 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...
243
            return new HTTPResponse(null, 400);
244
        }
245
246
        // Check parent record
247
        /** @var Folder $parentRecord */
248
        $parentRecord = null;
249
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
250
            $parentRecord = Folder::get()->byID($data['ParentID']);
251
        }
252
        $data['Parent'] = $parentRecord;
253
254
        $tmpFile = $request->postVar('Upload');
255 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...
256
            $result = ['message' => null];
257
            $errors = $upload->getErrors();
258
            if ($message = array_shift($errors)) {
259
                $result['message'] = [
260
                    'type' => 'error',
261
                    'value' => $message,
262
                ];
263
            }
264
            return (new HTTPResponse(json_encode($result), 400))
265
                ->addHeader('Content-Type', 'application/json');
266
        }
267
268
        // TODO Allow batch uploads
269
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
270
        /** @var File $file */
271
        $file = Injector::inst()->create($fileClass);
272
273
        // check canCreate permissions
274 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...
275
            $result = ['message' => [
276
                'type' => 'error',
277
                'value' => _t(
278
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
279
                    'You do not have permission to add files'
280
                )
281
            ]];
282
            return (new HTTPResponse(json_encode($result), 403))
283
                ->addHeader('Content-Type', 'application/json');
284
        }
285
286
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
287 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...
288
            $result = ['message' => [
289
                'type' => 'error',
290
                'value' => _t(
291
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
292
                    'Failed to load file'
293
                )
294
            ]];
295
            return (new HTTPResponse(json_encode($result), 400))
296
                ->addHeader('Content-Type', 'application/json');
297
        }
298
299
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
300
        $file->write();
301
302
        $result = [$this->getObjectFromData($file)];
303
304
        // Don't discard pre-generated client side canvas thumbnail
305
        if ($result[0]['category'] === 'image') {
306
            unset($result[0]['thumbnail']);
307
        }
308
309
        return (new HTTPResponse(json_encode($result)))
310
            ->addHeader('Content-Type', 'application/json');
311
    }
312
313
    /**
314
     * Upload a new asset for a pre-existing record. Returns the asset tuple.
315
     *
316
     * Note that conflict resolution is as follows:
317
     *  - If uploading a file with the same extension, we simply keep the same filename,
318
     *    and overwrite any existing files (same name + sha = don't duplicate).
319
     *  - If uploading a new file with a different extension, then the filename will
320
     *    be replaced, and will be checked for uniqueness against other File dataobjects.
321
     *
322
     * @param HTTPRequest $request Request containing vars 'ID' of parent record ID,
323
     * and 'Name' as form filename value
324
     * @return HTTPRequest|HTTPResponse
325
     */
326
    public function apiUploadFile(HTTPRequest $request)
327
    {
328
        $data = $request->postVars();
329
330
        // When updating files, replace on conflict
331
        $upload = $this->getUpload();
332
        $upload->setReplaceFile(true);
333
334
        // CSRF check
335
        $token = SecurityToken::inst();
336 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...
337
            return new HTTPResponse(null, 400);
338
        }
339
        $tmpFile = $data['Upload'];
340
        if (empty($data['ID']) || empty($tmpFile['name']) || !array_key_exists('Name', $data)) {
341
            return new HTTPResponse('Invalid request', 400);
342
        }
343
344
        // Check parent record
345
        /** @var File $file */
346
        $file = File::get()->byID($data['ID']);
347
        if (!$file) {
348
            return new HTTPResponse('File not found', 404);
349
        }
350
        $folder = $file->ParentID ? $file->Parent()->getFilename() : '/';
351
352
        // If extension is the same, attempt to re-use existing name
353
        if (File::get_file_extension($tmpFile['name']) === File::get_file_extension($data['Name'])) {
354
            $tmpFile['name'] = $data['Name'];
355
        } else {
356
            // If we are allowing this upload to rename the underlying record,
357
            // do a uniqueness check.
358
            $renamer = $this->getNameGenerator($tmpFile['name']);
359
            foreach ($renamer as $name) {
360
                $filename = File::join_paths($folder, $name);
361
                if (!File::find($filename)) {
362
                    $tmpFile['name'] = $name;
363
                    break;
364
                }
365
            }
366
        }
367
368 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...
369
            $result = ['message' => null];
370
            $errors = $upload->getErrors();
371
            if ($message = array_shift($errors)) {
372
                $result['message'] = [
373
                    'type' => 'error',
374
                    'value' => $message,
375
                ];
376
            }
377
            return (new HTTPResponse(json_encode($result), 400))
378
                ->addHeader('Content-Type', 'application/json');
379
        }
380
381
        try {
382
            $tuple = $upload->load($tmpFile, $folder);
383
        } catch (Exception $e) {
384
            $result = [
385
                'message' => [
386
                    'type' => 'error',
387
                    'value' => $e->getMessage(),
388
                ]
389
            ];
390
            return (new HTTPResponse(json_encode($result), 400))
391
                ->addHeader('Content-Type', 'application/json');
392
        }
393
394
        if ($upload->isError()) {
395
            $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...
396
                'type' => 'error',
397
                'value' => implode(' ' . PHP_EOL, $upload->getErrors()),
398
            ];
399
            return (new HTTPResponse(json_encode($result), 400))
400
                ->addHeader('Content-Type', 'application/json');
401
        }
402
403
        $tuple['Name'] = basename($tuple['Filename']);
404
        return (new HTTPResponse(json_encode($tuple)))
405
            ->addHeader('Content-Type', 'application/json');
406
    }
407
408
    /**
409
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
410
     *
411
     * @param HTTPRequest $request
412
     * @return HTTPResponse
413
     */
414
    public function apiHistory(HTTPRequest $request)
415
    {
416
        // CSRF check not required as the GET request has no side effects.
417
        $fileId = $request->getVar('fileId');
418
419
        if (!$fileId || !is_numeric($fileId)) {
420
            return new HTTPResponse(null, 400);
421
        }
422
423
        $class = File::class;
424
        $file = DataObject::get($class)->byID($fileId);
425
426
        if (!$file) {
427
            return new HTTPResponse(null, 404);
428
        }
429
430
        if (!$file->canView()) {
431
            return new HTTPResponse(null, 403);
432
        }
433
434
        $versions = Versioned::get_all_versions($class, $fileId)
435
            ->limit($this->config()->max_history_entries)
436
            ->sort('Version', 'DESC');
437
438
        $output = array();
439
        $next = array();
440
        $prev = null;
441
442
        // swap the order so we can get the version number to compare against.
443
        // i.e version 3 needs to know version 2 is the previous version
444
        $copy = $versions->map('Version', 'Version')->toArray();
445
        foreach (array_reverse($copy) as $k => $v) {
446
            if ($prev) {
447
                $next[$v] = $prev;
448
            }
449
450
            $prev = $v;
451
        }
452
453
        $_cachedMembers = array();
454
455
        /** @var File|AssetAdminFile $version */
456
        foreach ($versions as $version) {
457
            $author = null;
458
459
            if ($version->AuthorID) {
460
                if (!isset($_cachedMembers[$version->AuthorID])) {
461
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
462
                        ->byID($version->AuthorID);
463
                }
464
465
                $author = $_cachedMembers[$version->AuthorID];
466
            }
467
468
            if ($version->canView()) {
469
                if (isset($next[$version->Version])) {
470
                    $summary = $version->humanizedChanges(
471
                        $version->Version,
472
                        $next[$version->Version]
473
                    );
474
475
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
476
                    // generic message
477
                    if (!$summary) {
478
                        $summary = _t(__CLASS__.'.SAVEDFILE', "Saved file");
479
                    }
480
                } else {
481
                    $summary = _t(__CLASS__.'.UPLOADEDFILE', "Uploaded file");
482
                }
483
484
                $output[] = array(
485
                    'versionid' => $version->Version,
486
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
487
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
488
                    'status' => ($version->WasPublished) ? _t(__CLASS__.'.PUBLISHED', 'Published') : '',
489
                    'author' => ($author)
490
                        ? $author->Name
491
                        : _t(__CLASS__.'.UNKNOWN', "Unknown"),
492
                    'summary' => ($summary)
493
                        ? $summary
494
                        : _t(__CLASS__.'.NOSUMMARY', "No summary available")
495
                );
496
            }
497
        }
498
499
        return
500
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
501
    }
502
503
    /**
504
     * Redirects 3.x style detail links to new 4.x style routing.
505
     *
506
     * @param HTTPRequest $request
507
     */
508
    public function legacyRedirectForEditView($request)
509
    {
510
        $fileID = $request->param('FileID');
511
        /** @var File $file */
512
        $file = File::get()->byID($fileID);
513
        $link = $this->getFileEditLink($file) ?: $this->Link();
514
        $this->redirect($link);
515
    }
516
517
    /**
518
     * Given a file return the CMS link to edit it
519
     *
520
     * @param File $file
521
     * @return string
522
     */
523
    public function getFileEditLink($file)
524
    {
525
        if (!$file || !$file->isInDB()) {
526
            return null;
527
        }
528
529
        return Controller::join_links(
530
            $this->Link('show'),
531
            $file->ParentID,
532
            'edit',
533
            $file->ID
534
        );
535
    }
536
537
    /**
538
     * Get an asset renamer for the given filename.
539
     *
540
     * @param string $filename Path name
541
     * @return AssetNameGenerator
542
     */
543
    protected function getNameGenerator($filename)
544
    {
545
        return Injector::inst()
546
            ->createWithArgs(AssetNameGenerator::class, array($filename));
547
    }
548
549
    /**
550
     * @todo Implement on client
551
     *
552
     * @param bool $unlinked
553
     * @return ArrayList
554
     */
555
    public function breadcrumbs($unlinked = false)
556
    {
557
        return null;
558
    }
559
560
561
    /**
562
     * Don't include class namespace in auto-generated CSS class
563
     */
564
    public function baseCSSClasses()
565
    {
566
        return 'AssetAdmin LeftAndMain';
567
    }
568
569
    public function providePermissions()
570
    {
571
        return array(
572
            "CMS_ACCESS_AssetAdmin" => array(
573
                'name' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS', "Access to '{title}' section", array(
574
                    'title' => static::menu_title()
575
                )),
576
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
577
            )
578
        );
579
    }
580
581
    /**
582
     * Build a form scaffolder for this model
583
     *
584
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
585
     *
586
     * @param File $file
587
     * @return FormFactory
588
     */
589
    public function getFormFactory(File $file)
590
    {
591
        // Get service name based on file class
592
        $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...
593
        if ($file instanceof Folder) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
594
            $name = FolderFormFactory::class;
595
        } elseif ($file instanceof Image) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Image does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
596
            $name = ImageFormFactory::class;
597
        } else {
598
            $name = FileFormFactory::class;
599
        }
600
        return Injector::inst()->get($name);
601
    }
602
603
    /**
604
     * The form is used to generate a form schema,
605
     * as well as an intermediary object to process data through API endpoints.
606
     * Since it's used directly on API endpoints, it does not have any form actions.
607
     * It handles both {@link File} and {@link Folder} records.
608
     *
609
     * @param int $id
610
     * @return Form
611
     */
612
    public function getFileEditForm($id)
613
    {
614
        return $this->getAbstractFileForm($id, 'fileEditForm');
615
    }
616
617
    /**
618
     * Get file edit form
619
     *
620
     * @param HTTPRequest $request
621
     * @return Form
622
     */
623 View Code Duplication
    public function fileEditForm($request = null)
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...
624
    {
625
        // Get ID either from posted back value, or url parameter
626
        if (!$request) {
627
            $this->httpError(400);
628
            return null;
629
        }
630
        $id = $request->param('ID');
631
        if (!$id) {
632
            $this->httpError(400);
633
            return null;
634
        }
635
        return $this->getFileEditForm($id);
636
    }
637
638
    /**
639
     * The form is used to generate a form schema,
640
     * as well as an intermediary object to process data through API endpoints.
641
     * Since it's used directly on API endpoints, it does not have any form actions.
642
     * It handles both {@link File} and {@link Folder} records.
643
     *
644
     * @param int $id
645
     * @return Form
646
     */
647
    public function getFileInsertForm($id)
648
    {
649
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT_MEDIA ]);
650
    }
651
652
    /**
653
     * Get file insert media form
654
     *
655
     * @param HTTPRequest $request
656
     * @return Form
657
     */
658 View Code Duplication
    public function fileInsertForm($request = null)
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...
659
    {
660
        // Get ID either from posted back value, or url parameter
661
        if (!$request) {
662
            $this->httpError(400);
663
            return null;
664
        }
665
        $id = $request->param('ID');
666
        if (!$id) {
667
            $this->httpError(400);
668
            return null;
669
        }
670
        return $this->getFileInsertForm($id);
671
    }
672
673
    /**
674
     * Similar to {@see AssetAdmin::getFileInsertForm()} but with Update button
675
     * instead of Insert button.
676
     *
677
     * @param int $id
678
     * @return Form
679
     */
680
    public function getFileUpdateform($id)
681
    {
682
        return $this->getAbstractFileForm($id, 'fileUpdateForm', [ 'Type' => AssetFormFactory::TYPE_UPDATE_MEDIA ]);
683
    }
684
685
    /**
686
     * Get file update media form
687
     *
688
     * @param HTTPRequest $request
689
     * @return Form
690
     */
691 View Code Duplication
    public function fileUpdateForm($request = null)
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...
692
    {
693
        // Get ID either from posted back value, or url parameter
694
        if (!$request) {
695
            $this->httpError(400);
696
            return null;
697
        }
698
        $id = $request->param('ID');
699
        if (!$id) {
700
            $this->httpError(400);
701
            return null;
702
        }
703
        return $this->getFileUpdateform($id);
704
    }
705
706
    /**
707
     * The form used to generate a form schema, since it's used directly on API endpoints,
708
     * it does not have any form actions.
709
     *
710
     * @param $id
711
     * @return Form
712
     */
713
    public function getFileEditorLinkForm($id)
714
    {
715
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT_LINK ]);
716
    }
717
718
    /**
719
     * Get the file insert link form
720
     *
721
     * @param HTTPRequest $request
722
     * @return Form
723
     */
724 View Code Duplication
    public function fileEditorLinkForm($request = null)
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...
725
    {
726
        // Get ID either from posted back value, or url parameter
727
        if (!$request) {
728
            $this->httpError(400);
729
            return null;
730
        }
731
        $id = $request->param('ID');
732
        if (!$id) {
733
            $this->httpError(400);
734
            return null;
735
        }
736
        return $this->getFileInsertForm($id);
737
    }
738
739
    /**
740
     * The form used to generate a form schema, since it's used directly on API endpoints,
741
     * it does not have any form actions.
742
     *
743
     * @param $id
744
     * @return Form
745
     */
746
    public function getFileUpdateEditorLinkForm($id)
747
    {
748
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_UPDATE_LINK ]);
749
    }
750
751
    /**
752
     * Get the file update link form
753
     *
754
     * @param HTTPRequest $request
755
     * @return Form
756
     */
757 View Code Duplication
    public function fileUpdateEditorLinkForm($request = null)
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...
758
    {
759
        // Get ID either from posted back value, or url parameter
760
        if (!$request) {
761
            $this->httpError(400);
762
            return null;
763
        }
764
        $id = $request->param('ID');
765
        if (!$id) {
766
            $this->httpError(400);
767
            return null;
768
        }
769
        return $this->getFileUpdateForm($id);
770
    }
771
772
    /**
773
     * Abstract method for generating a form for a file
774
     *
775
     * @param int $id Record ID
776
     * @param string $name Form name
777
     * @param array $context Form context
778
     * @return Form
779
     */
780
    protected function getAbstractFileForm($id, $name, $context = [])
781
    {
782
        /** @var File $file */
783
        $file = File::get()->byID($id);
784
785
        if (!$file) {
786
            $this->httpError(404);
787
            return null;
788
        }
789
790 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...
791
            $this->httpError(403, _t(
792
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
793
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
794
                '',
795
                ['ObjectTitle' => $file->i18n_singular_name()]
796
            ));
797
            return null;
798
        }
799
800
        // Pass to form factory
801
        $augmentedContext = array_merge($context, ['Record' => $file]);
802
        $scaffolder = $this->getFormFactory($file);
803
        $form = $scaffolder->getForm($this, $name, $augmentedContext);
804
805
        // Set form action handler with ID included
806
        $form->setRequestHandler(
807
            LeftAndMainFormRequestHandler::create($form, [ $id ])
808
        );
809
810
        // Configure form to respond to validation errors with form schema
811
        // if requested via react.
812
        $form->setValidationResponseCallback(function (ValidationResult $error) use ($form, $id, $name) {
813
            $schemaId = Controller::join_links($this->Link('schema'), $name, $id);
814
            return $this->getSchemaResponse($schemaId, $form, $error);
815
        });
816
817
        return $form;
818
    }
819
820
    /**
821
     * Get form for selecting a file
822
     *
823
     * @return Form
824
     */
825
    public function fileSelectForm()
826
    {
827
        // Get ID either from posted back value, or url parameter
828
        $request = $this->getRequest();
829
        $id = $request->param('ID') ?: $request->postVar('ID');
830
        return $this->getFileSelectForm($id);
831
    }
832
833
    /**
834
     * Get form for selecting a file
835
     *
836
     * @param int $id ID of the record being selected
837
     * @return Form
838
     */
839
    public function getFileSelectForm($id)
840
    {
841
        return $this->getAbstractFileForm($id, 'fileSelectForm', [ 'Type' => AssetFormFactory::TYPE_SELECT ]);
842
    }
843
844
    /**
845
     * @param array $context
846
     * @return Form
847
     * @throws InvalidArgumentException
848
     */
849
    public function getFileHistoryForm($context)
850
    {
851
        // Check context
852
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
853
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
854
        }
855
        $id = $context['RecordID'];
856
        $versionId = $context['RecordVersion'];
857
        if (!$id || !$versionId) {
858
            return $this->httpError(404);
859
        }
860
861
        /** @var File $file */
862
        $file = Versioned::get_version(File::class, $id, $versionId);
863
        if (!$file) {
864
            return $this->httpError(404);
865
        }
866
867 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...
868
            $this->httpError(403, _t(
869
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
870
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
871
                '',
872
                ['ObjectTitle' => $file->i18n_singular_name()]
873
            ));
874
            return null;
875
        }
876
877
        $effectiveContext = array_merge($context, ['Record' => $file]);
878
        /** @var FormFactory $scaffolder */
879
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
880
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
881
882
        // Set form handler with ID / VersionID
883
        $form->setRequestHandler(
884
            LeftAndMainFormRequestHandler::create($form, [ $id, $versionId ])
885
        );
886
887
        // Configure form to respond to validation errors with form schema
888
        // if requested via react.
889 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...
890
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
891
            return $this->getSchemaResponse($schemaId, $form, $errors);
892
        });
893
894
        return $form;
895
    }
896
897
    /**
898
     * Gets a JSON schema representing the current edit form.
899
     *
900
     * WARNING: Experimental API.
901
     *
902
     * @param HTTPRequest $request
903
     * @return HTTPResponse
904
     */
905
    public function schema($request)
906
    {
907
        $formName = $request->param('FormName');
908
        if ($formName !== 'fileHistoryForm') {
909
            return parent::schema($request);
910
        }
911
912
        // Get schema for history form
913
        // @todo Eventually all form scaffolding will be based on context rather than record ID
914
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
915
        $itemID = $request->param('ItemID');
916
        $version = $request->param('OtherItemID');
917
        $form = $this->getFileHistoryForm([
918
            'RecordID' => $itemID,
919
            'RecordVersion' => $version,
920
        ]);
921
922
        // Respond with this schema
923
        $response = $this->getResponse();
924
        $response->addHeader('Content-Type', 'application/json');
925
        $schemaID = $this->getRequest()->getURL();
926
        return $this->getSchemaResponse($schemaID, $form);
927
    }
928
929
    /**
930
     * Get file history form
931
     *
932
     * @param HTTPRequest $request
933
     * @return Form
934
     */
935
    public function fileHistoryForm($request = null)
936
    {
937
        // Get ID either from posted back value, or url parameter
938
        if (!$request) {
939
            $this->httpError(400);
940
            return null;
941
        }
942
        $id = $request->param('ID');
943
        if (!$id) {
944
            $this->httpError(400);
945
            return null;
946
        }
947
        $versionID = $request->param('VersionID');
948
        if (!$versionID) {
949
            $this->httpError(400);
950
            return null;
951
        }
952
        $form = $this->getFileHistoryForm([
953
            'RecordID' => $id,
954
            'RecordVersion' => $versionID,
955
        ]);
956
        return $form;
957
    }
958
959
    /**
960
     * @param array $data
961
     * @param Form $form
962
     * @return HTTPResponse
963
     */
964
    public function createfolder($data, $form)
965
    {
966
        $parentID = isset($data['ParentID']) ? intval($data['ParentID']) : 0;
967
        $data['Parent'] = null;
968
        if ($parentID) {
969
            $parent = Folder::get()->byID($parentID);
970
            if (!$parent) {
971
                throw new \InvalidArgumentException(sprintf(
972
                    '%s#%s not found',
973
                    Folder::class,
974
                    $parentID
975
                ));
976
            }
977
            $data['Parent'] = $parent;
978
        }
979
980
        // Check permission
981
        if (!Folder::singleton()->canCreate(Security::getCurrentUser(), $data)) {
0 ignored issues
show
Bug introduced by
The method getCurrentUser() does not seem to exist on object<SilverStripe\Security\Security>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
982
            throw new \InvalidArgumentException(sprintf(
983
                '%s create not allowed',
984
                Folder::class
985
            ));
986
        }
987
988
        $folder = Folder::create();
989
        $form->saveInto($folder);
990
        $folder->write();
991
992
        $createForm = $this->getFolderCreateForm($folder->ID);
993
994
        // Return the record data in the same response as the schema to save a postback
995
        $schemaData = ['record' => $this->getObjectFromData($folder)];
996
        $schemaId = Controller::join_links($this->Link('schema/folderCreateForm'), $folder->ID);
997
        return $this->getSchemaResponse($schemaId, $createForm, null, $schemaData);
998
    }
999
1000
    /**
1001
     * @param array $data
1002
     * @param Form $form
1003
     * @return HTTPResponse
1004
     */
1005
    public function save($data, $form)
1006
    {
1007
        return $this->saveOrPublish($data, $form, false);
1008
    }
1009
1010
    /**
1011
     * @param array $data
1012
     * @param Form $form
1013
     * @return HTTPResponse
1014
     */
1015
    public function publish($data, $form)
1016
    {
1017
        return $this->saveOrPublish($data, $form, true);
1018
    }
1019
1020
    /**
1021
     * Update thisrecord
1022
     *
1023
     * @param array $data
1024
     * @param Form $form
1025
     * @param bool $doPublish
1026
     * @return HTTPResponse
1027
     */
1028
    protected function saveOrPublish($data, $form, $doPublish = false)
1029
    {
1030 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...
1031
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1032
                ->addHeader('Content-Type', 'application/json');
1033
        }
1034
1035
        $id = (int) $data['ID'];
1036
        /** @var File $record */
1037
        $record = DataObject::get_by_id(File::class, $id);
1038
1039
        if (!$record) {
1040
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1041
                ->addHeader('Content-Type', 'application/json');
1042
        }
1043
1044
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
1045
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1046
                ->addHeader('Content-Type', 'application/json');
1047
        }
1048
1049
        // check File extension
1050
        if (!empty($data['FileFilename'])) {
1051
            $extension = File::get_file_extension($data['FileFilename']);
1052
            $newClass = File::get_class_for_file_extension($extension);
1053
1054
            // if the class has changed, cast it to the proper class
1055
            if ($record->getClassName() !== $newClass) {
1056
                $record = $record->newClassInstance($newClass);
1057
1058
                // update the allowed category for the new file extension
1059
                $category = File::get_app_category($extension);
1060
                $record->File->setAllowedCategories($category);
1061
            }
1062
        }
1063
1064
        $form->saveInto($record);
1065
        $record->write();
1066
1067
        // Publish this record and owned objects
1068
        if ($doPublish) {
1069
            $record->publishRecursive();
1070
        }
1071
        // regenerate form, so that it constants/literals on the form are updated
1072
        $form = $this->getFileEditForm($record->ID);
1073
1074
        // Note: Force return of schema / state in success result
1075
        return $this->getRecordUpdatedResponse($record, $form);
0 ignored issues
show
Bug introduced by
It seems like $form defined by $this->getFileEditForm($record->ID) on line 1072 can be null; however, SilverStripe\AssetAdmin\...RecordUpdatedResponse() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1076
    }
1077
1078
    public function unpublish($data, $form)
1079
    {
1080 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...
1081
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1082
                ->addHeader('Content-Type', 'application/json');
1083
        }
1084
1085
        $id = (int) $data['ID'];
1086
        /** @var File $record */
1087
        $record = DataObject::get_by_id(File::class, $id);
1088
1089
        if (!$record) {
1090
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1091
                ->addHeader('Content-Type', 'application/json');
1092
        }
1093
1094
        if (!$record->canUnpublish()) {
1095
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1096
                ->addHeader('Content-Type', 'application/json');
1097
        }
1098
1099
        $record->doUnpublish();
1100
        return $this->getRecordUpdatedResponse($record, $form);
1101
    }
1102
1103
    /**
1104
     * @param File $file
1105
     *
1106
     * @return array
1107
     */
1108
    public function getObjectFromData(File $file)
1109
    {
1110
        $object = array(
1111
            'id' => $file->ID,
1112
            'created' => $file->Created,
1113
            'lastUpdated' => $file->LastEdited,
1114
            'owner' => null,
1115
            'parent' => null,
1116
            'title' => $file->Title,
1117
            'exists' => $file->exists(), // Broken file check
1118
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1119
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
0 ignored issues
show
Bug introduced by
The class SilverStripe\Assets\Folder does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1120
            'name' => $file->Name,
1121
            'filename' => $file->Filename,
1122
            'extension' => $file->Extension,
1123
            'size' => $file->AbsoluteSize,
1124
            'url' => $file->AbsoluteURL,
1125
            'published' => $file->isPublished(),
1126
            'modified' => $file->isModifiedOnDraft(),
1127
            'draft' => $file->isOnDraftOnly(),
1128
            'canEdit' => $file->canEdit(),
1129
            'canDelete' => $file->canArchive(),
1130
        );
1131
1132
        /** @var Member $owner */
1133
        $owner = $file->Owner();
1134
1135
        if ($owner) {
1136
            $object['owner'] = array(
1137
                'id' => $owner->ID,
1138
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1139
            );
1140
        }
1141
1142
        /** @var Folder $parent */
1143
        $parent = $file->Parent();
1144
1145
        if ($parent) {
1146
            $object['parent'] = array(
1147
                'id' => $parent->ID,
1148
                'title' => $parent->Title,
1149
                'filename' => $parent->Filename,
1150
            );
1151
        }
1152
1153
        /** @var File $file */
1154
        if ($file->getIsImage()) {
1155
            // Small thumbnail
1156
            $smallWidth = UploadField::config()->uninherited('thumbnail_width');
1157
            $smallHeight = UploadField::config()->uninherited('thumbnail_height');
1158
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1159
            if ($smallThumbnail && $smallThumbnail->exists()) {
1160
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1161
            }
1162
1163
            // Large thumbnail
1164
            $width = $this->config()->get('thumbnail_width');
1165
            $height = $this->config()->get('thumbnail_height');
1166
            $thumbnail = $file->FitMax($width, $height);
1167
            if ($thumbnail && $thumbnail->exists()) {
1168
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1169
            }
1170
            $object['width'] = $file->Width;
1171
            $object['height'] = $file->Height;
1172
        } else {
1173
            $object['thumbnail'] = $file->PreviewLink();
1174
        }
1175
1176
        return $object;
1177
    }
1178
1179
    /**
1180
     * Action handler for adding pages to a campaign
1181
     *
1182
     * @param array $data
1183
     * @param Form $form
1184
     * @return DBHTMLText|HTTPResponse
1185
     */
1186
    public function addtocampaign($data, $form)
1187
    {
1188
        $id = $data['ID'];
1189
        $record = File::get()->byID($id);
1190
1191
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1192
        $results = $handler->addToCampaign($record, $data['Campaign']);
1193
        if (!isset($results)) {
1194
            return null;
1195
        }
1196
1197
        // Send extra "message" data with schema response
1198
        $extraData = ['message' => $results];
1199
        $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1200
        return $this->getSchemaResponse($schemaId, $form, null, $extraData);
1201
    }
1202
1203
    /**
1204
     * Url handler for add to campaign form
1205
     *
1206
     * @param HTTPRequest $request
1207
     * @return Form
1208
     */
1209
    public function addToCampaignForm($request)
1210
    {
1211
        // Get ID either from posted back value, or url parameter
1212
        $id = $request->param('ID') ?: $request->postVar('ID');
1213
        return $this->getAddToCampaignForm($id);
1214
    }
1215
1216
    /**
1217
     * @param int $id
1218
     * @return Form
1219
     */
1220
    public function getAddToCampaignForm($id)
1221
    {
1222
        // Get record-specific fields
1223
        $record = File::get()->byID($id);
1224
1225
        if (!$record) {
1226
            $this->httpError(404, _t(
1227
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1228
                'That {Type} couldn\'t be found',
1229
                '',
1230
                ['Type' => File::singleton()->i18n_singular_name()]
1231
            ));
1232
            return null;
1233
        }
1234 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...
1235
            $this->httpError(403, _t(
1236
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1237
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1238
                '',
1239
                ['ObjectTitle' => $record->i18n_singular_name()]
1240
            ));
1241
            return null;
1242
        }
1243
1244
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1245
        $form = $handler->Form($record);
1246
1247 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...
1248
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1249
            return $this->getSchemaResponse($schemaId, $form, $errors);
1250
        });
1251
1252
        return $form;
1253
    }
1254
1255
    /**
1256
     * @return Upload
1257
     */
1258
    protected function getUpload()
1259
    {
1260
        $upload = Upload::create();
1261
        $upload->getValidator()->setAllowedExtensions(
1262
            // filter out '' since this would be a regex problem on JS end
1263
            array_filter(File::config()->uninherited('allowed_extensions'))
1264
        );
1265
1266
        return $upload;
1267
    }
1268
1269
    /**
1270
     * Get response for successfully updated record
1271
     *
1272
     * @param File $record
1273
     * @param Form $form
1274
     * @return HTTPResponse
1275
     */
1276
    protected function getRecordUpdatedResponse($record, $form)
1277
    {
1278
        // Return the record data in the same response as the schema to save a postback
1279
        $schemaData = ['record' => $this->getObjectFromData($record)];
1280
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1281
        return $this->getSchemaResponse($schemaId, $form, null, $schemaData);
1282
    }
1283
1284
    /**
1285
     * @param HTTPRequest $request
1286
     * @return Form
1287
     */
1288
    public function folderCreateForm($request = null)
1289
    {
1290
        // Get ID either from posted back value, or url parameter
1291
        if (!$request) {
1292
            $this->httpError(400);
1293
            return null;
1294
        }
1295
        $id = $request->param('ParentID');
1296
        // Fail on null ID (but not parent)
1297
        if (!isset($id)) {
1298
            $this->httpError(400);
1299
            return null;
1300
        }
1301
        return $this->getFolderCreateForm($id);
1302
    }
1303
1304
    /**
1305
     * Returns the form to be used for creating a new folder
1306
     * @param $parentId
1307
     * @return Form
1308
     */
1309
    public function getFolderCreateForm($parentId = 0)
1310
    {
1311
        /** @var FolderCreateFormFactory $factory */
1312
        $factory = Injector::inst()->get(FolderCreateFormFactory::class);
1313
        $form = $factory->getForm($this, 'folderCreateForm', [ 'ParentID' => $parentId ]);
1314
1315
        // Set form action handler with ParentID included
1316
        $form->setRequestHandler(
1317
            LeftAndMainFormRequestHandler::create($form, [ $parentId ])
1318
        );
1319
1320
        return $form;
1321
    }
1322
1323
    /**
1324
     * Scaffold a search form.
1325
     * Note: This form does not submit to itself, but rather uses the apiReadFolder endpoint
1326
     * (to be replaced with graphql)
1327
     *
1328
     * @return Form
1329
     */
1330
    public function fileSearchForm()
1331
    {
1332
        $scaffolder = FileSearchFormFactory::singleton();
1333
        return $scaffolder->getForm($this, 'fileSearchForm', []);
1334
    }
1335
1336
    /**
1337
     * Allow search form to be accessible to schema
1338
     *
1339
     * @return Form
1340
     */
1341
    public function getFileSearchform()
1342
    {
1343
        return $this->fileSearchForm();
1344
    }
1345
}
1346