Completed
Pull Request — master (#202)
by Ingo
02:38
created

AssetAdmin::breadcrumbs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
namespace SilverStripe\AssetAdmin\Controller;
4
5
use SilverStripe\Filesystem\Storage\AssetNameGenerator;
6
use LeftAndMain;
7
use PermissionProvider;
8
use DateField;
9
use DropdownField;
10
use Controller;
11
use FieldList;
12
use Form;
13
use CheckboxField;
14
use File;
15
use Requirements;
16
use CMSBatchActionHandler;
17
use DataObject;
18
use Injector;
19
use Folder;
20
use CMSForm;
21
use SS_List;
22
use SSViewer;
23
use HeaderField;
24
use FieldGroup;
25
use SecurityToken;
26
use SS_HTTPRequest;
27
use SS_HTTPResponse;
28
use Upload;
29
use Config;
30
31
/**
32
 * AssetAdmin is the 'file store' section of the CMS.
33
 * It provides an interface for manipulating the File and Folder objects in the system.
34
 *
35
 * @package cms
36
 * @subpackage assets
37
 */
38
class AssetAdmin extends LeftAndMain implements PermissionProvider
39
{
40
    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...
41
42
    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...
43
44
    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...
45
46
    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...
47
48
    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...
49
        // Legacy redirect for SS3-style detail view
50
        'EditForm/field/File/item/$FileID/$Action' => 'legacyRedirectForEditView',
51
        // Pass all URLs to the index, for React to unpack
52
        'show/$FolderID/edit/$FileID' => 'index',
53
        // API access points with structured data
54
        'POST api/createFolder' => 'apiCreateFolder',
55
        'POST api/createFile' => 'apiCreateFile',
56
        'GET api/readFolder' => 'apiReadFolder',
57
        'PUT api/updateFolder' => 'apiUpdateFolder',
58
        'PUT api/updateFile' => 'apiUpdateFile',
59
        'DELETE api/delete' => 'apiDelete',
60
        'GET api/search' => 'apiSearch',
61
    ];
62
63
    /**
64
     * Amount of results showing on a single page.
65
     *
66
     * @config
67
     * @var int
68
     */
69
    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...
70
71
    /**
72
     * @config
73
     * @see Upload->allowedMaxFileSize
74
     * @var int
75
     */
76
    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...
77
78
    /**
79
     * @var array
80
     */
81
    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...
82
        'legacyRedirectForEditView',
83
        'apiCreateFolder',
84
        'apiCreateFile',
85
        'apiReadFolder',
86
        'apiUpdateFolder',
87
        'apiUpdateFile',
88
        'apiDelete',
89
        'apiSearch',
90
    );
91
92
    /**
93
     * Set up the controller
94
     */
95
    public function init()
96
    {
97
        parent::init();
98
99
        Requirements::javascript(FRAMEWORK_DIR . "/admin/client/dist/js/bundle-lib.js");
100
        Requirements::add_i18n_javascript(ASSET_ADMIN_DIR . '/client/lang', false, true);
101
        Requirements::javascript(ASSET_ADMIN_DIR . "/client/dist/js/bundle.js");
102
        Requirements::css(ASSET_ADMIN_DIR . "/client/dist/styles/bundle.css");
103
104
        CMSBatchActionHandler::register('delete', 'SilverStripe\AssetAdmin\BatchAction\DeleteAssets', 'Folder');
105
    }
