Passed
Push — 4 ( c79638...9463aa )
by Steve
07:35 queued 12s
created

FileField::validateFileData()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 13
rs 10
1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\Assets\File;
6
use SilverStripe\Control\HTTP;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\ORM\DataObjectInterface;
10
11
/**
12
 * Represents a file type which can be added to a form.
13
 * Automatically tries to save has_one-relations on the saved
14
 * record.
15
 *
16
 * Please set a validator on the form-object to get feedback
17
 * about imposed filesize/extension restrictions.
18
 *
19
 * <b>Usage</p>
20
 *
21
 * If you want to implement a FileField into a form element, you need to pass it an array of source data.
22
 *
23
 * <code>
24
 * class ExampleFormController extends PageController {
25
 *
26
 *  function Form() {
27
 *      $fields = new FieldList(
28
 *          new TextField('MyName'),
29
 *          new FileField('MyFile')
30
 *      );
31
 *      $actions = new FieldList(
32
 *          new FormAction('doUpload', 'Upload file')
33
 *      );
34
 *    $validator = new RequiredFields(['MyName', 'MyFile']);
35
 *
36
 *      return new Form($this, 'Form', $fields, $actions, $validator);
37
 *  }
38
 *
39
 *  function doUpload($data, $form) {
40
 *      $file = $data['MyFile'];
41
 *      $content = file_get_contents($file['tmp_name']);
42
 *      // ... process content
43
 *  }
44
 * }
45
 * </code>
46
 */
