Completed
Push — master ( 51bc1d...ebadd9 )
by Ingo
02:40
created

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

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

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

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

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

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

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

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

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

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

Loading history...
56
        // Legacy redirect for SS3-style detail view
57
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
58
        // Pass all URLs to the index, for React to unpack
59
        'show/$FolderID/edit/$FileID' => 'index',
60
        // API access points with structured data
61
        'POST api/createFile' => 'apiCreateFile',
62
        'POST api/uploadFile' => 'apiUploadFile',
63
        'GET api/history' => 'apiHistory'
64
    ];
65
66
    /**
67
     * Amount of results showing on a single page.
68
     *
69
     * @config
70
     * @var int
71
     */
72
    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...
73
74
    /**
75
     * @config
76
     * @see Upload->allowedMaxFileSize
77
     * @var int
78
     */
79
    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...
80
81
    /**
82
     * @config
83
     *
84
     * @var int
85
     */
86
    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...
87
88
    /**
89
     * @var array
90
     */
91
    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...
92
        'legacyRedirectForEditView',
93
        'apiCreateFile',
94
        'apiUploadFile',
95
        'apiHistory',
96
        'fileEditForm',
97
        'fileHistoryForm',
98
        'addToCampaignForm',
99
        'fileInsertForm',
100
        'schema',
101
        'fileSelectForm',
102
        'fileSearchForm',
103
    );
104
105
    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...
106
107
    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...
108
109
    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...
110
111
    /**
112
     * Set up the controller
113
     */
114
    public function init()
115
    {
116
        parent::init();
117
118
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
119
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
120
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
121
122
        CMSBatchActionHandler::register('delete', DeleteAssets::class, Folder::class);
123
    }
124
125
    public function getClientConfig()
126
    {
127
        $baseLink = $this->Link();
128
        return array_merge(parent::getClientConfig(), [
129
            'reactRouter' => true,
130
            'createFileEndpoint' => [
131
                'url' => Controller::join_links($baseLink, 'api/createFile'),
132
                'method' => 'post',
133
                'payloadFormat' => 'urlencoded',
134
            ],
135
            'uploadFileEndpoint' => [
136
                'url' => Controller::join_links($baseLink, 'api/uploadFile'),
137
                'method' => 'post',
138
                'payloadFormat' => 'urlencoded',
139
            ],
140
            'historyEndpoint' => [
141
                'url' => Controller::join_links($baseLink, 'api/history'),
142
                'method' => 'get',
143
                'responseFormat' => 'json',
144
            ],
145
            'limit' => $this->config()->page_length,
146
            'form' => [
147
                'fileEditForm' => [
148
                    'schemaUrl' => $this->Link('schema/fileEditForm')
149
                ],
150
                'fileInsertForm' => [
151
                    'schemaUrl' => $this->Link('schema/fileInsertForm')
152
                ],
153
                'fileSelectForm' => [
154
                    'schemaUrl' => $this->Link('schema/fileSelectForm')
155
                ],
156
                'addToCampaignForm' => [
157
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
158
                ],
159
                'fileHistoryForm' => [
160
                    'schemaUrl' => $this->Link('schema/fileHistoryForm')
161
                ],
162
                'fileSearchForm' => [
163
                    'schemaUrl' => $this->Link('schema/fileSearchForm')
164
                ],
165
            ],
166
        ]);
167
    }
168
169
    /**
170
     * Creates a single file based on a form-urlencoded upload.
171
     *
172
     * @param HTTPRequest $request
173
     * @return HTTPRequest|HTTPResponse
174
     */
175
    public function apiCreateFile(HTTPRequest $request)
176
    {
177
        $data = $request->postVars();
178
        $upload = $this->getUpload();
179
180
        // CSRF check
181
        $token = SecurityToken::inst();
182 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...
183
            return new HTTPResponse(null, 400);
184
        }
185
186
        // Check parent record
187
        /** @var Folder $parentRecord */
188
        $parentRecord = null;
189 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...
190
            $parentRecord = Folder::get()->byID($data['ParentID']);
