Completed
Push — master ( f39c4d...b2e354 )
by Sam
03:35 queued 03:17
created

DBFile::setField()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 3
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Assets\Storage;
4
5
use SilverStripe\Assets\File;
6
use SilverStripe\Assets\Thumbnail;
7
use SilverStripe\Assets\ImageManipulation;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Forms\AssetField;
11
use SilverStripe\ORM\ValidationResult;
12
use SilverStripe\ORM\ValidationException;
13
use SilverStripe\ORM\FieldType\DBComposite;
14
use SilverStripe\Security\Permission;
15
16
/**
17
 * Represents a file reference stored in a database
18
 *
19
 * @property string $Hash SHA of the file
20
 * @property string $Filename Name of the file, including directory
21
 * @property string $Variant Variant of the file
22
 */
23
class DBFile extends DBComposite implements AssetContainer, Thumbnail
24
{
25
26
    use ImageManipulation;
27
28
    /**
29
     * List of allowed file categories.
30
     *
31
     * {@see File::$app_categories}
32
     *
33
     * @var array
34
     */
35
    protected $allowedCategories = array();
36
37
    /**
38
     * List of image mime types supported by the image manipulations API
39
     *
40
     * {@see File::app_categories} for matching extensions.
41
     *
42
     * @config
43
     * @var array
44
     */
45
    private static $supported_images = array(
46
        'image/jpeg',
47
        'image/gif',
48
        'image/png'
49
    );
50
51
    /**
52
     * Create a new image manipulation
53
     *
54
     * @param string $name
55
     * @param array|string $allowed List of allowed file categories (not extensions), as per File::$app_categories
56
     */
57
    public function __construct($name = null, $allowed = array())
58
    {
59
        parent::__construct($name);
60
        $this->setAllowedCategories($allowed);
61
    }
62
63
    /**
64
     * Determine if a valid non-empty image exists behind this asset, which is a format
65
     * compatible with image manipulations
66
     *
67
     * @return boolean
68
     */
69
    public function getIsImage()
70
    {
71
        // Check file type
72
        $mime = $this->getMimeType();
73
        return $mime && in_array($mime, $this->config()->supported_images);
0 ignored issues
show
Bug Best Practice introduced by
The expression $mime of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
74
    }
75
76
    /**
77
     * @return AssetStore
78
     */
79
    protected function getStore()
80
    {
81
        return Injector::inst()->get('AssetStore');
82
    }
83
84
    private static $composite_db = array(
85
        "Hash" => "Varchar(255)", // SHA of the base content
86
        "Filename" => "Varchar(255)", // Path identifier of the base content
87
        "Variant" => "Varchar(255)", // Identifier of the variant to the base, if given
88
    );
89
90
    private static $casting = array(
91
        'URL' => 'Varchar',
92
        'AbsoluteURL' => 'Varchar',
93
        'Basename' => 'Varchar',
94
        'Title' => 'Varchar',
95
        'MimeType' => 'Varchar',
96
        'String' => 'Text',
97
        'Tag' => 'HTMLFragment',
98
        'Size' => 'Varchar'
99
    );
100
101
    public function scaffoldFormField($title = null, $params = null)
102
    {
103
        return AssetField::create($this->getName(), $title);
104
    }
105
106
    /**
107
     * Return a html5 tag of the appropriate for this file (normally img or a)
108
     *
109
     * @return string
110
     */
111
    public function XML()
112
    {
113
        return $this->getTag() ?: '';
114
    }
115
116
    /**
117
     * Return a html5 tag of the appropriate for this file (normally img or a)
118
     *
119
     * @return string
120
     */
121
    public function getTag()
122
    {
123
        $template = $this->getFrontendTemplate();
124
        if (empty($template)) {
125
            return '';
126
        }
127
        return (string)$this->renderWith($template);
128
    }
129
130
    /**
131
     * Determine the template to render as on the frontend
132
     *
133
     * @return string Name of template
134
     */
135
    public function getFrontendTemplate()
136
    {
137
        // Check that path is available
138
        $url = $this->getURL();
139
        if (empty($url)) {
140
            return null;
141
        }
142
143
        // Image template for supported images
144
        if ($this->getIsImage()) {
145
            return 'DBFile_image';
146
        }
147
148
        // Default download
149
        return 'DBFile_download';
150
    }
151
152
    /**
153
     * Get trailing part of filename
154
     *
155
     * @return string
156
     */
157
    public function getBasename()
158
    {
159
        if (!$this->exists()) {
160
            return null;
161
        }
162
        return basename($this->getSourceURL());
163
    }
164
165
    /**
166
     * Get file extension
167
     *
168
     * @return string
169
     */
170
    public function getExtension()
171
    {
172
        if (!$this->exists()) {
173
            return null;
174
        }
175
        return pathinfo($this->Filename, PATHINFO_EXTENSION);
176
    }
177
178
    /**
179
     * Alt title for this
180
     *
181
     * @return string
182
     */
183
    public function getTitle()
184
    {
185
        // If customised, use the customised title
186
        if ($this->failover && ($title = $this->failover->Title)) {
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<SilverStripe\View\ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
187
            return $title;
188
        }
189
        // fallback to using base name
190
        return $this->getBasename();
191
    }
192
193
    public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array())
