Completed
Pull Request — master (#215)
by Ingo
02:11
created

AssetAdmin::getFileEditForm()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 44
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 44
rs 8.5806
cc 4
eloc 25
nc 4
nop 1
1
<?php
2
3
namespace SilverStripe\AssetAdmin\Controller;
4
5
use SilverStripe\Filesystem\Storage\AssetNameGenerator;
6
use SilverStripe\ORM\DataObject;
7
use SilverStripe\ORM\SS_List;
8
use SilverStripe\Security\Member;
9
use SilverStripe\Security\PermissionProvider;
10
use SilverStripe\Security\SecurityToken;
11
use SearchContext;
12
use LeftAndMain;
13
use DateField;
14
use DropdownField;
15
use Controller;
16
use FieldList;
17
use Form;
18
use CheckboxField;
19
use File;
20
use Requirements;
21
use CMSBatchActionHandler;
22
use Injector;
23
use Folder;
24
use HeaderField;
25
use FieldGroup;
26
use SS_HTTPRequest;
27
use SS_HTTPResponse;
28
use Upload;
29
use Config;
30
use FormAction;
31
use TextField;
32
use HiddenField;
33
use ReadonlyField;
34
use LiteralField;
35
36
/**
37
 * AssetAdmin is the 'file store' section of the CMS.
38
 * It provides an interface for manipulating the File and Folder objects in the system.
39
 *
40
 * @package cms
41
 * @subpackage assets
42
 */
43
class AssetAdmin extends LeftAndMain implements PermissionProvider
44
{
45
    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...
46
47
    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...
48
49
    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...
50
51
    private static $tree_class = '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...
52
53
    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...
54
        // Legacy redirect for SS3-style detail view
55
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
56
        // Pass all URLs to the index, for React to unpack
57
        'show/$FolderID/edit/$FileID' => 'index',
58
        // API access points with structured data
59
        'POST api/createFolder' => 'apiCreateFolder',
60
        'POST api/createFile' => 'apiCreateFile',
61
        'GET api/readFolder' => 'apiReadFolder',
62
        'PUT api/updateFolder' => 'apiUpdateFolder',
63
        'PUT api/updateFile' => 'apiUpdateFile',
64
        'DELETE api/delete' => 'apiDelete',
65
        'GET api/search' => 'apiSearch',
66
    ];
67
68
    /**
69
     * Amount of results showing on a single page.
70
     *
71
     * @config
72
     * @var int
73
     */
74
    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...
75
76
    /**
77
     * @config
78
     * @see Upload->allowedMaxFileSize
79
     * @var int
80
     */
81
    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...
82
83
    /**
84
     * @var array
85
     */
86
    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...
87
        'legacyRedirectForEditView',
88
        'apiCreateFolder',
89
        'apiCreateFile',
90
        'apiReadFolder',
91
        'apiUpdateFolder',
92
        'apiUpdateFile',
93
        'apiDelete',
94
        'apiSearch',
95
        'FileEditForm',
96
    );
97
98
    /**
99
     * Set up the controller
100
     */
101
    public function init()
102
    {
103
        parent::init();
104
105
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
106
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
107
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
108
109
        CMSBatchActionHandler::register('delete', 'SilverStripe\AssetAdmin\BatchAction\DeleteAssets', 'Folder');
110
    }
111
112
    public function getClientConfig()