191
        }
192
        $data['Parent'] = $parentRecord;
193
194
        $tmpFile = $request->postVar('Upload');
195 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...
196
            $result = ['message' => null];
197
            $errors = $upload->getErrors();
198
            if ($message = array_shift($errors)) {
199
                $result['message'] = [
200
                    'type' => 'error',
201
                    'value' => $message,
202
                ];
203
            }
204
            return (new HTTPResponse(json_encode($result), 400))
205
                ->addHeader('Content-Type', 'application/json');
206
        }
207
208
        // TODO Allow batch uploads
209
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
210
        /** @var File $file */
211
        $file = Injector::inst()->create($fileClass);
212
213
        // check canCreate permissions
214 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...
215
            $result = ['message' => [
216
                'type' => 'error',
217
                'value' => _t(
218
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
219
                    'You do not have permission to add files'
220
                )
221
            ]];
222
            return (new HTTPResponse(json_encode($result), 403))
223
                ->addHeader('Content-Type', 'application/json');
224
        }
225
226
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
227 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...
228
            $result = ['message' => [
229
                'type' => 'error',
230
                'value' => _t(
231
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
232
                    'Failed to load file'
233
                )
234
            ]];
235
            return (new HTTPResponse(json_encode($result), 400))
236
                ->addHeader('Content-Type', 'application/json');
237
        }
238
239
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
240
        $file->write();
241
242
        $result = [$this->getObjectFromData($file)];
243
244
        return (new HTTPResponse(json_encode($result)))
245
            ->addHeader('Content-Type', 'application/json');
246
    }
247
248
    /**
249
     * Upload a new asset for a pre-existing record. Returns the asset tuple.
250
     *
251
     * @param HTTPRequest $request
252
     * @return HTTPRequest|HTTPResponse
253
     */
254
    public function apiUploadFile(HTTPRequest $request)
255
    {
256
        $data = $request->postVars();
257
        $upload = $this->getUpload();
258
259
        // CSRF check
260
        $token = SecurityToken::inst();
261 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...
262
            return new HTTPResponse(null, 400);
263
        }
264
265
        // Check parent record
266
        /** @var Folder $parentRecord */
267
        $parentRecord = null;
268 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...
269
            $parentRecord = Folder::get()->byID($data['ParentID']);
270
        }
271
272
        $tmpFile = $data['Upload'];
273 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...
274
            $result = ['message' => null];
275
            $errors = $upload->getErrors();
276
            if ($message = array_shift($errors)) {
277
                $result['message'] = [
278
                    'type' => 'error',
279
                    'value' => $message,
280
                ];
281
            }
282
            return (new HTTPResponse(json_encode($result), 400))
283
                ->addHeader('Content-Type', 'application/json');
284
        }
285
286
        $folder = $parentRecord ? $parentRecord->getFilename() : '/';
287
288
        try {
289
            $tuple = $upload->load($tmpFile, $folder);
290
        } catch (Exception $e) {
291
            $result = [
292
                'message' => [
293
                    'type' => 'error',
294
                    'value' => $e->getMessage(),
295
                ]
296
            ];
297
            return (new HTTPResponse(json_encode($result), 400))
298
                ->addHeader('Content-Type', 'application/json');
299
        }
300
301
        if ($upload->isError()) {
302
            $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...
303
                'type' => 'error',
304
                'value' => implode(' ' . PHP_EOL, $upload->getErrors()),
305
            ];
306
            return (new HTTPResponse(json_encode($result), 400))
307
                ->addHeader('Content-Type', 'application/json');
308
        }
309
310
        $tuple['Name'] = basename($tuple['Filename']);
311
        return (new HTTPResponse(json_encode($tuple)))
312
            ->addHeader('Content-Type', 'application/json');
313
    }
314
315
    /**
316
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
317
     *
318
     * @param HTTPRequest $request
319
     * @return HTTPResponse
320
     */
321
    public function apiHistory(HTTPRequest $request)
