Completed
Push — master ( 9fe022...710e95 )
by Damian
10:40
created

code/Controller/AssetAdmin.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
 * @skipUpgrade
48
 */
49
class AssetAdmin extends LeftAndMain implements PermissionProvider
50
{
51
    private static $url_segment = 'assets';
52
53
    private static $url_rule = '/$Action/$ID';
54
55
    private static $menu_title = 'Files';
56
57
    private static $menu_icon_class = 'font-icon-image';
58
59
    private static $tree_class = Folder::class;
60
61
    private static $url_handlers = [
62
        // Legacy redirect for SS3-style detail view
63
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
64
        // Pass all URLs to the index, for React to unpack
65
        'show/$FolderID/edit/$FileID' => 'index',
66
        // API access points with structured data
67
        'POST api/createFile' => 'apiCreateFile',
68
        'POST api/uploadFile' => 'apiUploadFile',
69
        'GET api/history' => 'apiHistory',
70
        'fileEditForm/$ID' => 'fileEditForm',
71
        'fileInsertForm/$ID' => 'fileInsertForm',
72
        'fileEditorLinkForm/$ID' => 'fileEditorLinkForm',
73
        'fileHistoryForm/$ID/$VersionID' => 'fileHistoryForm',
74
        'folderCreateForm/$ParentID' => 'folderCreateForm',
75
        'fileSelectForm/$ID' => 'fileSelectForm',
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;
85
86
    /**
87
     * @config
88
     * @see Upload->allowedMaxFileSize
89
     * @var int
90
     */
91
    private static $allowed_max_file_size;
92
93
    /**
94
     * @config
95
     *
96
     * @var int
97
     */
98
    private static $max_history_entries = 100;
99
100
    /**
101
     * @var array
102
     */
103
    private static $allowed_actions = array(
104
        'legacyRedirectForEditView',
105
        'apiCreateFile',
106
        'apiUploadFile',
107
        'apiHistory',
108
        'folderCreateForm',
109
        'fileEditForm',
110
        'fileHistoryForm',
111
        'addToCampaignForm',
112
        'fileInsertForm',
113
        'fileEditorLinkForm',
114
        'schema',
115
        'fileSelectForm',
116
        'fileSearchForm',
117
    );
118
119
    private static $required_permission_codes = 'CMS_ACCESS_AssetAdmin';
120
121
    /**
122
     * Retina thumbnail image (native size: 176)
123
     *
124
     * @config
125
     * @var int
126
     */
127
    private static $thumbnail_width = 352;
128
129
    /**
130
     * Retina thumbnail height (native size: 132)
131
     *
132
     * @config
133
     * @var int
134
     */
135
    private static $thumbnail_height = 264;
136
137
    /**
138
     * Safely limit max inline thumbnail size to 200kb
139
     *
140
     * @config
141
     * @var int
142
     */
143
    private static $max_thumbnail_bytes = 200000;
144
145
    /**
146
     * Set up the controller
147
     */
148
    public function init()
149
    {
150
        parent::init();
151
152
        $module = ModuleLoader::getModule('silverstripe/asset-admin');
153
        Requirements::add_i18n_javascript($module->getRelativeResourcePath('client/lang'), false, true);
0 ignored issues
show
The method getRelativeResourcePath() does not seem to exist on object<SilverStripe\Core\Manifest\Module>.

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...
154
        Requirements::javascript($module->getRelativeResourcePath("client/dist/js/bundle.js"));
155
        Requirements::css($module->getRelativeResourcePath("client/dist/styles/bundle.css"));
156
157
        CMSBatchActionHandler::register('delete', DeleteAssets::class, Folder::class);
158
    }
159
160
    public function getClientConfig()
161
    {
162
        $baseLink = $this->Link();
163
        return array_merge(parent::getClientConfig(), [
164
            'reactRouter' => true,
165
            'createFileEndpoint' => [
166
                'url' => Controller::join_links($baseLink, 'api/createFile'),
167
                'method' => 'post',
168
                'payloadFormat' => 'urlencoded',
169
            ],
170
            'uploadFileEndpoint' => [
171
                'url' => Controller::join_links($baseLink, 'api/uploadFile'),
172
                'method' => 'post',
173
                'payloadFormat' => 'urlencoded',
174
            ],
175
            'historyEndpoint' => [
176
                'url' => Controller::join_links($baseLink, 'api/history'),
177
                'method' => 'get',
178
                'responseFormat' => 'json',
179
            ],
180
            'limit' => $this->config()->page_length,
181
            'form' => [
182
                'fileEditForm' => [
183
                    'schemaUrl' => $this->Link('schema/fileEditForm')
184
                ],
185
                'fileInsertForm' => [
186
                    'schemaUrl' => $this->Link('schema/fileInsertForm')
187
                ],
188
                'remoteEditForm' => [
189
                    'schemaUrl' => LeftAndMain::singleton()
190
                        ->Link('Modals/remoteEditFormSchema'),
191
                ],
192
                'remoteCreateForm' => [
193
                    'schemaUrl' => LeftAndMain::singleton()
194
                        ->Link('methodSchema/Modals/remoteCreateForm')
195
                ],
196
                'fileSelectForm' => [
197
                    'schemaUrl' => $this->Link('schema/fileSelectForm')
198
                ],
199
                'addToCampaignForm' => [
200
                    'schemaUrl' => $this->Link('schema/addToCampaignForm')
201
                ],
202
                'fileHistoryForm' => [
203
                    'schemaUrl' => $this->Link('schema/fileHistoryForm')
204
                ],
205
                'fileSearchForm' => [
206
                    'schemaUrl' => $this->Link('schema/fileSearchForm')
207
                ],
208
                'folderCreateForm' => [
209
                    'schemaUrl' => $this->Link('schema/folderCreateForm')
210
                ],
211
                'fileEditorLinkForm' => [
212
                    'schemaUrl' => $this->Link('schema/fileEditorLinkForm'),
213
                ],
214
            ],
215
        ]);
216
    }
217
218
    /**
219
     * Creates a single file based on a form-urlencoded upload.
220
     *
221
     * @param HTTPRequest $request
222
     * @return HTTPRequest|HTTPResponse
223
     */
224
    public function apiCreateFile(HTTPRequest $request)
225
    {
226
        $data = $request->postVars();
227
228
        // When creating new files, rename on conflict
229
        $upload = $this->getUpload();
230
        $upload->setReplaceFile(false);
231
232
        // CSRF check
233
        $token = SecurityToken::inst();
234 View Code Duplication
        if (empty($data[$token->getName()]) || !$token->check($data[$token->getName()])) {
235
            return new HTTPResponse(null, 400);
236
        }
237
238
        // Check parent record
239
        /** @var Folder $parentRecord */
240
        $parentRecord = null;
241
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
242
            $parentRecord = Folder::get()->byID($data['ParentID']);
243
        }
244
        $data['Parent'] = $parentRecord;
245
246
        $tmpFile = $request->postVar('Upload');
247 View Code Duplication
        if (!$upload->validate($tmpFile)) {
248
            $result = ['message' => null];
249
            $errors = $upload->getErrors();
250
            if ($message = array_shift($errors)) {
251
                $result['message'] = [
252
                    'type' => 'error',
253
                    'value' => $message,
254
                ];
255
            }
256
            return (new HTTPResponse(json_encode($result), 400))
257
                ->addHeader('Content-Type', 'application/json');
258
        }
259
260
        // TODO Allow batch uploads
261
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
262
        /** @var File $file */
263
        $file = Injector::inst()->create($fileClass);
264
265
        // check canCreate permissions
266 View Code Duplication
        if (!$file->canCreate(null, $data)) {
267
            $result = ['message' => [
268
                'type' => 'error',
269
                'value' => _t(
270
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.CreatePermissionDenied',
271
                    'You do not have permission to add files'
272
                )
273
            ]];
274
            return (new HTTPResponse(json_encode($result), 403))
275
                ->addHeader('Content-Type', 'application/json');
276
        }
277
278
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
279 View Code Duplication
        if (!$uploadResult) {
280
            $result = ['message' => [
281
                'type' => 'error',
282
                'value' => _t(
283
                    'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.LoadIntoFileFailed',
284
                    'Failed to load file'
285
                )
286
            ]];
287
            return (new HTTPResponse(json_encode($result), 400))
288
                ->addHeader('Content-Type', 'application/json');
289
        }
290
291
        $file->ParentID = $parentRecord ? $parentRecord->ID : 0;
292
        $file->write();
293
294
        $result = [$this->getObjectFromData($file)];
295
296
        // Don't discard pre-generated client side canvas thumbnail
297
        if ($result[0]['category'] === 'image') {
298
            unset($result[0]['thumbnail']);
299
        }
300
301
        return (new HTTPResponse(json_encode($result)))
302
            ->addHeader('Content-Type', 'application/json');
303
    }
304
305
    /**
306
     * Upload a new asset for a pre-existing record. Returns the asset tuple.
307
     *
308
     * Note that conflict resolution is as follows:
309
     *  - If uploading a file with the same extension, we simply keep the same filename,
310
     *    and overwrite any existing files (same name + sha = don't duplicate).
311
     *  - If uploading a new file with a different extension, then the filename will
312
     *    be replaced, and will be checked for uniqueness against other File dataobjects.
313
     *
314
     * @param HTTPRequest $request Request containing vars 'ID' of parent record ID,
315
     * and 'Name' as form filename value
316
     * @return HTTPRequest|HTTPResponse
317
     */
318
    public function apiUploadFile(HTTPRequest $request)
319
    {
320
        $data = $request->postVars();
321
322
        // When updating files, replace on conflict
323
        $upload = $this->getUpload();
324
        $upload->setReplaceFile(true);
325
326
        // CSRF check
327
        $token = SecurityToken::inst();
328 View Code Duplication
        if (empty($data[$token->getName()]) || !$token->check($data[$token->getName()])) {
329
            return new HTTPResponse(null, 400);
330
        }
331
        $tmpFile = $data['Upload'];
332
        if (empty($data['ID']) || empty($tmpFile['name']) || !array_key_exists('Name', $data)) {
333
            return new HTTPResponse('Invalid request', 400);
334
        }
335
336
        // Check parent record
337
        /** @var File $file */
338
        $file = File::get()->byID($data['ID']);
339
        if (!$file) {
340
            return new HTTPResponse('File not found', 404);
341
        }
342
        $folder = $file->ParentID ? $file->Parent()->getFilename() : '/';
343
344
        // If extension is the same, attempt to re-use existing name
345
        if (File::get_file_extension($tmpFile['name']) === File::get_file_extension($data['Name'])) {
346
            $tmpFile['name'] = $data['Name'];
347
        } else {
348
            // If we are allowing this upload to rename the underlying record,
349
            // do a uniqueness check.
350
            $renamer = $this->getNameGenerator($tmpFile['name']);
351
            foreach ($renamer as $name) {
352
                $filename = File::join_paths($folder, $name);
353
                if (!File::find($filename)) {
354
                    $tmpFile['name'] = $name;
355
                    break;
356
                }
357
            }
358
        }
359
360 View Code Duplication
        if (!$upload->validate($tmpFile)) {
361
            $result = ['message' => null];
362
            $errors = $upload->getErrors();
363
            if ($message = array_shift($errors)) {
364
                $result['message'] = [
365
                    'type' => 'error',
366
                    'value' => $message,
367
                ];
368
            }
369
            return (new HTTPResponse(json_encode($result), 400))
370
                ->addHeader('Content-Type', 'application/json');
371
        }
372
373
        try {
374
            $tuple = $upload->load($tmpFile, $folder);
375
        } catch (Exception $e) {
376
            $result = [
377
                'message' => [
378
                    'type' => 'error',
379
                    'value' => $e->getMessage(),
380
                ]
381
            ];
382
            return (new HTTPResponse(json_encode($result), 400))
383
                ->addHeader('Content-Type', 'application/json');
384
        }
385
386
        if ($upload->isError()) {
387
            $result['message'] = [
388
                'type' => 'error',
389
                'value' => implode(' ' . PHP_EOL, $upload->getErrors()),
390
            ];
391
            return (new HTTPResponse(json_encode($result), 400))
392
                ->addHeader('Content-Type', 'application/json');
393
        }
394
395
        $tuple['Name'] = basename($tuple['Filename']);
396
        return (new HTTPResponse(json_encode($tuple)))
397
            ->addHeader('Content-Type', 'application/json');
398
    }
399
400
    /**
401
     * Returns a JSON array for history of a given file ID. Returns a list of all the history.
402
     *
403
     * @param HTTPRequest $request
404
     * @return HTTPResponse
405
     */
406
    public function apiHistory(HTTPRequest $request)
407
    {
408
        // CSRF check not required as the GET request has no side effects.
409
        $fileId = $request->getVar('fileId');
410
411
        if (!$fileId || !is_numeric($fileId)) {
412
            return new HTTPResponse(null, 400);
413
        }
414
415
        $class = File::class;
416
        $file = DataObject::get($class)->byID($fileId);
417
418
        if (!$file) {
419
            return new HTTPResponse(null, 404);
420
        }
421
422
        if (!$file->canView()) {
423
            return new HTTPResponse(null, 403);
424
        }
425
426
        $versions = Versioned::get_all_versions($class, $fileId)
427
            ->limit($this->config()->max_history_entries)
428
            ->sort('Version', 'DESC');
429
430
        $output = array();
431
        $next = array();
432
        $prev = null;
433
434
        // swap the order so we can get the version number to compare against.
435
        // i.e version 3 needs to know version 2 is the previous version
436
        $copy = $versions->map('Version', 'Version')->toArray();
437
        foreach (array_reverse($copy) as $k => $v) {
438
            if ($prev) {
439
                $next[$v] = $prev;
440
            }
441
442
            $prev = $v;
443
        }
444
445
        $_cachedMembers = array();
446
447
        /** @var File|AssetAdminFile $version */
448
        foreach ($versions as $version) {
449
            $author = null;
450
451
            if ($version->AuthorID) {
452
                if (!isset($_cachedMembers[$version->AuthorID])) {
453
                    $_cachedMembers[$version->AuthorID] = DataObject::get(Member::class)
454
                        ->byID($version->AuthorID);
455
                }
456
457
                $author = $_cachedMembers[$version->AuthorID];
458
            }
459
460
            if ($version->canView()) {
461
                if (isset($next[$version->Version])) {
462
                    $summary = $version->humanizedChanges(
463
                        $version->Version,
464
                        $next[$version->Version]
465
                    );
466
467
                    // if no summary returned by humanizedChanges, i.e we cannot work out what changed, just show a
468
                    // generic message
469
                    if (!$summary) {
470
                        $summary = _t(__CLASS__.'.SAVEDFILE', "Saved file");
471
                    }
472
                } else {
473
                    $summary = _t(__CLASS__.'.UPLOADEDFILE', "Uploaded file");
474
                }
475
476
                $output[] = array(
477
                    'versionid' => (int) $version->Version,
478
                    'date_ago' => $version->dbObject('LastEdited')->Ago(),
479
                    'date_formatted' => $version->dbObject('LastEdited')->Nice(),
480
                    'status' => ($version->WasPublished) ? _t(__CLASS__.'.PUBLISHED', 'Published') : '',
481
                    'author' => ($author)
482
                        ? $author->Name
483
                        : _t(__CLASS__.'.UNKNOWN', "Unknown"),
484
                    'summary' => ($summary)
485
                        ? $summary
486
                        : _t(__CLASS__.'.NOSUMMARY', "No summary available")
487
                );
488
            }
489
        }
490
491
        return
492
            (new HTTPResponse(json_encode($output)))->addHeader('Content-Type', 'application/json');
493
    }
494
495
    /**
496
     * Redirects 3.x style detail links to new 4.x style routing.
497
     *
498
     * @param HTTPRequest $request
499
     */
500
    public function legacyRedirectForEditView($request)
501
    {
502
        $fileID = $request->param('FileID');
503
        /** @var File $file */
504
        $file = File::get()->byID($fileID);
505
        $link = $this->getFileEditLink($file) ?: $this->Link();
506
        $this->redirect($link);
507
    }
508
509
    /**
510
     * Given a file return the CMS link to edit it
511
     *
512
     * @param File $file
513
     * @return string
514
     */
515
    public function getFileEditLink($file)
516
    {
517
        if (!$file || !$file->isInDB()) {
518
            return null;
519
        }
520
521
        return Controller::join_links(
522
            $this->Link('show'),
523
            $file->ParentID,
524
            'edit',
525
            $file->ID
526
        );
527
    }
528
529
    /**
530
     * Get an asset renamer for the given filename.
531
     *
532
     * @param string $filename Path name
533
     * @return AssetNameGenerator
534
     */
535
    protected function getNameGenerator($filename)
536
    {
537
        return Injector::inst()
538
            ->createWithArgs(AssetNameGenerator::class, array($filename));
539
    }
540
541
    /**
542
     * @todo Implement on client
543
     *
544
     * @param bool $unlinked
545
     * @return ArrayList
546
     */
547
    public function breadcrumbs($unlinked = false)
548
    {
549
        return null;
550
    }
551
552
553
    /**
554
     * Don't include class namespace in auto-generated CSS class
555
     */
556
    public function baseCSSClasses()
557
    {
558
        return 'AssetAdmin LeftAndMain';
559
    }
560
561
    public function providePermissions()
562
    {
563
        return array(
564
            "CMS_ACCESS_AssetAdmin" => array(
565
                'name' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS', "Access to '{title}' section", array(
566
                    'title' => static::menu_title()
567
                )),
568
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
569
            )
570
        );