113
    {
114
        $baseLink = $this->Link();
115
        return array_merge( parent::getClientConfig(), [
116
            'reactRouter' => true,
117
            'createFileEndpoint' => [
118
                'url' => Controller::join_links($baseLink, 'api/createFile'),
119
                'method' => 'post',
120
                'payloadFormat' => 'urlencoded',
121
            ],
122
            'createFolderEndpoint' => [
123
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
124
                'method' => 'post',
125
                'payloadFormat' => 'urlencoded',
126
            ],
127
            'readFolderEndpoint' => [
128
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
129
                'method' => 'get',
130
                'responseFormat' => 'json',
131
            ],
132
            'searchEndpoint' => [
133
                'url' => Controller::join_links($baseLink, 'api/search'),
134
                'method' => 'get',
135
                'responseFormat' => 'json',
136
            ],
137
            'updateFileEndpoint' => [
138
                'url' => Controller::join_links($baseLink, 'api/updateFile'),
139
                'method' => 'put',
140
                'payloadFormat' => 'urlencoded',
141
            ],
142
            'updateFolderEndpoint' => [
143
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
144
                'method' => 'put',
145
                'payloadFormat' => 'urlencoded',
146
            ],
147
            'deleteEndpoint' => [
148
                'url' => Controller::join_links($baseLink, 'api/delete'),
149
                'method' => 'delete',
150
                'payloadFormat' => 'urlencoded',
151
            ],
152
            'limit' => $this->config()->page_length,
153
            'form' => [
154
                'FileEditForm' => [
155
                    'schemaUrl' => $this->Link('schema/FileEditForm')
156
                ],
157
            ],
158
        ]);
159
    }
160
161
    /**
162
     * Fetches a collection of files by ParentID.
163
     *
164
     * @param SS_HTTPRequest $request
165
     * @return SS_HTTPResponse
166
     */
167
    public function apiReadFolder(SS_HTTPRequest $request)
168
    {
169
        $params = $request->requestVars();
170
        $items = array();
171
        $parentId = null;
0 ignored issues
show
Unused Code introduced by
$parentId is not used, you could remove the assignment.

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

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

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

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

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

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

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

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

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

Loading history...
173
174
        if (!isset($params['id']) && !strlen($params['id'])) {
175
            $this->httpError(400);
176
        }
177
178
        $folderID = (int)$params['id'];
179
        /** @var Folder $folder */
180
        $folder = $folderID ? Folder::get()->byID($folderID) : singleton('Folder');
181
182
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
183
        $files = $this->getList()->filter('ParentID', $folderID);
184
185
        if ($files) {
186
            foreach ($files as $file) {
187
                if (!$file->canView()) {
188
                    continue;
189
                }
190
191
                $items[] = $this->getObjectFromData($file);
192
            }
193
        }
194
195
        // Build parents (for breadcrumbs)
196
        $parents = [];
197
        $next = $folder->Parent();
198
        while($next && $next->exists()) {
199
            array_unshift($parents, [
200
                'id' => $next->ID,
201
                'title' => $next->getTitle(),
202
            ]);
203
            if($next->ParentID) {
204
                $next = $next->Parent();
205
            } else {
206
                break;
207
            }
208
        }
209
210
        // Build response
211
        $response = new SS_HTTPResponse();
212
        $response->addHeader('Content-Type', 'application/json');
213
        $response->setBody(json_encode([
214
            'files' => $items,
215
            'title' => $folder->getTitle(),
216
            'count' => count($items),
217
            'parents' => $parents,
218
            'parentID' => $folder->exists() ? $folder->ParentID : null, // grandparent
219
            'folderID' => $folderID,
220
            'canEdit' => $folder->canEdit(),
221
            'canDelete' => $folder->canDelete(),
222
        ]));
223
224
        return $response;
225
    }
226
227
    /**
228
     * @param SS_HTTPRequest $request
229
     *
230
     * @return SS_HTTPResponse
231
     */
232
    public function apiSearch(SS_HTTPRequest $request)
233
    {
234
        $params = $request->getVars();
235
        $list = $this->getList($params);
236
237
        $response = new SS_HTTPResponse();
238
        $response->addHeader('Content-Type', 'application/json');
239
        $response->setBody(json_encode([
240
            // Serialisation
241
            "files" => array_map(function($file) {
242
                return $this->getObjectFromData($file);
243
            }, $list->toArray()),
244
            "count" => $list->count(),
245
        ]));
246
247
        return $response;
248
    }