194
    {
195
        $this->assertFilenameValid($filename ?: $path);
196
        $result = $this
197
            ->getStore()
198
            ->setFromLocalFile($path, $filename, $hash, $variant, $config);
199
        // Update from result
200
        if ($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
201
            $this->setValue($result);
202
        }
203
        return $result;
204
    }
205
206
    public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array())
207
    {
208
        $this->assertFilenameValid($filename);
209
        $result = $this
210
            ->getStore()
211
            ->setFromStream($stream, $filename, $hash, $variant, $config);
212
        // Update from result
213
        if ($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
214
            $this->setValue($result);
215
        }
216
        return $result;
217
    }
218
219
    public function setFromString($data, $filename, $hash = null, $variant = null, $config = array())
220
    {
221
        $this->assertFilenameValid($filename);
222
        $result = $this
223
            ->getStore()
224
            ->setFromString($data, $filename, $hash, $variant, $config);
225
        // Update from result
226
        if ($result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
227
            $this->setValue($result);
228
        }
229
        return $result;
230
    }
231
232
    public function getStream()
233
    {
234
        if (!$this->exists()) {
235
            return null;
236
        }
237
        return $this
238
            ->getStore()
239
            ->getAsStream($this->Filename, $this->Hash, $this->Variant);
240
    }
241
242
    public function getString()
243
    {
244
        if (!$this->exists()) {
245
            return null;
246
        }
247
        return $this
248
            ->getStore()
249
            ->getAsString($this->Filename, $this->Hash, $this->Variant);
250
    }
251
252
    public function getURL($grant = true)
253
    {
254
        if (!$this->exists()) {
255
            return null;
256
        }
257
        $url = $this->getSourceURL($grant);
258
        $this->updateURL($url);
259
        $this->extend('updateURL', $url);
260
        return $url;
261
    }
262
263
    /**
264
     * Get URL, but without resampling.
265
     * Note that this will return the url even if the file does not exist.
266
     *
267
     * @param bool $grant Ensures that the url for any protected assets is granted for the current user.
268
     * @return string
269
     */
270
    public function getSourceURL($grant = true)
271
    {
272
        return $this
273
            ->getStore()
274
            ->getAsURL($this->Filename, $this->Hash, $this->Variant, $grant);
275
    }
276
277
    /**
278
     * Get the absolute URL to this resource
279
     *
280
     * @return string
281
     */
282
    public function getAbsoluteURL()
283
    {
284
        if (!$this->exists()) {
285
            return null;
286
        }
287
        return Director::absoluteURL($this->getURL());
288
    }
289
290
    public function getMetaData()
291
    {
292
        if (!$this->exists()) {
293
            return null;
294
        }
295
        return $this
296
            ->getStore()
297
            ->getMetadata($this->Filename, $this->Hash, $this->Variant);
298
    }
299
300
    public function getMimeType()
301
    {
302
        if (!$this->exists()) {
303
            return null;
304
        }
305
        return $this
306
            ->getStore()
307
            ->getMimeType($this->Filename, $this->Hash, $this->Variant);
308
    }
309
310
    public function getValue()
311
    {
312
        if (!$this->exists()) {
313
            return null;
314
        }
315
        return array(
316
            'Filename' => $this->Filename,
317
            'Hash' => $this->Hash,
318
            'Variant' => $this->Variant
319
        );
320
    }
321
322
    public function getVisibility()
323
    {
324
        if (empty($this->Filename)) {
325
            return null;
326
        }
327
        return $this
328
            ->getStore()
329
            ->getVisibility($this->Filename, $this->Hash);
330
    }
331
332
    public function exists()
333
    {
334
        if (empty($this->Filename)) {
335
            return false;
336
        }
337
        return $this
338
            ->getStore()
339
            ->exists($this->Filename, $this->Hash, $this->Variant);
340
    }
341
342
    public function getFilename()
343
    {
344
        return $this->getField('Filename');
345
    }
346
347
    public function getHash()
348
    {
349
        return $this->getField('Hash');
350
    }
351
352
    public function getVariant()
353
    {
354
        return $this->getField('Variant');
355
    }
356
357
    /**
358
     * Return file size in bytes.
359
     *
360
     * @return int
361
     */
362
    public function getAbsoluteSize()
363
    {
364
        $metadata = $this->getMetaData();
365
        if (isset($metadata['size'])) {
366
            return $metadata['size'];
367
        }
368
        return 0;
369
    }
370
371
    /**
372
     * Customise this object with an "original" record for getting other customised fields
373
     *
374
     * @param AssetContainer $original
375
     * @return $this
376
     */
377
    public function setOriginal($original)