571
    }
572
573
    /**
574
     * Build a form scaffolder for this model
575
     *
576
     * NOTE: Volatile api. May be moved to {@see LeftAndMain}
577
     *
578
     * @param File $file
579
     * @return FormFactory
580
     */
581
    public function getFormFactory(File $file)
582
    {
583
        // Get service name based on file class
584
        $name = null;
585
        if ($file instanceof Folder) {
586
            $name = FolderFormFactory::class;
587
        } elseif ($file instanceof Image) {
588
            $name = ImageFormFactory::class;
589
        } else {
590
            $name = FileFormFactory::class;
591
        }
592
        return Injector::inst()->get($name);
593
    }
594
595
    /**
596
     * The form is used to generate a form schema,
597
     * as well as an intermediary object to process data through API endpoints.
598
     * Since it's used directly on API endpoints, it does not have any form actions.
599
     * It handles both {@link File} and {@link Folder} records.
600
     *
601
     * @param int $id
602
     * @return Form
603
     */
604
    public function getFileEditForm($id)
605
    {
606
        $form = $this->getAbstractFileForm($id, 'fileEditForm');
607
        $form->setNotifyUnsavedChanges(true);
608
        return $form;
609
    }
610
611
    /**
612
     * Get file edit form
613
     *
614
     * @param HTTPRequest $request
615
     * @return Form
616
     */