249
250
    /**
251
     * @param SS_HTTPRequest $request
252
     *
253
     * @return SS_HTTPResponse
254
     */
255
    public function apiUpdateFile(SS_HTTPRequest $request)
256
    {
257
        parse_str($request->getBody(), $data);
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 SS_HTTPResponse(null, 400);
263
        }
264
265 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...
266
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
267
                ->addHeader('Content-Type', 'application/json');
268
        }
269
270
        $id = $data['id'];
271
        $record = $this->getList()->filter('ID', (int) $id)->first();
272
273 View Code Duplication
        if (!$record) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
274
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
275
                ->addHeader('Content-Type', 'application/json');
276
        }
277
278 View Code Duplication
        if (!$record->canEdit()) {
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...
279
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
280
                ->addHeader('Content-Type', 'application/json');
281
        }
282
283
        // TODO Use same property names and capitalisation as DataObject
284
        if (!empty($data['title'])) {
285
            $record->Title = $data['title'];
286
        }
287
288
        // TODO Use same property names and capitalisation as DataObject
289
        if (!empty($data['basename'])) {
290
            $record->Name = $data['basename'];
291
        }
292
293
        $record->write();
294
295
        return (new SS_HTTPResponse(json_encode(['status' => 'ok']), 200))
296
            ->addHeader('Content-Type', 'application/json');
297
    }
298
299
    /**
300
     * @param SS_HTTPRequest $request
301
     *
302
     * @return SS_HTTPResponse
303
     */
304
    public function apiDelete(SS_HTTPRequest $request)
305
    {
306
        parse_str($request->getBody(), $vars);
307
308
        // CSRF check
309
        $token = SecurityToken::inst();
310 View Code Duplication
        if (empty($vars[$token->getName()]) || !$token->check($vars[$token->getName()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
315
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
316
                ->addHeader('Content-Type', 'application/json');
317
        }
318
319
        $fileIds = $vars['ids'];
320
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
321
322 View Code Duplication
        if (!count($files)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
323
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
324
                ->addHeader('Content-Type', 'application/json');
325
        }
326
327
        if (!min(array_map(function ($file) {
328
            return $file->canDelete();
329
        }, $files))) {
330
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
331
                ->addHeader('Content-Type', 'application/json');
332
        }
333
334
        foreach ($files as $file) {
335
            $file->delete();
336
        }
337
338
        return (new SS_HTTPResponse(json_encode(['status' => 'file was deleted'])))
339
            ->addHeader('Content-Type', 'application/json');
340
    }
341
342
    /**
343
     * Creates a single file based on a form-urlencoded upload.
344
     *
345
     * @param SS_HTTPRequest $request
346
     * @return SS_HTTPRequest|SS_HTTPResponse
347
     */
348
    public function apiCreateFile(SS_HTTPRequest $request)
349
    {
350
        $data = $request->postVars();
351
        $upload = $this->getUpload();
352
353
        // CSRF check
354
        $token = SecurityToken::inst();
355 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...
356
            return new SS_HTTPResponse(null, 400);
357
        }
358
359
        // check canAddChildren permissions
360
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
361
            $parentRecord = Folder::get()->byID($data['ParentID']);
362 View Code Duplication
            if ($parentRecord->hasMethod('canAddChildren') && !$parentRecord->canAddChildren()) {
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...
363
                return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
364
                    ->addHeader('Content-Type', 'application/json');
365
            }
366
        } else {
367
            $parentRecord = singleton('Folder');
368
        }
369
370
        // check create permissions
371
        if (!$parentRecord->canCreate()) {
372
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
373
                ->addHeader('Content-Type', 'application/json');
374
        }
375
376
        $tmpFile = $request->postVar('Upload');
377
        if(!$upload->validate($tmpFile)) {
378
            $result = ['error' => $upload->getErrors()];
379
            return (new SS_HTTPResponse(json_encode($result), 400))
380
                ->addHeader('Content-Type', 'application/json');
381
        }
382
383
        // TODO Allow batch uploads
384
        $fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
385
        $file = Injector::inst()->create($fileClass);
386
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
387 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...
388
            $result = ['error' => 'unknown'];
389
            return (new SS_HTTPResponse(json_encode($result), 400))
390
                ->addHeader('Content-Type', 'application/json');
391
        }