322
    {
323
        // CSRF check not required as the GET request has no side effects.
324
        $fileId = $request->getVar('fileId');
325
326
        if (!$fileId || !is_numeric($fileId)) {
327
            return new HTTPResponse(null, 400);
328
        }
329
330
        $class = File::class;
331
        $file = DataObject::get($class)->byID($fileId);
332
333
        if (!$file) {
334
            return new HTTPResponse(null, 404);
335
        }
336
337
        if (!$file->canView()) {
338
            return new HTTPResponse(null, 403);
339
        }
340
341
        $versions = Versioned::get_all_versions($class, $fileId)
342
            ->limit($this->config()->max_history_entries)
343
            ->sort('Version', 'DESC');
344
345
        $output = array();
346
        $next = array();
347
        $prev = null;
348
349
        // swap the order so we can get the version number to compare against.
350
        // i.e version 3 needs to know version 2 is the previous version
351
        $copy = $versions->map('Version', 'Version')->toArray();
352
        foreach (array_reverse($copy) as $k => $v) {
353
            if ($prev) {
354
                $next[$v] = $prev;
355
            }
356
357
            $prev = $v;
358
        }
359
360
        $_cachedMembers = array();
361
362
        /** @var File $version */
363
        foreach ($versions as $version) {
364
            $author = null;
365
366
            if ($version->AuthorID) {
367
                if (!isset($_cachedMembers[$version->AuthorID])) {
368
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
369
                        ->byID($version->AuthorID);
370
                }
371
372
                $author = $_cachedMembers[$version->AuthorID];
373
            }
374
375
            if ($version->canView()) {
376
                if (isset($next[$version->Version])) {
377
                    $summary = $version->humanizedChanges(
378
                        $version->Version,
379
                        $next[$version->Version]
380
                    );
381
382
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
383
                    // generic message
384
                    if (!$summary) {
385
                        $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.SAVEDFILE', "Saved file");
386
                    }
387
                } else {
388
                    $summary = _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UPLOADEDFILE', "Uploaded file");
389
                }
390
391
                $output[] = array(
392
                    'versionid' => $version->Version,
393
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
394
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
395
                    'status' => ($version->WasPublished) ? _t('File.PUBLISHED', 'Published') : '',
396
                    'author' => ($author)
397
                        ? $author->Name
398
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.UNKNOWN', "Unknown"),
399
                    'summary' => ($summary)
400
                        ? $summary
401
                        : _t('SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.NOSUMMARY', "No summary available")
402
                );
403
            }
404
        }
405
406
        return
407
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
408
    }
409
410
    /**
411
     * Redirects 3.x style detail links to new 4.x style routing.
412
     *
413
     * @param HTTPRequest $request
414
     */
415
    public function legacyRedirectForEditView($request)
416
    {
417
        $fileID = $request->param('FileID');
418
        /** @var File $file */
419
        $file = File::get()->byID($fileID);
420
        $link = $this->getFileEditLink($file) ?: $this->Link();
421
        $this->redirect($link);
422
    }
423
424
    /**
425
     * Given a file return the CMS link to edit it
426
     *
427
     * @param File $file
428
     * @return string
429
     */
430
    public function getFileEditLink($file)
431
    {
432
        if (!$file || !$file->isInDB()) {
433
            return null;
434
        }
435
436
        return Controller::join_links(
437
            $this->Link('show'),
438
            $file->ParentID,
439
            'edit',
440
            $file->ID
441
        );
442
    }
443
444
    /**
445
     * Get an asset renamer for the given filename.
446
     *
447
     * @param  string             $filename Path name
448
     * @return AssetNameGenerator
449
     */
450
    protected function getNameGenerator($filename)
451
    {
452
        return Injector::inst()
453
            ->createWithArgs('AssetNameGenerator', array($filename));
454
    }
455
456
    /**
457
     * @todo Implement on client
458
     *
459
     * @param bool $unlinked
460
     * @return ArrayList
461
     */
462
    public function breadcrumbs($unlinked = false)
463
    {
464
        return null;
465
    }