617 View Code Duplication
    public function fileEditForm($request = null)
618
    {
619
        // Get ID either from posted back value, or url parameter
620
        if (!$request) {
621
            $this->httpError(400);
622
            return null;
623
        }
624
        $id = $request->param('ID');
625
        if (!$id) {
626
            $this->httpError(400);
627
            return null;
628
        }
629
        return $this->getFileEditForm($id);
630
    }
631
632
    /**
633
     * The form is used to generate a form schema,
634
     * as well as an intermediary object to process data through API endpoints.
635
     * Since it's used directly on API endpoints, it does not have any form actions.
636
     * It handles both {@link File} and {@link Folder} records.
637
     *
638
     * @param int $id
639
     * @return Form
640
     */
641
    public function getFileInsertForm($id)
642
    {
643
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT_MEDIA ]);
644
    }
645
646
    /**
647
     * Get file insert media form
648
     *
649
     * @param HTTPRequest $request
650
     * @return Form
651
     */
652 View Code Duplication
    public function fileInsertForm($request = null)
653
    {
654
        // Get ID either from posted back value, or url parameter
655
        if (!$request) {
656
            $this->httpError(400);
657
            return null;
658
        }
659
        $id = $request->param('ID');
660
        if (!$id) {
661
            $this->httpError(400);
662
            return null;
663
        }
664
        return $this->getFileInsertForm($id);
665
    }