106
107
	public function getClientConfig() {
108
        $baseLink = $this->Link();
109
		return array_merge( parent::getClientConfig(), [
110
            'assetsRoute' => $this->Link() . ':folderAction?/:folderId?/:fileAction?/:fileId?',
111
            'assetsRouteHome' => $this->Link() . 'show/0',
112
            'createFileEndpoint' => [
113
                'url' => Controller::join_links($baseLink, 'api/createFile'),
114
                'method' => 'post',
115
                'payloadFormat' => 'urlencoded',
116
            ],
117
            'createFolderEndpoint' => [
118
                'url' => Controller::join_links($baseLink, 'api/createFolder'),
119
                'method' => 'post',
120
                'payloadFormat' => 'urlencoded',
121
            ],
122
            'readFolderEndpoint' => [
123
                'url' => Controller::join_links($baseLink, 'api/readFolder'),
124
                'method' => 'get',
125
                'responseFormat' => 'json',
126
            ],
127
            'searchEndpoint' => [
128
                'url' => Controller::join_links($baseLink, 'api/search'),
129
                'method' => 'get',
130
                'responseFormat' => 'json',
131
            ],
132
            'updateFileEndpoint' => [
133
                'url' => Controller::join_links($baseLink, 'api/updateFile'),
134
                'method' => 'put',
135
                'payloadFormat' => 'urlencoded',
136
            ],
137
            'updateFolderEndpoint' => [
138
                'url' => Controller::join_links($baseLink, 'api/updateFolder'),
139
                'method' => 'put',
140
                'payloadFormat' => 'urlencoded',
141
            ],
142
            'deleteEndpoint' => [
143
                'url' => Controller::join_links($baseLink, 'api/delete'),
144
                'method' => 'delete',
145
                'payloadFormat' => 'urlencoded',
146
            ],
147
            'limit' => $this->config()->page_length,
148
        ]);
149
	}
150
151
    /**
152
     * Fetches a collection of files by ParentID.
153
     *
154
     * @param SS_HTTPRequest $request
155
     * @return SS_HTTPResponse
156
     */
157
    public function apiReadFolder(SS_HTTPRequest $request)
158
    {
159
        $params = $request->requestVars();
160
        $items = array();
161
        $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...
162
        $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...
163
164
        if (!isset($params['id']) && !strlen($params['id'])) {
165
            $this->httpError(400);
166
        }
167
168
        $folderID = (int)$params['id'];
169
        if ($folderID === 0) {
170
            $folder = new Folder();
171
        } else {
172
            $folder = Folder::get()->byID($folderID);
173
        }
174
175
        // TODO Limit results to avoid running out of memory (implement client-side pagination)
176
        $files = $this->getList()->filter('ParentID', $folderID);
177
178
        if ($files) {
179
            foreach ($files as $file) {
180
                if (!$file->canView()) {
181
                    continue;
182
                }
183
184
                $items[] = $this->getObjectFromData($file);
185
            }
186
        }
187
188
        $response = new SS_HTTPResponse();
189
        $response->addHeader('Content-Type', 'application/json');
190
        $response->setBody(json_encode([
191
            'files' => $items,
192
            'count' => count($items),
193
            'parent' => $folder->ParentID, // grandparent
194
            'folderID' => $folderID,
195
            'canEdit' => $folder ? $folder->canEdit() : false,
196
            'canDelete' => $folder ? $folder->canDelete() : false
197
        ]));
198
199
        return $response;
200
    }
201
202
    /**
203
     * @param SS_HTTPRequest $request
204
     *
205
     * @return SS_HTTPResponse
206
     */
207
    public function apiSearch(SS_HTTPRequest $request)
208
    {
209
        $params = $request->getVars();
210
        $list = $this->getList($params);
211
212
        $response = new SS_HTTPResponse();
213
        $response->addHeader('Content-Type', 'application/json');
214
        $response->setBody(json_encode([
215
            // Serialisation
216
            "files" => array_map(function($file) {
217
                return $this->getObjectFromData($file);
218
            }, $list->toArray()),
219
            "count" => $list->count(),
220
        ]));
221
222
        return $response;
223
    }
224
225
    /**
226
     * @param SS_HTTPRequest $request
227
     *
228
     * @return SS_HTTPResponse
229
     */
230
    public function apiUpdateFile(SS_HTTPRequest $request)
231
    {
232
        parse_str($request->getBody(), $data);
233
234
        // CSRF check
235
        $token = SecurityToken::inst();
236 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...
237
            return new SS_HTTPResponse(null, 400);
238
        }
239
240 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...
241
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
242
                ->addHeader('Content-Type', 'application/json');
243
        }
244
245
        $id = $data['id'];
246
        $record = $this->getList()->filter('ID', (int) $id)->first();
247
248 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...
249
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
250
                ->addHeader('Content-Type', 'application/json');
251
        }
252
253 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...
254
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
255
                ->addHeader('Content-Type', 'application/json');
256
        }
257
258
        // TODO Use same property names and capitalisation as DataObject
