Completed
Pull Request — master (#28)
by Jarek
08:05
created

Handler::processSingleFile()   C

Complexity

Conditions 9
Paths 28

Size

Total Lines 46
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 9.1244

Importance

Changes 0
Metric Value
dl 0
loc 46
ccs 23
cts 26
cp 0.8846
rs 5.0942
c 0
b 0
f 0
cc 9
eloc 21
nc 28
nop 1
crap 9.1244
1
<?php
2
namespace Sirius\Upload;
3
4
use Sirius\Upload\Container\ContainerInterface;
5
use Sirius\Upload\Container\Local as LocalContainer;
6
use Sirius\Upload\Exception\InvalidContainerException;
7
use Sirius\Upload\Util\Arr;
8
use Sirius\Validation\ErrorMessage;
9
use Sirius\Validation\ValueValidator;
10
11
class Handler implements UploadHandlerInterface
12
{
13
    // constants for constructor options
14
    const OPTION_PREFIX = 'prefix';
15
    const OPTION_OVERWRITE = 'overwrite';
16
    const OPTION_AUTOCONFIRM = 'autoconfirm';
17
18
    // constants for validation rules
19
    const RULE_EXTENSION = 'extension';
20
    const RULE_SIZE = 'size';
21
    const RULE_IMAGE = 'image';
22
    const RULE_IMAGE_HEIGHT = 'imageheight';
23
    const RULE_IMAGE_WIDTH = 'imagewidth';
24
    const RULE_IMAGE_RATIO = 'imageratio';
25
26
    /**
27
     * @var ContainerInterface
28
     */
29
    protected $container;
30
31
    /**
32
     * Prefix to be added to the file.
33
     * It can be a subfolder (if it ends with '/', a string to be used as prefix)
34
     * or a callback that returns a string
35
     *
36
     * @var string|callback
37
     */
38
    protected $prefix = '';
39
40
    /**
41
     * When uploading a file that has the same name as a file that is
42
     * already in the container should it overwrite it or use another name
43
     *
44
     * @var boolean
45
     */
46
    protected $overwrite = false;
47
48
    /**
49
     * Whether or not the uploaded files are auto confirmed
50
     *
51
     * @var boolean
52
     */
53
    protected $autoconfirm = false;
54
55
    /**
56
     * @var \Sirius\Validation\ValueValidator
57
     */
58
    protected $validator;
59
    
60
    /**
61
     * @var function|callback
62
     */
63
    protected $sanitizerCallback;
64
65
    /**
66
     * @param $directoryOrContainer
67
     * @param  array                               $options
68
     * @param  ValueValidator                      $validator
69
     * @throws InvalidContainerException
70
     */
71 15
    public function __construct($directoryOrContainer, $options = array(), ValueValidator $validator = null)
72
    {
73 15
        $container = $directoryOrContainer;
74 15
        if (is_string($directoryOrContainer)) {
75 15
            $container = new LocalContainer($directoryOrContainer);
76 15
        }
77 15
        if (!$container instanceof ContainerInterface) {
78 1
            throw new InvalidContainerException('Destination container for uploaded files is not valid');
79
        }
80 15
        $this->container = $container;
81
82
        // create the validator
83 15
        if (!$validator) {
84 15
            $validator = new ValueValidator();
85 15
        }
86 15
        $this->validator = $validator;
87
88
        // set options
89
        $availableOptions = array(
90 15
            static::OPTION_PREFIX => 'setPrefix',
91 15
            static::OPTION_OVERWRITE => 'setOverwrite',
92 15
            static::OPTION_AUTOCONFIRM => 'setAutoconfirm'
93 15
        );
94 15
        foreach ($availableOptions as $key => $method) {
95 15
            if (isset($options[$key])) {
96 15
                $this->{$method}($options[$key]);
97 15
            }
98 15
        }
99 15
    }
100
101
    /**
102
     * Enable/disable upload overwrite
103
     *
104
     * @param  bool                   $overwrite
105
     * @return \Sirius\Upload\Handler
106
     */
107 15
    public function setOverwrite($overwrite)
108
    {
109 15
        $this->overwrite = (bool) $overwrite;
110
111 15
        return $this;
112
    }
113
114
    /**
115
     * File prefix for the upload. Can be
116
     * - a folder (if it ends with /)
117
     * - a string to be used as prefix
118
     * - a function that returns a string
119
     *
120
     * @param  string|callable        $prefix
121
     * @return \Sirius\Upload\Handler
122
     */
123 15
    public function setPrefix($prefix)
124
    {
125 15
        $this->prefix = $prefix;
126
127 15
        return $this;
128
    }
129
130
    /**
131
     * Enable/disable upload autoconfirmation
132
     * Autoconfirmation does not require calling `confirm()`
133
     *
134
     * @param  boolean                $autoconfirm
135
     * @return \Sirius\Upload\Handler
136
     */
137 15
    public function setAutoconfirm($autoconfirm)
138
    {
139 15
        $this->autoconfirm = (bool) $autoconfirm;
140
141 15
        return $this;
142
    }
143
    
144
    /**
145
     * Set the sanitizer function for cleaning up the file names
146
     * 
147
     * @param callable $callback
148
     * @throws \InvalidArgumentException
149
     * @return \Sirius\Upload\Handler
150
     */
151 2
    function setSanitizerCallback($callback)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
152
    {
153 2
        if (!is_callable($callback)) {
154 1
            throw new \InvalidArgumentException('The $callback parameter is not a valid callable entity');
155
        }
156 1
        $this->sanitizerCallback = $callback;
157 1
        return $this;
158
    }
159
160
    /**
161
     * Add validation rule (extension|size|width|height|ratio)
162
     *
163
     * @param  string                 $name
164
     * @param  mixed                  $options
165
     * @param  string                 $errorMessageTemplate
166
     * @param  string                 $label
167
     * @return \Sirius\Upload\Handler
168
     */
169 2
    public function addRule($name, $options = null, $errorMessageTemplate = null, $label = null)
170
    {
171
        $predefinedRules = array(
172 2
            static::RULE_EXTENSION,
173 2
            static::RULE_IMAGE,
174 2
            static::RULE_SIZE,
175 2
            static::RULE_IMAGE_WIDTH,
176 2
            static::RULE_IMAGE_HEIGHT,
177
            static::RULE_IMAGE_RATIO
178 2
        );
179
        // convert to a name that is known by the default RuleFactory
180 2
        if (in_array($name, $predefinedRules)) {
181 2
            $name = 'upload' . $name;
182 2
        }
183 2
        $this->validator->add($name, $options, $errorMessageTemplate, $label);
184
185 2
        return $this;
186
    }
187
188
    /**
189
     * Processes a file upload and returns an upload result file/collection
190
     *
191
     * @param  array                         $files
192
     * @return Result\Collection|Result\File
193
     */
194 12
    public function process($files = array())
195
    {
196 12
        $files = Arr::normalizeFiles($files);
197
198 12
        foreach ($files as $k => $file) {
199 11
            $files[$k] = $this->processSingleFile($file);
200 12
        }
201
202 12
        if (count($files) == 1) {
203 8
            return new Result\File(array_pop($files), $this->container);
204
        }
205
206 4
        return new Result\Collection($files, $this->container);
207
    }
208
209
    /**
210
     * Processes a single uploaded file
211
     * - sanitize the name
212
     * - validates the file
213
     * - if valid, moves the file to the container
214
     *
215
     * @param  array $file
216
     * @return array
217
     */
218 11
    protected function processSingleFile(array $file)
219
    {
220
        // store it for future reference
221 11
        $file['original_name'] = $file['name'];
222
223
        // sanitize the file name
224 11
        $file['name'] = $this->sanitizeFileName($file['name']);
225
226 11
        $file = $this->validateFile($file);
227
        // if there are messages the file is not valid
228 11
        if (isset($file['messages']) && $file['messages']) {
229 2
            return $file;
230
        }
231
232
        // add the prefix
233 9
        $prefix = '';
234 9
        if (is_callable($this->prefix)) {
235
            $prefix = (string) call_user_func($this->prefix, $file['name']);
236 9
        } elseif (is_string($this->prefix)) {
237 9
            $prefix = (string) $this->prefix;
238 9
        }
239
240
        // if overwrite is not allowed, check if the file is already in the container
241 9
        if (!$this->overwrite) {
242 9
            if ($this->container->has($prefix . $file['name'])) {
243
                // add the timestamp to ensure the file is unique
244
                // method is not bulletproof but it's pretty safe
245 1
                $file['name'] = time() . '_' . $file['name'];
246 1
            }
247 11
        }
248
249
        // attempt to move the uploaded file into the container
250 9
        if (!$this->container->moveUploadedFile($file['tmp_name'], $prefix . $file['name'])) {
251
            $file['name'] = false;
252
253
            return $file;
254
        }
255
256 9
        $file['name'] = $prefix . $file['name'];
257
        // create the lock file if autoconfirm is disabled
258 9
        if (!$this->autoconfirm) {
259 8
            $this->container->save($file['name'] . '.lock', (string) time());
260 8
        }
261
262 9
        return $file;
263 11
    }
264
265
    /**
266
     * Validates a file according to the rules configured on the handler
267
     *
268
     * @param $file
269
     * @return mixed
270
     */
271 11
    protected function validateFile($file)
272
    {
273 11
        if (!$this->validator->validate($file)) {
274 2
            $file['messages'] = $this->validator->getMessages();
275 2
        }
276
277 11
        return $file;
278
    }
279
280
    /**
281
     * Sanitize the name of the uploaded file by stripping away bad characters
282
     * and replacing "invalid" characters with underscore _
283
     *
284
     * @param  string $name
285
     * @return string
286
     */
287 11
    protected function sanitizeFileName($name)
288
    {
289 11
        if ($this->sanitizerCallback) {
290 1
            return call_user_func($this->sanitizerCallback, $name);
291
        }
292 10
        return preg_replace('/[^A-Za-z0-9\.]+/', '_', $name);
293
    }
294
}
295