666
667
    /**
668
     * The form used to generate a form schema, since it's used directly on API endpoints,
669
     * it does not have any form actions.
670
     *
671
     * @param $id
672
     * @return Form
673
     */
674
    public function getFileEditorLinkForm($id)
675
    {
676
        return $this->getAbstractFileForm($id, 'fileInsertForm', [ 'Type' => AssetFormFactory::TYPE_INSERT_LINK ]);
677
    }
678
679
    /**
680
     * Get the file insert link form
681
     *
682
     * @param HTTPRequest $request
683
     * @return Form
684
     */
685 View Code Duplication
    public function fileEditorLinkForm($request = null)
686
    {
687
        // Get ID either from posted back value, or url parameter
688
        if (!$request) {
689
            $this->httpError(400);
690
            return null;
691
        }
692
        $id = $request->param('ID');
693
        if (!$id) {
694
            $this->httpError(400);
695
            return null;
696
        }
697
        return $this->getFileInsertForm($id);
698
    }
699
700
    /**
701
     * Abstract method for generating a form for a file
702
     *
703
     * @param int $id Record ID
704
     * @param string $name Form name
705
     * @param array $context Form context
706
     * @return Form
707
     */
708
    protected function getAbstractFileForm($id, $name, $context = [])
709
    {
710
        /** @var File $file */
711
        $file = File::get()->byID($id);
712
713
        if (!$file) {
714
            $this->httpError(404);
715
            return null;
716
        }
717
718 View Code Duplication
        if (!$file->canView()) {
719
            $this->httpError(403, _t(
720
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
721
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
722
                '',
723
                ['ObjectTitle' => $file->i18n_singular_name()]
724
            ));
725
            return null;
726
        }
727
728
        // Pass to form factory
729
        $augmentedContext = array_merge($context, ['Record' => $file]);
730
        $scaffolder = $this->getFormFactory($file);
731
        $form = $scaffolder->getForm($this, $name, $augmentedContext);
732
733
        // Set form action handler with ID included
734
        $form->setRequestHandler(
735
            LeftAndMainFormRequestHandler::create($form, [ $id ])
736
        );
737
738
        // Configure form to respond to validation errors with form schema
739
        // if requested via react.
740
        $form->setValidationResponseCallback(function (ValidationResult $error) use ($form, $id, $name) {
741
            $schemaId = Controller::join_links($this->Link('schema'), $name, $id);
742
            return $this->getSchemaResponse($schemaId, $form, $error);
743
        });
744
745
        return $form;
746
    }