378
    {
379
        $this->failover = $original;
0 ignored issues
show
Documentation Bug introduced by
It seems like $original of type object<SilverStripe\Asse...Storage\AssetContainer> is incompatible with the declared type object<SilverStripe\View\ViewableData> of property $failover.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
380
        return $this;
381
    }
382
383
    /**
384
     * Get list of allowed file categories
385
     *
386
     * @return array
387
     */
388
    public function getAllowedCategories()
389
    {
390
        return $this->allowedCategories;
391
    }
392
393
    /**
394
     * Assign allowed categories
395
     *
396
     * @param array|string $categories
397
     * @return $this
398
     */
399
    public function setAllowedCategories($categories)
400
    {
401
        if (is_string($categories)) {
402
            $categories = preg_split('/\s*,\s*/', $categories);
403
        }
404
        $this->allowedCategories = (array)$categories;
405
        return $this;
406
    }
407
408
    /**
409
     * Gets the list of extensions (if limited) for this field. Empty list
410
     * means there is no restriction on allowed types.
411
     *
412
     * @return array
413
     */
414
    protected function getAllowedExtensions()
415
    {
416
        $categories = $this->getAllowedCategories();
417
        return File::get_category_extensions($categories);
418
    }
419
420
    /**
421
     * Validate that this DBFile accepts this filename as valid
422
     *
423
     * @param string $filename
424
     * @throws ValidationException
425
     * @return bool
426
     */
427
    protected function isValidFilename($filename)
428
    {
429
        $extension = strtolower(File::get_file_extension($filename));
430
431
        // Validate true if within the list of allowed extensions
432
        $allowed = $this->getAllowedExtensions();
433
        if ($allowed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowed of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
434
            return in_array($extension, $allowed);
435
        }
436
437
        // If no extensions are configured, fallback to global list
438
        $globalList = File::config()->allowed_extensions;
0 ignored issues
show
Documentation introduced by
The property allowed_extensions does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
439
        if (in_array($extension, $globalList)) {
440
            return true;
441
        }
442
443
        // Only admins can bypass global rules
444
        return !File::config()->apply_restrictions_to_admin && Permission::check('ADMIN');
445
    }
446
447
    /**
448
     * Check filename, and raise a ValidationException if invalid
449
     *
450
     * @param string $filename
451
     * @throws ValidationException
452
     */
453
    protected function assertFilenameValid($filename)
454
    {
455
        $result = new ValidationResult();
456
        $this->validate($result, $filename);
457
        if (!$result->isValid()) {
458
            throw new ValidationException($result);
459
        }
460
    }
461
462
463
    /**
464
     * Hook to validate this record against a validation result
465
     *
466
     * @param ValidationResult $result
467
     * @param string $filename Optional filename to validate. If omitted, the current value is validated.
468
     * @return bool Valid flag
469
     */
470
    public function validate(ValidationResult $result, $filename = null)
471
    {
472
        if (empty($filename)) {
473
            $filename = $this->getFilename();
474
        }
475
        if (empty($filename) || $this->isValidFilename($filename)) {
476
            return true;
477
        }
478
479
        $message = _t('File.INVALIDEXTENSIONSHORT', 'Extension is not allowed');
480
        $result->addError($message);
481
        return false;
482
    }
483
484
    public function setField($field, $value, $markChanged = true)
485
    {
486
        // Catch filename validation on direct assignment
487
        if ($field === 'Filename' && $value) {
488
            $this->assertFilenameValid($value);
489
        }
490
491
        return parent::setField($field, $value, $markChanged);
492
    }
493
494
495
    /**
496
     * Returns the size of the file type in an appropriate format.
497
     *
498
     * @return string|false String value, or false if doesn't exist
499
     */
500
    public function getSize()
501
    {
502
        $size = $this->getAbsoluteSize();
503
        if ($size) {
504
            return File::format_size($size);
505
        }
506
        return false;
507
    }
508
509
    public function deleteFile()
510
    {
511
        if (!$this->Filename) {
512
            return false;
513
        }
514
515
        return $this
516
            ->getStore()
517
            ->delete($this->Filename, $this->Hash);
518
    }
519
520
    public function publishFile()
521
    {
522
        if ($this->Filename) {
523
            $this
524
                ->getStore()
525
                ->publish($this->Filename, $this->Hash);
526
        }
527
    }
528
529
    public function protectFile()
530
    {
531
        if ($this->Filename) {
532
            $this
533
                ->getStore()
534
                ->protect($this->Filename, $this->Hash);
535
        }
536
    }
537
538
    public function grantFile()
539
    {
540
        if ($this->Filename) {
541
            $this
542
                ->getStore()
543
                ->grant($this->Filename, $this->Hash);
544
        }
545
    }
546
547
    public function revokeFile()
548
    {
549
        if ($this->Filename) {
550
            $this
551
                ->getStore()
552
                ->revoke($this->Filename, $this->Hash);
553
        }
554
    }
555
556
    public function canViewFile()
557
    {
558
        return $this->Filename
559
            && $this
560
                ->getStore()
561
                ->canView($this->Filename, $this->Hash);
562
    }
563
}
564