466
467
468
    /**
469
     * Don't include class namespace in auto-generated CSS class
470
     */
471
    public function baseCSSClasses()
472
    {
473
        return 'AssetAdmin LeftAndMain';
474
    }
475
476
    public function providePermissions()
477
    {
478
        return array(
479
            "CMS_ACCESS_AssetAdmin" => array(
480
                '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...
481
                    'title' => static::menu_title()
482
                )),
483
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
484
            )
485
        );
486
    }
487
488
    /**
489
     * Build a form scaffolder for this model
490
     *
491
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
492
     *
493
     * @param File $file
494
     * @return FormFactory
495
     */
496
    public function getFormFactory(File $file)
497
    {
498
        // Get service name based on file class
499
        $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...
500
        if ($file instanceof Folder) {
501
            $name = FolderFormFactory::class;
502
        } elseif ($file instanceof Image) {
503
            $name = ImageFormFactory::class;
504
        } else {
505
            $name = FileFormFactory::class;
506
        }
507
        return Injector::inst()->get($name);
508
    }
509
510
    /**
511
     * The form is used to generate a form schema,
512
     * as well as an intermediary object to process data through API endpoints.
513
     * Since it's used directly on API endpoints, it does not have any form actions.
514
     * It handles both {@link File} and {@link Folder} records.
515
     *
516
     * @param int $id
517
     * @return Form
518
     */
519
    public function getFileEditForm($id)
520
    {
521
        return $this->getAbstractFileForm($id, 'fileEditForm');
522
    }
523
524
    /**
525
     * Get file edit form
526
     *
527
     * @return Form
528
     */
529 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...
530
    {
531
        // Get ID either from posted back value, or url parameter
532
        $request = $this->getRequest();
533
        $id = $request->param('ID') ?: $request->postVar('ID');
534
        return $this->getFileEditForm($id);
535
    }
536
537
    /**
538
     * The form is used to generate a form schema,
539
     * as well as an intermediary object to process data through API endpoints.
540
     * Since it's used directly on API endpoints, it does not have any form actions.
541
     * It handles both {@link File} and {@link Folder} records.
542
     *
543
     * @param int $id
544
     * @return Form
545
     */
546
    public function getFileInsertForm($id)
547
    {
548
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT ]);
549
    }
550
551
    /**
552
     * Get file insert form
553
     *
554
     * @return Form
555
     */
556 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...
557
    {
558
        // Get ID either from posted back value, or url parameter
559
        $request = $this->getRequest();
560
        $id = $request->param('ID') ?: $request->postVar('ID');
561
        return $this->getFileInsertForm($id);
562
    }
563
564
    /**
565
     * Abstract method for generating a form for a file
566
     *
567
     * @param int $id Record ID
568
     * @param string $name Form name
569
     * @param array $context Form context
570
     * @return Form
571
     */
572
    protected function getAbstractFileForm($id, $name, $context = [])
573
    {
574
        /** @var File $file */
575
        $file = File::get()->byID($id);
576
577 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...
578
            $this->httpError(403, _t(
579
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
580
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
581
                '',
582
                ['ObjectTitle' => $file->i18n_singular_name()]
583
            ));
584
            return null;
585
        }
586
587
        // Pass to form factory
588
        $augmentedContext = array_merge($context, ['Record' => $file]);
589
        $scaffolder = $this->getFormFactory($file);
590
        $form = $scaffolder->getForm($this, $name, $augmentedContext);
591
592
        // Configure form to respond to validation errors with form schema
593
        // if requested via react.
594
        $form->setValidationResponseCallback(function (ValidationResult $error) use ($form, $id, $name) {
595
            $schemaId = Controller::join_links($this->Link('schema'), $name, $id);
596
            return $this->getSchemaResponse($schemaId, $form, $error);
597
        });
598
599
        return $form;
600
    }
601
602
    /**
603
     * Get form for selecting a file
604
     *
605
     * @return Form
606
     */
607
    public function fileSelectForm()