747
748
    /**
749
     * Get form for selecting a file
750
     *
751
     * @return Form
752
     */
753
    public function fileSelectForm()
754
    {
755
        // Get ID either from posted back value, or url parameter
756
        $request = $this->getRequest();
757
        $id = $request->param('ID') ?: $request->postVar('ID');
758
        return $this->getFileSelectForm($id);
759
    }
760
761
    /**
762
     * Get form for selecting a file
763
     *
764
     * @param int $id ID of the record being selected
765
     * @return Form
766
     */
767
    public function getFileSelectForm($id)
768
    {
769
        return $this->getAbstractFileForm($id, 'fileSelectForm', [ 'Type' => AssetFormFactory::TYPE_SELECT ]);
770
    }
771
772
    /**
773
     * @param array $context
774
     * @return Form
775
     * @throws InvalidArgumentException
776
     */
777
    public function getFileHistoryForm($context)
778
    {
779
        // Check context
780
        if (!isset($context['RecordID']) || !isset($context['RecordVersion'])) {
781
            throw new InvalidArgumentException("Missing RecordID / RecordVersion for this form");
782
        }
783
        $id = $context['RecordID'];
784
        $versionId = $context['RecordVersion'];
785
        if (!$id || !$versionId) {
786
            return $this->httpError(404);
787
        }
788
789
        /** @var File $file */
790
        $file = Versioned::get_version(File::class, $id, $versionId);
791
        if (!$file) {
792
            return $this->httpError(404);
793
        }
794
795 View Code Duplication
        if (!$file->canView()) {
796
            $this->httpError(403, _t(
797
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
798
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
799
                '',
800
                ['ObjectTitle' => $file->i18n_singular_name()]
801
            ));
802
            return null;
803
        }
804
805
        $effectiveContext = array_merge($context, ['Record' => $file]);
806
        /** @var FormFactory $scaffolder */
807
        $scaffolder = Injector::inst()->get(FileHistoryFormFactory::class);
808
        $form = $scaffolder->getForm($this, 'fileHistoryForm', $effectiveContext);
809
810
        // Set form handler with ID / VersionID
811
        $form->setRequestHandler(
812
            LeftAndMainFormRequestHandler::create($form, [ $id, $versionId ])
813
        );
814
815
        // Configure form to respond to validation errors with form schema
816
        // if requested via react.
817 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id, $versionId) {
818
            $schemaId = Controller::join_links($this->Link('schema/fileHistoryForm'), $id, $versionId);
819
            return $this->getSchemaResponse($schemaId, $form, $errors);
820
        });