259
        if (!empty($data['title'])) {
260
            $record->Title = $data['title'];
261
        }
262
263
        // TODO Use same property names and capitalisation as DataObject
264
        if (!empty($data['basename'])) {
265
            $record->Name = $data['basename'];
266
        }
267
268
        $record->write();
269
270
        return (new SS_HTTPResponse(json_encode(['status' => 'ok']), 200))
271
            ->addHeader('Content-Type', 'application/json');
272
    }
273
274
    /**
275
     * @param SS_HTTPRequest $request
276
     *
277
     * @return SS_HTTPResponse
278
     */
279
    public function apiDelete(SS_HTTPRequest $request)
280
    {
281
        parse_str($request->getBody(), $vars);
282
283
        // CSRF check
284
        $token = SecurityToken::inst();
285 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...
286
            return new SS_HTTPResponse(null, 400);
287
        }
288
289 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...
290
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 400))
291
                ->addHeader('Content-Type', 'application/json');
292
        }
293
294
        $fileIds = $vars['ids'];
295
        $files = $this->getList()->filter("ID", $fileIds)->toArray();
296
297 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...
298
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 404))
299
                ->addHeader('Content-Type', 'application/json');
300
        }
301
302
        if (!min(array_map(function ($file) {
303
            return $file->canDelete();
304
        }, $files))) {
305
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 401))
306
                ->addHeader('Content-Type', 'application/json');
307
        }
308
309
        foreach ($files as $file) {
310
            $file->delete();
311
        }
312
313
        return (new SS_HTTPResponse(json_encode(['status' => 'file was deleted'])))
314
            ->addHeader('Content-Type', 'application/json');
315
    }
316
317
    public function apiCreateFile(SS_HTTPRequest $request)
318
    {
319
        $class = 'File';
0 ignored issues
show
Unused Code introduced by
$class 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...
320
        $data = $request->postVars();
321
        $upload = $this->getUpload();
322
323
        // CSRF check
324
        $token = SecurityToken::inst();
325 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...
326
            return new SS_HTTPResponse(null, 400);
327
        }
328
329
        // check canAddChildren permissions
330
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
331
            $parentRecord = Folder::get()->byID($data['ParentID']);
332 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...
333
                return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
334
                    ->addHeader('Content-Type', 'application/json');
335
            }
336
        } else {
337
            $parentRecord = singleton('Folder');
338
        }
339
340
        // check create permissions
341
        if (!$parentRecord->canCreate()) {
342
            return (new SS_HTTPResponse(json_encode(['status' => 'error']), 403))
343
                ->addHeader('Content-Type', 'application/json');
344
        }
345
346
        $tmpFile = $request->postVar('Upload');
347
        if(!$upload->validate($tmpFile)) {
348
            $result = ['error' => $upload->getErrors()];
349
            return (new SS_HTTPResponse(json_encode($result), 400))
350
                ->addHeader('Content-Type', 'application/json');
351
        }
352
353
        // TODO Allow batch uploads
354
355
        $file = File::create();
356
        $uploadResult = $upload->loadIntoFile($tmpFile, $file, $parentRecord ? $parentRecord->getFilename() : '/');
357 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...
358
            $result = ['error' => 'unknown'];
359
            return (new SS_HTTPResponse(json_encode($result), 400))
360
                ->addHeader('Content-Type', 'application/json');
361
        }
362
363
        $file->ParentID = $parentRecord->ID;
364
        $file->write();
365
366
        $result = [$this->getObjectFromData($file)];
367
368
        return (new SS_HTTPResponse(json_encode($result)))
369
            ->addHeader('Content-Type', 'application/json');
370
    }
371
372
    public function apiCreateFolder(SS_HTTPRequest $request)
373
    {
374
        $data = $request->postVars();
375
376
        $class = 'Folder';
377
378
        // CSRF check
379
        $token = SecurityToken::inst();
380 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...
381
            return new SS_HTTPResponse(null, 400);
382
        }
383
384
        // check addchildren permissions
385
        if (!empty($data['ParentID']) && is_numeric($data['ParentID'])) {
386
            $parentRecord = \DataObject::get_by_id($class, $data['ParentID']);
387 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...
388
                return (new SS_HTTPResponse(null, 403))
389
                    ->addHeader('Content-Type', 'application/json');
390
            }