392
393
        $file->ParentID = $parentRecord->ID;
394
        $file->write();
395
396
        $result = [$this->getObjectFromData($file)];
397
398
        return (new SS_HTTPResponse(json_encode($result)))
399
            ->addHeader('Content-Type', 'application/json');
400
    }
401
402
    /**
403
     * Creates a single folder, within an optional parent folder.
404
     *
405
     * @param SS_HTTPRequest $request
406
     * @return SS_HTTPRequest|SS_HTTPResponse
407
     */
408
    public function apiCreateFolder(SS_HTTPRequest $request)
409
    {
410
        $data = $request->postVars();
411
412
        $class = 'Folder';
413
414
        // CSRF check
415
        $token = SecurityToken::inst();
416 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...
417
            return new SS_HTTPResponse(null, 400);
418
        }
419
420
        // check addchildren permissions
421
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
422
            $parentRecord = DataObject::get_by_id($class, $data['ParentID']);
423 View Code Duplication
            if ($parentRecord->hasMethod('canAddChildren') && !$parentRecord->canAddChildren()) {
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...
424
                return (new SS_HTTPResponse(null, 403))
425
                    ->addHeader('Content-Type', 'application/json');
426
            }
427
        } else {
428
            $parentRecord = singleton($class);
429
        }
430
        $data['ParentID'] = ($parentRecord->exists()) ? (int)$parentRecord->ID : 0;
431
432
        // check create permissions
433
        if (!$parentRecord->canCreate()) {
434
            return (new SS_HTTPResponse(null, 403))
435
                ->addHeader('Content-Type', 'application/json');
436
        }
437
438
        // Build filename
439
        $baseFilename = isset($data['Name'])
440
            ? basename($data['Name'])
441
            : _t('AssetAdmin.NEWFOLDER', "NewFolder");
442
443
        if ($parentRecord && $parentRecord->ID) {
444
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
445
        }
446
447
        // Ensure name is unique
448
        $nameGenerator = $this->getNameGenerator($baseFilename);
449
        foreach ($nameGenerator as $filename) {
450
            if (! File::find($filename)) {
451
                break;
452
            }
453
        }
454
        $data['Name'] = basename($filename);
0 ignored issues
show
Bug introduced by
The variable $filename seems to be defined by a foreach iteration on line 449. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
455
456
        // Create record
457
        $record = $class::create();
458
        $record->ParentID = $data['ParentID'];
459
        $record->Name = $record->Title = basename($data['Name']);
460
        $record->write();
461
462
        $result = [
463
            "ParentID" => $record->ParentID,
464
            "ID" => $record->ID,
465
            "Filename" => $record->Filename,
466
        ];
467
468
        return (new SS_HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
469
    }
470
471
    /**
472
     * Redirects 3.x style detail links to new 4.x style routing.
473
     *
474
     * @param $request
475
     */
476
    public function legacyRedirectForEditView($request)
477
    {
478
        $fileID = $request->param('FileID');
479
        $file = File::get()->byID($fileID);
480
        $link = $this->getFileEditLink($file) ?: $this->Link();
0 ignored issues
show
Documentation introduced by
$file is of type object<SilverStripe\ORM\DataObject>|null, but the function expects a object<File>.

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
        $this->redirect($link);
482
    }
483
484
    /**
485
     * Given a file return the CMS link to edit it
486
     *
487
     * @param File $file
488
     * @return string
489
     */