821
822
        return $form;
823
    }
824
825
    /**
826
     * Gets a JSON schema representing the current edit form.
827
     *
828
     * WARNING: Experimental API.
829
     *
830
     * @param HTTPRequest $request
831
     * @return HTTPResponse
832
     */
833
    public function schema($request)
834
    {
835
        $formName = $request->param('FormName');
836
        if ($formName !== 'fileHistoryForm') {
837
            return parent::schema($request);
838
        }
839
840
        // Get schema for history form
841
        // @todo Eventually all form scaffolding will be based on context rather than record ID
842
        // See https://github.com/silverstripe/silverstripe-framework/issues/6362
843
        $itemID = $request->param('ItemID');
844
        $version = $request->param('OtherItemID');
845
        $form = $this->getFileHistoryForm([
846
            'RecordID' => $itemID,
847
            'RecordVersion' => $version,
848
        ]);
849
850
        // Respond with this schema
851
        $response = $this->getResponse();
852
        $response->addHeader('Content-Type', 'application/json');
853
        $schemaID = $this->getRequest()->getURL();
854
        return $this->getSchemaResponse($schemaID, $form);
855
    }
856
857
    /**
858
     * Get file history form
859
     *
860
     * @param HTTPRequest $request
861
     * @return Form
862
     */
863
    public function fileHistoryForm($request = null)
864
    {
865
        // Get ID either from posted back value, or url parameter
866
        if (!$request) {
867
            $this->httpError(400);
868
            return null;
869
        }
870
        $id = $request->param('ID');
871
        if (!$id) {
872
            $this->httpError(400);
873
            return null;
874
        }
875
        $versionID = $request->param('VersionID');
876
        if (!$versionID) {
877
            $this->httpError(400);
878
            return null;
879
        }
880
        $form = $this->getFileHistoryForm([
881
            'RecordID' => $id,
882
            'RecordVersion' => $versionID,
883
        ]);
884
        return $form;
885
    }
886
887
    /**
888
     * @param array $data
889
     * @param Form $form
890
     * @return HTTPResponse
891
     */
892
    public function createfolder($data, $form)
893
    {
894
        $parentID = isset($data['ParentID']) ? intval($data['ParentID']) : 0;
895
        $data['Parent'] = null;
896
        if ($parentID) {
897
            $parent = Folder::get()->byID($parentID);
898
            if (!$parent) {
899
                throw new \InvalidArgumentException(sprintf(
900
                    '%s#%s not found',
901
                    Folder::class,
902
                    $parentID
903
                ));
904
            }
905
            $data['Parent'] = $parent;
906
        }
907
908
        // Check permission
909
        if (!Folder::singleton()->canCreate(Security::getCurrentUser(), $data)) {
910
            throw new \InvalidArgumentException(sprintf(
911
                '%s create not allowed',
912
                Folder::class
913
            ));
914
        }
915
916
        $folder = Folder::create();
917
        $form->saveInto($folder);
918
        $folder->write();
919
920
        $createForm = $this->getFolderCreateForm($folder->ID);
921
922
        // Return the record data in the same response as the schema to save a postback
923
        $schemaData = ['record' => $this->getObjectFromData($folder)];
924
        $schemaId = Controller::join_links($this->Link('schema/folderCreateForm'), $folder->ID);
925
        return $this->getSchemaResponse($schemaId, $createForm, null, $schemaData);
926
    }
927
928
    /**
929
     * @param array $data
930
     * @param Form $form
931
     * @return HTTPResponse
932
     */
933
    public function save($data, $form)
934
    {
935
        return $this->saveOrPublish($data, $form, false);
936
    }
937
938
    /**
939
     * @param array $data
940
     * @param Form $form
941
     * @return HTTPResponse
942
     */
943
    public function publish($data, $form)
944
    {
945
        return $this->saveOrPublish($data, $form, true);
946
    }
947
948
    /**
949
     * Update thisrecord
950
     *
951
     * @param array $data
952
     * @param Form $form
953
     * @param bool $doPublish
954
     * @return HTTPResponse
955
     */
956
    protected function saveOrPublish($data, $form, $doPublish = false)
957
    {
958 View Code Duplication
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
959
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
960
                ->addHeader('Content-Type', 'application/json');
961
        }
962
963
        $id = (int) $data['ID'];
964
        /** @var File $record */
965
        $record = DataObject::get_by_id(File::class, $id);
966
967
        if (!$record) {
968
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
969
                ->addHeader('Content-Type', 'application/json');
970
        }
971
972
        if (!$record->canEdit() || ($doPublish && !$record->canPublish())) {
973
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
974
                ->addHeader('Content-Type', 'application/json');
975
        }
976
977
        // check File extension