608
    {
609
        // Get ID either from posted back value, or url parameter
610
        $request = $this->getRequest();
611
        $id = $request->param('ID') ?: $request->postVar('ID');
612
        return $this->getFileSelectForm($id);
613
    }
614
615
    /**
616
     * Get form for selecting a file
617
     *
618
     * @param int $id ID of the record being selected
619
     * @return Form
620
     */
621
    public function getFileSelectForm($id)
622
    {
623
        return $this->getAbstractFileForm($id, 'fileSelectForm', [ 'Type' => AssetFormFactory::TYPE_SELECT ]);
624
    }
625
626
    /**
627
     * @param array $context
628
     * @return Form
629
     * @throws InvalidArgumentException
630
     */
631
    public function getFileHistoryForm($context)
632
    {
633
        // Check context
634
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
635
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
636
        }
637
        $id = $context['RecordID'];
638
        $versionId = $context['RecordVersion'];
639
        if (!$id || !$versionId) {
640
            return $this->httpError(404);
641
        }
642
643
        /** @var File $file */
644
        $file = Versioned::get_version(File::class, $id, $versionId);
645
        if (!$file) {
646
            return $this->httpError(404);
647
        }
648
649 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...
650
            $this->httpError(403, _t(
651
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
652
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
653
                '',
654
                ['ObjectTitle' => $file->i18n_singular_name()]
655
            ));
656
            return null;
657
        }
658
659
        $effectiveContext = array_merge($context, ['Record' => $file]);
660
        /** @var FormFactory $scaffolder */
661
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
662
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
663
664
        // Configure form to respond to validation errors with form schema
665
        // if requested via react.
666 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...
667
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
668
            return $this->getSchemaResponse($schemaId, $form, $errors);
669
        });
670
671
        return $form;
672
    }
673
674
    /**
675
     * Gets a JSON schema representing the current edit form.
676
     *
677
     * WARNING: Experimental API.
678
     *
679
     * @param HTTPRequest $request
680
     * @return HTTPResponse
681
     */
682
    public function schema($request)
683
    {
684
        $formName = $request->param('FormName');
685
        if ($formName !== 'fileHistoryForm') {
686
            return parent::schema($request);
687
        }
688
689
        // Get schema for history form
690
        // @todo Eventually all form scaffolding will be based on context rather than record ID
691
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
692
        $itemID = $request->param('ItemID');
693
        $version = $request->param('OtherItemID');
694
        $form = $this->getFileHistoryForm([
695
            'RecordID' => $itemID,
696
            'RecordVersion' => $version,
697
        ]);
698
699
        // Respond with this schema
700
        $response = $this->getResponse();
701
        $response->addHeader('Content-Type', 'application/json');
702
        $schemaID = $this->getRequest()->getURL();
703
        return $this->getSchemaResponse($schemaID, $form);
704
    }
705
706
    /**
707
     * Get file history form
708
     *
709
     * @return Form
710
     */
711
    public function fileHistoryForm()
712
    {
713
        $request = $this->getRequest();
714
        $id = $request->param('ID') ?: $request->postVar('ID');
715
        $version = $request->param('OtherID') ?: $request->postVar('Version');
716
        $form = $this->getFileHistoryForm([
717
            'RecordID' => $id,
718
            'RecordVersion' => $version,
719
        ]);
720
        return $form;
721
    }
722
723
    /**
724
     * @param array $data
725
     * @param Form $form
726
     * @return HTTPResponse
727
     */
728
    public function save($data, $form)
729
    {
730
        return $this->saveOrPublish($data, $form, false);
731
    }
732
733
    /**
734
     * @param array $data
735
     * @param Form $form
736
     * @return HTTPResponse
737
     */
738
    public function publish($data, $form)
739
    {
740
        return $this->saveOrPublish($data, $form, true);
741
    }
742
743
    /**
744
     * Update thisrecord
745
     *
746
     * @param array $data
747
     * @param Form $form
748
     * @param bool $doPublish
749
     * @return HTTPResponse
750
     */
751
    protected function saveOrPublish($data, $form, $doPublish = false)