490
    public function getFileEditLink($file) {
491
        if(!$file || !$file->isInDB()) {
492
            return null;
493
        }
494
495
        return Controller::join_links(
496
            $this->Link('show'),
497
            $file->ParentID,
498
            'edit',
499
            $file->ID
500
        );
501
    }
502
503
    /**
504
     * Get the search context from {@link File}, used to create the search form
505
     * as well as power the /search API endpoint.
506
     *
507
     * @return SearchContext
508
     */
509
    public function getSearchContext()
510
    {
511
        $context = singleton('File')->getDefaultSearchContext();
512
513
        // Customize fields
514
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
515
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
516
        ->setConfig('showcalendar', true);
517
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
518
        ->setConfig('showcalendar', true);
519
        $dateGroup = FieldGroup::create(
520
            $dateHeader,
521
            $dateFrom,
522
            $dateTo
523
        );
524
        $context->addField($dateGroup);
525
        $appCategories = array(
526
            'archive' => _t('AssetAdmin.AppCategoryArchive', 'Archive', 'A collection of files'),
527
            'audio' => _t('AssetAdmin.AppCategoryAudio', 'Audio'),
528
            'document' => _t('AssetAdmin.AppCategoryDocument', 'Document'),
529
            'flash' => _t('AssetAdmin.AppCategoryFlash', 'Flash', 'The fileformat'),
530
            'image' => _t('AssetAdmin.AppCategoryImage', 'Image'),
531
            'video' => _t('AssetAdmin.AppCategoryVideo', 'Video'),
532
        );
533
        $context->addField(
534
            $typeDropdown = new DropdownField(
535
                'AppCategory',
536
                _t('AssetAdmin.Filetype', 'File type'),
537
                $appCategories
538
            )
539
        );
540
541
        $typeDropdown->setEmptyString(' ');
542
543
        $context->addField(
544
            new CheckboxField('CurrentFolderOnly', _t('AssetAdmin.CurrentFolderOnly', 'Limit to current folder?'))
545
        );
546
        $context->getFields()->removeByName('Title');
547
548
        return $context;
549
    }
550
551
    /**
552
     * Get an asset renamer for the given filename.
553
     *
554
     * @param  string             $filename Path name
555
     * @return AssetNameGenerator
556
     */
557
    protected function getNameGenerator($filename)
558
    {
559
        return Injector::inst()
560
            ->createWithArgs('AssetNameGenerator', array($filename));
561
    }
562
563
    /**
564
     * @todo Implement on client
565
     */
566
    public function breadcrumbs($unlinked = false)
567
    {
568
        return null;
569
    }
570
571
572
    /**
573
     * Don't include class namespace in auto-generated CSS class
574
     */
575
    public function baseCSSClasses()
576
    {
577
        return 'AssetAdmin LeftAndMain';
578
    }
579
580
    public function providePermissions()