978
        if (!empty($data['FileFilename'])) {
979
            $extension = File::get_file_extension($data['FileFilename']);
980
            $newClass = File::get_class_for_file_extension($extension);
981
982
            // if the class has changed, cast it to the proper class
983
            if ($record->getClassName() !== $newClass) {
984
                $record = $record->newClassInstance($newClass);
985
986
                // update the allowed category for the new file extension
987
                $category = File::get_app_category($extension);
988
                $record->File->setAllowedCategories($category);
989
            }
990
        }
991
992
        $form->saveInto($record);
993
        $record->write();
994
995
        // Publish this record and owned objects
996
        if ($doPublish) {
997
            $record->publishRecursive();
998
        }
999
        // regenerate form, so that it constants/literals on the form are updated
1000
        $form = $this->getFileEditForm($record->ID);
1001
1002
        // Note: Force return of schema / state in success result
1003
        return $this->getRecordUpdatedResponse($record, $form);
1004
    }
1005
1006
    public function unpublish($data, $form)
1007
    {
1008 View Code Duplication
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
1009
            return (new HTTPResponse(json_encode(['status' => 'error']), 400))
1010
                ->addHeader('Content-Type', 'application/json');
1011
        }
1012
1013
        $id = (int) $data['ID'];
1014
        /** @var File $record */
1015
        $record = DataObject::get_by_id(File::class, $id);
1016
1017
        if (!$record) {
1018
            return (new HTTPResponse(json_encode(['status' => 'error']), 404))
1019
                ->addHeader('Content-Type', 'application/json');
1020
        }
1021
1022
        if (!$record->canUnpublish()) {
1023
            return (new HTTPResponse(json_encode(['status' => 'error']), 401))
1024
                ->addHeader('Content-Type', 'application/json');
1025
        }
1026
1027
        $record->doUnpublish();
1028
        return $this->getRecordUpdatedResponse($record, $form);
1029
    }
1030
1031
    /**
1032
     * @param File $file
1033
     *
1034
     * @return array
1035
     */
1036
    public function getObjectFromData(File $file)
1037
    {
1038
        $object = array(
1039
            'id' => $file->ID,
1040
            'created' => $file->Created,
1041
            'lastUpdated' => $file->LastEdited,
1042
            'owner' => null,
1043
            'parent' => null,
1044
            'title' => $file->Title,
1045
            'exists' => $file->exists(), // Broken file check
1046
            'type' => $file instanceof Folder ? 'folder' : $file->FileType,
1047
            'category' => $file instanceof Folder ? 'folder' : $file->appCategory(),
1048
            'name' => $file->Name,
1049
            'filename' => $file->Filename,
1050
            'extension' => $file->Extension,
1051
            'size' => $file->AbsoluteSize,
1052
            'url' => $file->AbsoluteURL,
1053
            'published' => $file->isPublished(),
1054
            'modified' => $file->isModifiedOnDraft(),
1055
            'draft' => $file->isOnDraftOnly(),
1056
            'canEdit' => $file->canEdit(),
1057
            'canDelete' => $file->canArchive(),
1058
        );
1059
1060
        /** @var Member $owner */
1061
        $owner = $file->Owner();
1062
1063
        if ($owner) {
1064
            $object['owner'] = array(
1065
                'id' => $owner->ID,
1066
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
1067
            );
1068
        }
1069
1070
        /** @var Folder $parent */
1071
        $parent = $file->Parent();
1072
1073
        if ($parent) {
1074
            $object['parent'] = array(
1075
                'id' => $parent->ID,
1076
                'title' => $parent->Title,
1077
                'filename' => $parent->Filename,
1078
            );
1079
        }
1080
1081
        /** @var File $file */
1082
        if ($file->getIsImage()) {
1083
            // Small thumbnail
1084
            $smallWidth = UploadField::config()->uninherited('thumbnail_width');
1085
            $smallHeight = UploadField::config()->uninherited('thumbnail_height');
1086
            $smallThumbnail = $file->FitMax($smallWidth, $smallHeight);
1087
            if ($smallThumbnail && $smallThumbnail->exists()) {
1088
                $object['smallThumbnail'] = $smallThumbnail->getAbsoluteURL();
1089
            }
1090
1091
            // Large thumbnail
1092
            $width = $this->config()->get('thumbnail_width');
1093
            $height = $this->config()->get('thumbnail_height');
1094
            $thumbnail = $file->FitMax($width, $height);
1095
            if ($thumbnail && $thumbnail->exists()) {
1096
                $object['thumbnail'] = $thumbnail->getAbsoluteURL();
1097
            }
1098
            $object['width'] = $file->Width;
1099
            $object['height'] = $file->Height;
1100
        } else {
1101
            $object['thumbnail'] = $file->PreviewLink();
1102
        }
1103
1104
        return $object;
1105
    }
1106
1107
    /**
1108
     * Action handler for adding pages to a campaign
1109
     *
1110
     * @param array $data
1111
     * @param Form $form
1112
     * @return DBHTMLText|HTTPResponse
1113
     */
1114
    public function addtocampaign($data, $form)