752
    {
753 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...
754
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
755
                ->addHeader('Content-Type', 'application/json');
756
        }
757
758
        $id = (int) $data['ID'];
759
        /** @var File $record */
760
        $record = DataObject::get_by_id(File::class, $id);
761
762
        if (!$record) {
763
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
764
                ->addHeader('Content-Type', 'application/json');
765
        }
766
767
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
768
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
769
                ->addHeader('Content-Type', 'application/json');
770
        }
771
772
        // check File extension
773
        if (!empty($data['FileFilename'])) {
774
            $extension = File::get_file_extension($data['FileFilename']);
775
            $newClass = File::get_class_for_file_extension($extension);
776
777
            // if the class has changed, cast it to the proper class
778
            if ($record->getClassName() !== $newClass) {
779
                $record = $record->newClassInstance($newClass);
780
781
                // update the allowed category for the new file extension
782
                $category = File::get_app_category($extension);
783
                $record->File->setAllowedCategories($category);
784
            }
785
        }
786
787
        $form->saveInto($record);
788
        $record->write();
789
790
        // Publish this record and owned objects
791
        if ($doPublish) {
792
            $record->publishRecursive();
793
        }
794
795
        // Note: Force return of schema / state in success result
796
        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...
797
    }
798
799
    public function unpublish($data, $form)
800
    {
801 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...
802
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
803
                ->addHeader('Content-Type', 'application/json');
804
        }
805
806
        $id = (int) $data['ID'];
807
        /** @var File $record */
808
        $record = DataObject::get_by_id(File::class, $id);
809
810
        if (!$record) {
811
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
812
                ->addHeader('Content-Type', 'application/json');
813
        }
814
815
        if (!$record->canUnpublish()) {
816
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
817
                ->addHeader('Content-Type', 'application/json');
818
        }
819
820
        $record->doUnpublish();
821
        return $this->getRecordUpdatedResponse($record, $form);
822
    }
823
824
    /**
825
     * @param File $file
826
     *
827
     * @return array
828
     */
829
    public function getObjectFromData(File $file)