47
class FileField extends FormField implements FileHandleField
48
{
49
    use UploadReceiver;
0 ignored issues
show
introduced by
The trait SilverStripe\Forms\UploadReceiver requires some properties which are not provided by SilverStripe\Forms\FileField: $allowed_extensions, $uploads_folder
Loading history...
50
51
    protected $inputType = 'file';
52
53
    /**
54
     * Flag to automatically determine and save a has_one-relationship
55
     * on the saved record (e.g. a "Player" has_one "PlayerImage" would
56
     * trigger saving the ID of newly created file into "PlayerImageID"
57
     * on the record).
58
     *
59
     * @var boolean
60
     */
61
    protected $relationAutoSetting = true;
62
63
    /**
64
     * Create a new file field.
65
     *
66
     * @param string $name The internal field name, passed to forms.
67
     * @param string $title The field label.
68
     * @param int $value The value of the field.
69
     */
70
    public function __construct($name, $title = null, $value = null)
71
    {
72
        $this->constructUploadReceiver();
73
        parent::__construct($name, $title, $value);
74
    }
75
76
    /**
77
     * @param array $properties
78
     * @return string
79
     */
80
    public function Field($properties = [])
81
    {
82
        $properties = array_merge($properties, [
83
            'MaxFileSize' => $this->getValidator()->getAllowedMaxFileSize()
84
        ]);
85
86
        return parent::Field($properties);
87
    }
88
89
    public function getAttributes()
90
    {
91
        $attributes = parent::getAttributes();
92
93
        $accept = $this->getAcceptFileTypes();
94
        if ($accept) {
95
            $attributes = array_merge(['accept' => implode(',', $accept)], $attributes);
96
        }
97
98
        return $attributes;
99
    }
100
101
    /**
102
     * Returns a list of file extensions (and corresponding mime types) that will be accepted
103
     *
104
     * @return array
105
     */
106
    protected function getAcceptFileTypes()
107
    {
108
        $extensions = $this->getValidator()->getAllowedExtensions();
109
        if (!$extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions 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...
110
            return [];
111
        }
112
113
        $accept = [];
114
        $mimeTypes = HTTP::config()->uninherited('MimeTypes');
115
        foreach ($extensions as $extension) {
116
            $accept[] = ".{$extension}";
117
            // Check for corresponding mime type
118
            if (isset($mimeTypes[$extension])) {
119
                $accept[] = $mimeTypes[$extension];
120
            }
121
        }
122
123
        return array_unique($accept);
124
    }
125
126
    /**
127
     * @param DataObject|DataObjectInterface $record
128
     */
129
    public function saveInto(DataObjectInterface $record)
130
    {
131
        if (!isset($_FILES[$this->name]['error']) || $_FILES[$this->name]['error'] == UPLOAD_ERR_NO_FILE) {
132
            return;
133
        }
134
135
        $fileClass = File::get_class_for_file_extension(
136
            File::get_file_extension($_FILES[$this->name]['name'])
137
        );
138
139
        /** @var File $file */
140
        if ($this->relationAutoSetting) {
141
            // assume that the file is connected via a has-one
142
            $objectClass = DataObject::getSchema()->hasOneComponent(get_class($record), $this->name);
143
            if ($objectClass === File::class || empty($objectClass)) {
144
                // Create object of the appropriate file class
145
                $file = Injector::inst()->create($fileClass);
146
            } else {
147
                // try to create a file matching the relation
148
                $file = Injector::inst()->create($objectClass);
149
            }
150
        } elseif ($record instanceof File) {
151
            $file = $record;
152
        } else {
153
            $file = Injector::inst()->create($fileClass);
154
        }
155
156
        $this->upload->loadIntoFile($_FILES[$this->name], $file, $this->getFolderName());
157
158
        if ($this->upload->isError()) {
159
            return;
160
        }
161
162
        if ($this->relationAutoSetting) {
163
            if (empty($objectClass)) {
164
                return;
165
            }
166
167
            $file = $this->upload->getFile();
168
169
            $record->{$this->name . 'ID'} = $file->ID;
0 ignored issues
show
Bug introduced by
Accessing ID on the interface SilverStripe\Assets\Storage\AssetContainer suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
170
        }
171
    }
172
173
    public function Value()
174
    {
175
        return isset($_FILES[$this->getName()]) ? $_FILES[$this->getName()] : null;
176
    }
177
178
    public function validate($validator)
179
    {
180
        // FileField with the name multi_file_syntax[] or multi_file_syntax[key] will have the brackets trimmed in
181
        // $_FILES super-global so it will be stored as $_FILES['mutli_file_syntax']
182
        // multi-file uploads, which are not officially supported by Silverstripe, though may be
183
        // implemented in custom code, so we should still ensure they are at least validated
184
        $isMultiFileUpload = strpos($this->name, '[') !== false;
185
        $fieldName = preg_replace('#\[(.*?)\]$#', '', $this->name);
186
187
        if (!isset($_FILES[$fieldName])) {
188
            return true;
189
        }
190
191
        if ($isMultiFileUpload) {
192
            $isValid = true;
193
            foreach (array_keys($_FILES[$fieldName]['name']) as $key) {
194
                $fileData = [
195
                    'name' => $_FILES[$fieldName]['name'][$key],
196
                    'type' => $_FILES[$fieldName]['type'][$key],
197
                    'tmp_name' => $_FILES[$fieldName]['tmp_name'][$key],
198
                    'error' => $_FILES[$fieldName]['error'][$key],
199
                    'size' => $_FILES[$fieldName]['size'][$key],
200
                ];
201
                if (!$this->validateFileData($validator, $fileData)) {
202
                    $isValid = false;
203
                }
204
            }
205
            return $isValid;
206
        }
207
208
        // regular single-file upload
209
        return $this->validateFileData($validator, $_FILES[$this->name]);
210
    }
211
212
    /**
213
     * @param Validator $validator
214
     * @param array $fileData
215
     * @return bool
216
     */
217
    private function validateFileData($validator, array $fileData): bool
218
    {
219
        $valid = $this->upload->validate($fileData);
220
        if (!$valid) {
221
            $errors = $this->upload->getErrors();
222
            if ($errors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $errors 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...
223
                foreach ($errors as $error) {
224
                    $validator->validationError($this->name, $error, "validation");
225
                }
226
            }
227
            return false;
228
        }
229
        return true;
230
    }
231
232
    /**
233
     * Set if relation can be automatically assigned to the underlying dataobject
234
     *
235
     * @param bool $auto
236
     * @return $this
237
     */
238
    public function setRelationAutoSetting($auto)
239
    {
240
        $this->relationAutoSetting = $auto;
241
        return $this;
242
    }
243
244
    /**
245
     * Check if relation can be automatically assigned to the underlying dataobject
246
     *
247
     * @return bool
248
     */
249
    public function getRelationAutoSetting()
250
    {
251
        return $this->relationAutoSetting;
252
    }
253
}
254