391
        } else {
392
            $parentRecord = singleton($class);
393
        }
394
        $data['ParentID'] = ($parentRecord->exists()) ? (int)$parentRecord->ID : 0;
395
396
        // check create permissions
397
        if (!$parentRecord->canCreate()) {
398
            return (new SS_HTTPResponse(null, 403))
399
                ->addHeader('Content-Type', 'application/json');
400
        }
401
402
        // Build filename
403
        $baseFilename = isset($data['Name'])
404
            ? basename($data['Name'])
405
            : _t('AssetAdmin.NEWFOLDER', "NewFolder");
406
407
        if ($parentRecord && $parentRecord->ID) {
408
            $baseFilename = $parentRecord->getFilename() . '/' . $baseFilename;
409
        }
410
411
        // Ensure name is unique
412
        $nameGenerator = $this->getNameGenerator($baseFilename);
413
        foreach ($nameGenerator as $filename) {
414
            if (! File::find($filename)) {
415
                break;
416
            }
417
        }
418
        $data['Name'] = basename($filename);
0 ignored issues
show
Bug introduced by
The variable $filename seems to be defined by a foreach iteration on line 413. 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...
419
420
        // Create record
421
        $record = $class::create();
422
        $record->ParentID = $data['ParentID'];
423
        $record->Name = $record->Title = basename($data['Name']);
424
        $record->write();
425
426
        $result = [
427
            "ParentID" => $record->ParentID,
428
            "ID" => $record->ID,
429
            "Filename" => $record->Filename,
430
        ];
431
432
        return (new SS_HTTPResponse(json_encode($result)))->addHeader('Content-Type', 'application/json');
433
    }
434
435
    public function legacyRedirectForEditView($request)
436
    {
437
        $fileID = $request->param('FileID');
438
        $file = File::get()->byID($fileID);
439
        $link = $this->getFileEditLink($file) ?: $this->Link();
0 ignored issues
show
Documentation introduced by
$file is of type object<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...
440
        $this->redirect($link);
441
    }
442
443
    /**
444
     * Given a file return the CMS link to edit it
445
     *
446
     * @param File $file
447
     * @return string
448
     */
449
    public function getFileEditLink($file) {
450
        if(!$file || !$file->isInDB()) {
451
            return null;
452
        }
453
454
        return Controller::join_links(
455
            $this->Link('show'),
456
            $file->ParentID,
457
            'edit',
458
            $file->ID
459
        );
460
    }
461
462
    /**
463
     * Get the search context from {@link File}, used to create the search form
464
     * as well as power the /search API endpoint.
465
     *
466
     * @return SearchContext
467
     */
468
    public function getSearchContext()
469
    {
470
        $context = singleton('File')->getDefaultSearchContext();
471
472
        // Customize fields
473
        $dateHeader = HeaderField::create('Date', _t('CMSSearch.FILTERDATEHEADING', 'Date'), 4);
474
        $dateFrom = DateField::create('CreatedFrom', _t('CMSSearch.FILTERDATEFROM', 'From'))
475
        ->setConfig('showcalendar', true);
476
        $dateTo = DateField::create('CreatedTo', _t('CMSSearch.FILTERDATETO', 'To'))
477
        ->setConfig('showcalendar', true);
478
        $dateGroup = FieldGroup::create(
479
            $dateHeader,
480
            $dateFrom,
481
            $dateTo
482
        );
483
        $context->addField($dateGroup);
484
        $appCategories = array(
485
            'archive' => _t('AssetAdmin.AppCategoryArchive', 'Archive', 'A collection of files'),
486
            'audio' => _t('AssetAdmin.AppCategoryAudio', 'Audio'),
487
            'document' => _t('AssetAdmin.AppCategoryDocument', 'Document'),
488
            'flash' => _t('AssetAdmin.AppCategoryFlash', 'Flash', 'The fileformat'),
489
            'image' => _t('AssetAdmin.AppCategoryImage', 'Image'),
490
            'video' => _t('AssetAdmin.AppCategoryVideo', 'Video'),
491
        );
492
        $context->addField(
493
            $typeDropdown = new DropdownField(
494
                'AppCategory',
495
                _t('AssetAdmin.Filetype', 'File type'),
496
                $appCategories
497
            )
498
        );
499
500
        $typeDropdown->setEmptyString(' ');
501
502
        $context->addField(
503
            new CheckboxField('CurrentFolderOnly', _t('AssetAdmin.CurrentFolderOnly', 'Limit to current folder?'))
504
        );
505
        $context->getFields()->removeByName('Title');
506
507
        return $context;
508
    }