830
    {
831
        $object = array(
832
            'id' => $file->ID,
833
            'created' => $file->Created,
834
            'lastUpdated' => $file->LastEdited,
835
            'owner' => null,
836
            'parent' => null,
837
            'title' => $file->Title,
838
            'exists' => $file->exists(), // Broken file check
839
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
840
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
841
            'name' => $file->Name,
842
            'filename' => $file->Filename,
843
            '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...
844
            'size' => $file->AbsoluteSize,
845
            'url' => $file->AbsoluteURL,
846
            'published' => $file->isPublished(),
847
            'modified' => $file->isModifiedOnDraft(),
848
            'draft' => $file->isOnDraftOnly(),
849
            'canEdit' => $file->canEdit(),
850
            'canDelete' => $file->canArchive(),
851
        );
852
853
        /** @var Member $owner */
854
        $owner = $file->Owner();
855
856
        if ($owner) {
857
            $object['owner'] = array(
858
                'id' => $owner->ID,
859
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
860
            );
861
        }
862
863
        /** @var Folder $parent */
864
        $parent = $file->Parent();
865
866
        if ($parent) {
867
            $object['parent'] = array(
868
                'id' => $parent->ID,
869
                'title' => $parent->Title,
870
                'filename' => $parent->Filename,
871
            );
872
        }
873
874
        /** @var File $file */
875
        if ($file->getIsImage()) {
876
            // Small thumbnail
877
            $smallWidth = UploadField::config()->get('thumbnail_width');
878
            $smallHeight = UploadField::config()->get('thumbnail_height');
879
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
880
            if ($smallThumbnail && $smallThumbnail->exists()) {
881
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
882
            }
883
884
            // Large thumbnail
885
            $width = $this->config()->get('thumbnail_width');
886
            $height = $this->config()->get('thumbnail_height');
887
            $thumbnail = $file->FitMax($width, $height);
888
            if ($thumbnail && $thumbnail->exists()) {
889
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
890
            }
891
            $object['width'] = $file->Width;
892
            $object['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...
893
        } else {
894
            $object['thumbnail'] = $file->PreviewLink();
895
        }
896
897
        return $object;
898
    }
899
900
    /**
901
     * Action handler for adding pages to a campaign
902
     *
903
     * @param array $data
904
     * @param Form $form
905
     * @return DBHTMLText|HTTPResponse
906
     */
907
    public function addtocampaign($data, $form)
908
    {
909
        $id = $data['ID'];
910
        $record = File::get()->byID($id);
911
912
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
913
        $results = $handler->addToCampaign($record, $data['Campaign']);
0 ignored issues
show
Bug introduced by
It seems like $record defined by \SilverStripe\Assets\File::get()->byID($id) on line 910 can be null; however, SilverStripe\Admin\AddTo...andler::addToCampaign() 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...
914
        if (!isset($results)) {
915
            return null;
916
        }
917
918
        // Send extra "message" data with schema response
919
        $extraData = ['message' => $results];
920
        $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
921
        return $this->getSchemaResponse($schemaId, $form, null, $extraData);
922
    }
923
924
    /**
925
     * Url handler for add to campaign form
926
     *
927
     * @param HTTPRequest $request
928
     * @return Form
929
     */
930
    public function addToCampaignForm($request)
931
    {
932
        // Get ID either from posted back value, or url parameter
933
        $id = $request->param('ID') ?: $request->postVar('ID');
934
        return $this->getAddToCampaignForm($id);
935
    }
936
937
    /**
938
     * @param int $id
939
     * @return Form
940
     */
941
    public function getAddToCampaignForm($id)
942
    {
943
        // Get record-specific fields
944
        $record = File::get()->byID($id);
945
946
        if (!$record) {
947
            $this->httpError(404, _t(
948
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
949
                'That {Type} couldn\'t be found',
950
                '',
951
                ['Type' => File::singleton()->i18n_singular_name()]
952
            ));
953
            return null;
954
        }
955 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...
956
            $this->httpError(403, _t(
957
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
958
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
959
                '',
960
                ['ObjectTitle' => $record->i18n_singular_name()]
961
            ));
962
            return null;
963
        }
964
965
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
966
        $form = $handler->Form($record);
967
968 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...
969
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
970
            return $this->getSchemaResponse($schemaId, $form, $errors);
971
        });
972
973
        return $form;
974
    }
975
976
    /**
977
     * @return Upload
978
     */
979
    protected function getUpload()
980
    {
981
        $upload = Upload::create();
982
        $upload->getValidator()->setAllowedExtensions(
983
            // filter out '' since this would be a regex problem on JS end
984
            array_filter(File::config()->get('allowed_extensions'))
985
        );
986
987
        return $upload;
988
    }
989
990
    /**
991
     * Get response for successfully updated record
992
     *
993
     * @param File $record
994
     * @param Form $form
995
     * @return HTTPResponse
996
     */
997
    protected function getRecordUpdatedResponse($record, $form)
998
    {
999
        // Return the record data in the same response as the schema to save a postback
1000
        $schemaData = ['record' => $this->getObjectFromData($record)];
1001
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1002
        return $this->getSchemaResponse($schemaId, $form, null, $schemaData);
1003
    }
1004
1005
    /**
1006
     * Scaffold a search form.
1007
     * Note: This form does not submit to itself, but rather uses the apiReadFolder endpoint
1008
     * (to be replaced with graphql)
1009
     *
1010
     * @return Form
1011
     */
1012
    public function fileSearchForm()
1013
    {
1014
        $scaffolder = FileSearchFormFactory::singleton();
1015
        return $scaffolder->getForm($this, 'fileSearchForm', []);
1016
    }
1017
1018
    /**
1019
     * Allow search form to be accessible to schema
1020
     *
1021
     * @return Form
1022
     */
1023
    public function getFileSearchform()
1024
    {
1025
        return $this->fileSearchForm();
1026
    }
1027
}
1028