581
    {
582
        $title = _t("AssetAdmin.MENUTITLE", LeftAndMain::menu_title_for_class($this->class));
0 ignored issues
show
Deprecated Code introduced by
The method LeftAndMain::menu_title_for_class() has been deprecated with message: 5.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
583
584
        return array(
585
            "CMS_ACCESS_AssetAdmin" => array(
586
                'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
0 ignored issues
show
Documentation introduced by
array('title' => $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...
587
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
588
            )
589
        );
590
    }
591
592
    /**
593
     * The form is used to generate a form schema,
594
     * as well as an intermediary object to process data through API endpoints.
595
     * Since it's used directly on API endpoints, it does not have any form actions.
596
     *
597
     * @param Folder $folder
598
     * @return Form
599
     */
600
    public function getFolderEditForm(Folder $folder)
601
    {
602
        $form = Form::create(
603
            $this,
604
            'FolderEditForm',
605
            $folder->getCMSFields(),
606
            FieldList::create()
607
        );
608
609
        return $form;
610
    }
611
612
    /**
613
     * See {@link getFolderEditForm()} for details.
614
     *
615
     * @param int $id
616
     * @return Form
617
     */
618
    public function getFileEditForm($id)
619
    {
620
        /** @var File $file */
621
        $file = $this->getList()->byID($id);
622
623
        // TODO use $file->getCMSFields()
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
624
        $fields = FieldList::create([
625
            HeaderField::create('TitleHeader', $file->Title, 1),
626
            LiteralField::create("ImageFull", $file->PreviewThumbnail()),
627
            TextField::create("Title", _t('AssetTableField.TITLE','Title')),
628
            TextField::create("Name", _t('AssetTableField.FILENAME','Filename')),
629
            HiddenField::create('ID', $id),
630
        ]);
631
        if ($file->getIsImage()) {
632
            $size = sprintf('%spx, %s', $file->getDimensions(), $file->getSize());
633
            $fields->push(ReadonlyField::create("DisplaySize", _t('AssetAdmin.SIZE', "Size"), $size));
634
            $fields->push(LiteralField::create('Link', "<a href=\"{$file->Link()}\">{$file->Link()}</a>"));
635
        }
636
637
        $actions = FieldList::create([
638
            FormAction::create('save', _t('CMSMain.SAVE', 'Save'))
639
                ->setIcon('save'),
640
        ]);
641
642
        $form = Form::create(
643
            $this,
644
            'FileEditForm',
645
            $fields,
646
            $actions
647
        );
648
649
        // Load into form
650
        if($id && $file) {
651
            $form->loadDataFrom($file);
652
        }
653
654
        // Configure form to respond to validation errors with form schema
655
        // if requested via react.
656
        $form->setValidationResponseCallback(function() use ($form) {
657
            return $this->getSchemaResponse($form);
658
        });
659
660
        return $form;
661
    }
662
663
    /**
664
     * Get file edit form
665
     *
666
     * @return Form
667
     */
668
    public function FileEditForm()
669
    {
670
        // Get ID either from posted back value, or url parameter
671
        $request = $this->getRequest();
672
        $id = $request->param('ID') ?: $request->postVar('ID');
673
        return $this->getFileEditForm($id);
674
    }
675
676
    /**
677
     * @param array $data
678
     * @param Form $form
679
     * @return SS_HTTPResponse
680
     */
681
    public function save($data, $form)
682
    {
683 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...
684
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
685
                ->addHeader('Content-Type', 'application/json');
686
        }
687
688
        $id = (int) $data['ID'];
689
        $record = $this->getList()->filter('ID', $id)->first();
690
691
        if (!$record) {
692
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
693
                ->addHeader('Content-Type', 'application/json');
694
        }
695
696
        if (!$record->canEdit()) {
697
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
698
                ->addHeader('Content-Type', 'application/json');
699
        }
700
701
        $form->saveInto($record);
702
        $record->write();
703
704
        // Return the record data in the same response as the schema to save a postback
705
        $schemaData = $this->getSchemaForForm($this->getFileEditForm($id));
706
        $schemaData['record'] = $this->getObjectFromData($record);
707
        $response = new SS_HTTPResponse(\Convert::raw2json($schemaData));
708
        $response->addHeader('Content-Type', 'application/json');
709
        return $response;
710
    }
711
712
    /**
713
     * @param File $file
714
     *
715
     * @return array
716
     */
717
    protected function getObjectFromData(File $file)