1115
    {
1116
        $id = $data['ID'];
1117
        $record = File::get()->byID($id);
1118
1119
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1120
        $results = $handler->addToCampaign($record, $data['Campaign']);
1121
        if (!isset($results)) {
1122
            return null;
1123
        }
1124
1125
        // Send extra "message" data with schema response
1126
        $extraData = ['message' => $results];
1127
        $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1128
        return $this->getSchemaResponse($schemaId, $form, null, $extraData);
1129
    }
1130
1131
    /**
1132
     * Url handler for add to campaign form
1133
     *
1134
     * @param HTTPRequest $request
1135
     * @return Form
1136
     */
1137
    public function addToCampaignForm($request)
1138
    {
1139
        // Get ID either from posted back value, or url parameter
1140
        $id = $request->param('ID') ?: $request->postVar('ID');
1141
        return $this->getAddToCampaignForm($id);
1142
    }
1143
1144
    /**
1145
     * @param int $id
1146
     * @return Form
1147
     */
1148
    public function getAddToCampaignForm($id)
1149
    {
1150
        // Get record-specific fields
1151
        $record = File::get()->byID($id);
1152
1153
        if (!$record) {
1154
            $this->httpError(404, _t(
1155
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorNotFound',
1156
                'That {Type} couldn\'t be found',
1157
                '',
1158
                ['Type' => File::singleton()->i18n_singular_name()]
1159
            ));
1160
            return null;
1161
        }
1162 View Code Duplication
        if (!$record->canView()) {
1163
            $this->httpError(403, _t(
1164
                'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin.ErrorItemPermissionDenied',
1165
                'You don\'t have the necessary permissions to modify {ObjectTitle}',
1166
                '',
1167
                ['ObjectTitle' => $record->i18n_singular_name()]
1168
            ));
1169
            return null;
1170
        }
1171
1172
        $handler = AddToCampaignHandler::create($this, $record, 'addToCampaignForm');
1173
        $form = $handler->Form($record);
1174
1175 View Code Duplication
        $form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $id) {
1176
            $schemaId = Controller::join_links($this->Link('schema/addToCampaignForm'), $id);
1177
            return $this->getSchemaResponse($schemaId, $form, $errors);
1178
        });
1179
1180
        return $form;
1181
    }
1182
1183
    /**
1184
     * @return Upload
1185
     */
1186
    protected function getUpload()
1187
    {
1188
        $upload = Upload::create();
1189
        $upload->getValidator()->setAllowedExtensions(
1190
            // filter out '' since this would be a regex problem on JS end
1191
            array_filter(File::config()->uninherited('allowed_extensions'))
1192
        );
1193
1194
        return $upload;
1195
    }
1196
1197
    /**
1198
     * Get response for successfully updated record
1199
     *
1200
     * @param File $record
1201
     * @param Form $form
1202
     * @return HTTPResponse
1203
     */
1204
    protected function getRecordUpdatedResponse($record, $form)
1205
    {
1206
        // Return the record data in the same response as the schema to save a postback
1207
        $schemaData = ['record' => $this->getObjectFromData($record)];
1208
        $schemaId = Controller::join_links($this->Link('schema/fileEditForm'), $record->ID);
1209
        return $this->getSchemaResponse($schemaId, $form, null, $schemaData);
1210
    }
1211
1212
    /**
1213
     * @param HTTPRequest $request
1214
     * @return Form
1215
     */
1216
    public function folderCreateForm($request = null)
1217
    {
1218
        // Get ID either from posted back value, or url parameter
1219
        if (!$request) {
1220
            $this->httpError(400);
1221
            return null;
1222
        }
1223
        $id = $request->param('ParentID');
1224
        // Fail on null ID (but not parent)
1225
        if (!isset($id)) {
1226
            $this->httpError(400);
1227
            return null;
1228
        }
1229
        return $this->getFolderCreateForm($id);
1230
    }
1231
1232
    /**
1233
     * Returns the form to be used for creating a new folder
1234
     * @param $parentId
1235
     * @return Form
1236
     */
1237
    public function getFolderCreateForm($parentId = 0)
1238
    {
1239
        /** @var FolderCreateFormFactory $factory */
1240
        $factory = Injector::inst()->get(FolderCreateFormFactory::class);
1241
        $form = $factory->getForm($this, 'folderCreateForm', [ 'ParentID' => $parentId ]);
1242
1243
        // Set form action handler with ParentID included
1244
        $form->setRequestHandler(
1245
            LeftAndMainFormRequestHandler::create($form, [ $parentId ])
1246
        );
1247
1248
        return $form;
1249
    }
1250
1251
    /**
1252
     * Scaffold a search form.
1253
     * Note: This form does not submit to itself, but rather uses the apiReadFolder endpoint
1254
     * (to be replaced with graphql)
1255
     *
1256
     * @return Form
1257
     */
1258
    public function fileSearchForm()
1259
    {
1260
        $scaffolder = FileSearchFormFactory::singleton();
1261
        return $scaffolder->getForm($this, 'fileSearchForm', []);
1262
    }
1263
1264
    /**
1265
     * Allow search form to be accessible to schema
1266
     *
1267
     * @return Form
1268
     */
1269
    public function getFileSearchform()
1270
    {
1271
        return $this->fileSearchForm();
1272
    }
1273
}
1274