509
510
    /**
511
     * Get an asset renamer for the given filename.
512
     *
513
     * @param  string             $filename Path name
514
     * @return AssetNameGenerator
515
     */
516
    protected function getNameGenerator($filename)
517
    {
518
        return Injector::inst()
519
            ->createWithArgs('AssetNameGenerator', array($filename));
520
    }
521
522
    /**
523
     * @todo Implement on client
524
     */
525
    public function breadcrumbs($unlinked = false)
526
    {
527
        return null;
528
    }
529
530
531
    /**
532
     * Don't include class namespace in auto-generated CSS class
533
     */
534
    public function baseCSSClasses()
535
    {
536
        return 'AssetAdmin LeftAndMain';
537
    }
538
539
    /**
540
     * Don't include class namespace in template names
541
     * @todo Make code in framework more namespace-savvy so that we don't need this duplication
542
     */
543
    public function getTemplatesWithSuffix($suffix)
544
    {
545
        $className = get_class($this);
546
        $baseClass = 'LeftandMain';
547
548
        $templates = array();
549
        $classes = array_reverse(\ClassInfo::ancestry($className));
550
        foreach ($classes as $class) {
551
            $template = (new \ReflectionClass($class))->getShortName() . $suffix;
552
            if (\SSViewer::hasTemplate($template)) {
0 ignored issues
show
Documentation introduced by
$template is of type string, but the function expects a array.

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...
553
                $templates[] = $template;
554
            }
555
556
            // If the class is "Page_Controller", look for Page.ss
557
            if (stripos($class, '_controller') !== false) {
558
                $template = str_ireplace('_controller', '', $class) . $suffix;
559
                if (\SSViewer::hasTemplate($template)) {
0 ignored issues
show
Documentation introduced by
$template is of type string, but the function expects a array.

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...
560
                    $templates[] = $template;
561
                }
562
            }
563
564
            if ($baseClass && $class == $baseClass) {
565
                break;
566
            }
567
        }
568
569
        return $templates;
570
    }
571
572
    public function providePermissions()
573
    {
574
        $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...
575
576
        return array(
577
            "CMS_ACCESS_AssetAdmin" => array(
578
                '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...
579
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
580
            )
581
        );
582
    }
583
584
    /**
585
     * The form is used to generate a form schema,
586
     * as well as an intermediary object to process data through API endpoints.
587
     * Since it's used directly on API endpoints, it does not have any form actions.
588
     *
589
     * @param Folder
590
     * @return Form
591
     */
592
    protected function getFolderEditForm(Folder $folder)
593
    {
594
        $form = Form::create(
595
            $this,
596
            'FolderEditForm',
597
            $folder->getCMSFields(),
598
            FieldList::create()
599
        );
600
601
        return $form;
602
    }
603
604
    /**
605
     * See {@link getFolderEditForm()} for details.
606
     *
607
     * @param File $file
608
     * @return Form
609
     */
610
    protected function getFileEditForm(File $file)
611
    {
612
        $form = Form::create(
613
            $this,
614
            'FileEditForm',
615
            $file->getCMSFields(),
616
            FieldList::create()
617
        );
618
619
        return $form;
620
    }
621
622
    /**
623
     * @param File $file
624
     *
625
     * @return array
626
     */
627
    protected function getObjectFromData(File $file)