718
    {
719
        $object = array(
720
            'id' => $file->ID,
721
            'created' => $file->Created,
722
            'lastUpdated' => $file->LastEdited,
723
            'owner' => null,
724
            'parent' => null,
725
            'title' => $file->Title,
726
            'exists' => $file->exists(), // Broken file check
727
            'type' => $file->is_a('Folder') ? 'folder' : $file->FileType,
728
            'category' => $file->is_a('Folder') ? 'folder' : $file->appCategory(),
729
            'name' => $file->Name,
730
            'filename' => $file->Filename,
731
            '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...
732
            'size' => $file->Size,
733
            'url' => $file->AbsoluteURL,
734
            'canEdit' => $file->canEdit(),
735
            'canDelete' => $file->canDelete()
736
        );
737
738
        /** @var Member $owner */
739
        $owner = $file->Owner();
740
741
        if ($owner) {
742
            $object['owner'] = array(
743
                'id' => $owner->ID,
744
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
745
            );
746
        }
747
748
        /** @var Folder $parent */
749
        $parent = $file->Parent();
750
751
        if ($parent) {
752
            $object['parent'] = array(
753
                'id' => $parent->ID,
754
                'title' => $parent->Title,
755
                'filename' => $parent->Filename,
756
            );
757
        }
758
759
        /** @var File $file */
760
        if ($file->getIsImage()) {
761
            $object['dimensions']['width'] = $file->Width;
762
            $object['dimensions']['height'] = $file->Height;
0 ignored issues
show
Bug introduced by
The property Height does not seem to exist. Did you mean cms_thumbnail_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...
763
        }
764
765
        return $object;
766
    }
767
768
769
    /**
770
     * Returns the files and subfolders contained in the currently selected folder,
771
     * defaulting to the root node. Doubles as search results, if any search parameters
772
     * are set through {@link SearchForm()}.
773
     *
774
     * @param array $params Unsanitised request parameters
775
     * @return SS_List
776
     */
777
    protected function getList($params = array())
778
    {
779
        $context = $this->getSearchContext();
780
781
        // Overwrite name filter to search both Name and Title attributes
782
        $context->removeFilterByName('Name');
783
784
        // Lazy loaded list. Allows adding new filters through SearchContext.
785
        $list = $context->getResults($params);
786
787
        // Re-add previously removed "Name" filter as combined filter
788
        // TODO Replace with composite SearchFilter once that API exists
789
        if(!empty($params['Name'])) {
790
            $list = $list->filterAny(array(
791
                'Name:PartialMatch' => $params['Name'],
792
                'Title:PartialMatch' => $params['Name']
793
            ));
794
        }
795
796
        // Optionally limit search to a folder (non-recursive)
797
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
798
            $list = $list->filter('ParentID', $params['ParentID']);
799
        }
800
801
        // Date filtering
802
        if (!empty($params['CreatedFrom'])) {
803
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
804
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
805
        }
806
        if (!empty($params['CreatedTo'])) {
807
            $toDate = new DateField(null, null, $params['CreatedTo']);
808
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
809
        }
810
811
        // Categories
812
        if (!empty($filters['AppCategory']) && !empty(File::config()->app_categories[$filters['AppCategory']])) {
0 ignored issues
show
Bug introduced by
The variable $filters seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

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

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

Loading history...
813
            $extensions = File::config()->app_categories[$filters['AppCategory']];
814
            $list = $list->filter('Name:PartialMatch', $extensions);
815
        }
816
817
        // Sort folders first
818
        $list = $list->sort(
819
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
820
        );
821
822
        // Pagination
823
        if (isset($filters['page']) && isset($filters['limit'])) {
824
            $page = $filters['page'];
825
            $limit = $filters['limit'];
826
            $offset = ($page - 1) * $limit;
827
            $list = $list->limit($limit, $offset);
828
        }
829
830
        // Access checks
831
        $list = $list->filterByCallback(function($file) {return $file->canView();});
832
833
        return $list;
834
    }
835
836
    /**
837
     * @return Upload
838
     */
839
    protected function getUpload()
840
    {
841
        $upload = Upload::create();
842
        $upload->getValidator()->setAllowedExtensions(
843
            // filter out '' since this would be a regex problem on JS end
844
            array_filter(Config::inst()->get('File', 'allowed_extensions'))
845
        );
846
847
        return $upload;
848
    }
849
}
850