628
    {
629
        $object = array(
630
            'id' => $file->ID,
631
            'created' => $file->Created,
632
            'lastUpdated' => $file->LastEdited,
633
            'owner' => null,
634
            'parent' => null,
635
            'attributes' => array(
636
                'dimensions' => array(),
637
            ),
638
            'title' => $file->Title,
639
            'type' => $file->is_a('Folder') ? 'folder' : $file->FileType,
640
            'category' => $file->is_a('Folder') ? 'folder' : $file->appCategory(),
641
            'name' => $file->Name,
642
            'filename' => $file->Filename,
643
            '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...
644
            'size' => $file->Size,
645
            'url' => $file->AbsoluteURL,
646
            'canEdit' => $file->canEdit(),
647
            'canDelete' => $file->canDelete()
648
        );
649
650
        /** @var Member $owner */
651
        $owner = $file->Owner();
652
653
        if ($owner) {
654
            $object['owner'] = array(
655
                'id' => $owner->ID,
656
                'title' => trim($owner->FirstName . ' ' . $owner->Surname),
657
            );
658
        }
659
660
        /** @var Folder $parent */
661
        $parent = $file->Parent();
662
663
        if ($parent) {
664
            $object['parent'] = array(
665
                'id' => $parent->ID,
666
                'title' => $parent->Title,
667
                'filename' => $parent->Filename,
668
            );
669
        }
670
671
        /** @var File $file */
672
        if ($file->hasMethod('getWidth') && $file->hasMethod('getHeight')) {
673
            $object['attributes']['dimensions']['width'] = $file->Width;
674
            $object['attributes']['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...
675
        }
676
677
        return $object;
678
    }
679
680
681
    /**
682
     * Returns the files and subfolders contained in the currently selected folder,
683
     * defaulting to the root node. Doubles as search results, if any search parameters
684
     * are set through {@link SearchForm()}.
685
     *
686
     * @param array $params Unsanitised request parameters
687
     * @return SS_List
688
     */
689
    protected function getList($params = array())
690
    {
691
        $context = $this->getSearchContext();
692
693
        // Overwrite name filter to search both Name and Title attributes
694
        $context->removeFilterByName('Name');
695
696
        // Lazy loaded list. Allows adding new filters through SearchContext.
697
        $list = $context->getResults($params);
698
699
        // Re-add previously removed "Name" filter as combined filter
700
        // TODO Replace with composite SearchFilter once that API exists
701
        if(!empty($params['Name'])) {
702
            $list = $list->filterAny(array(
703
                'Name:PartialMatch' => $params['Name'],
704
                'Title:PartialMatch' => $params['Name']
705
            ));
706
        }
707
708
        // Optionally limit search to a folder (non-recursive)
709
        if(!empty($params['ParentID']) && is_numeric($params['ParentID'])) {
710
            $list = $list->filter('ParentID', $params['ParentID']);
711
        }
712
713
        // Date filtering
714
        if (!empty($params['CreatedFrom'])) {
715
            $fromDate = new DateField(null, null, $params['CreatedFrom']);
716
            $list = $list->filter("Created:GreaterThanOrEqual", $fromDate->dataValue().' 00:00:00');
717
        }
718
        if (!empty($params['CreatedTo'])) {
719
            $toDate = new DateField(null, null, $params['CreatedTo']);
720
            $list = $list->filter("Created:LessThanOrEqual", $toDate->dataValue().' 23:59:59');
721
        }
722
723
        // Categories
724
        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...
725
            $extensions = File::config()->app_categories[$filters['AppCategory']];
726
            $list = $list->filter('Name:PartialMatch', $extensions);
727
        }
728
729
        // Sort folders first
730
        $list = $list->sort(
731
            '(CASE WHEN "File"."ClassName" = \'Folder\' THEN 0 ELSE 1 END), "Name"'
732
        );
733
734
        // Pagination
735
        if (isset($filters['page']) && isset($filters['limit'])) {
736
            $page = $filters['page'];
737
            $limit = $filters['limit'];
738
            $offset = ($page - 1) * $limit;
739
            $list = $list->limit($limit, $offset);
740
        }
741
742
        // Access checks
743
        $list = $list->filterByCallback(function($file) {return $file->canView();});
744
745
        return $list;
746
    }
747
748
    /**
749
     * @return Upload
750
     */
751
    protected function getUpload()
752
    {
753
        $upload = Upload::create();
754
        $upload->getValidator()->setAllowedExtensions(
755
            // filter out '' since this would be a regex problem on JS end
756
            array_filter(Config::inst()->get('File', 'allowed_extensions'))
757
        );
758
759
        return $upload;
760
    }
761
}
762