Handler   A
last analyzed

Complexity

Total Complexity 29

Size/Duplication

Total Lines 289
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 96.7%

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 7
dl 0
loc 289
ccs 88
cts 91
cp 0.967
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 29 6
A setOverwrite() 0 6 1
A setPrefix() 0 6 1
A setAutoconfirm() 0 6 1
A setSanitizerCallback() 0 8 2
A addRule() 0 18 2
A process() 0 14 3
B processSingleFile() 0 46 9
A validateFile() 0 8 2
A sanitizeFileName() 0 7 2
1
<?php
2
declare(strict_types=1);
3
namespace Sirius\Upload;
4
5
use Sirius\Upload\Container\ContainerInterface;
6
use Sirius\Upload\Container\Local as LocalContainer;
7
use Sirius\Upload\Exception\InvalidContainerException;
8
use Sirius\Upload\Result\ResultInterface;
9
use Sirius\Upload\Util\Helper;
10
use Sirius\Validation\ErrorMessage;
11
use Sirius\Validation\ValueValidator;
12
13
class Handler implements UploadHandlerInterface
14
{
15
    // constants for constructor options
16
    const OPTION_PREFIX = 'prefix';
17
    const OPTION_OVERWRITE = 'overwrite';
18
    const OPTION_AUTOCONFIRM = 'autoconfirm';
19
20
    // constants for validation rules
21
    const RULE_EXTENSION = 'extension';
22
    const RULE_SIZE = 'size';
23
    const RULE_IMAGE = 'image';
24
    const RULE_IMAGE_HEIGHT = 'imageheight';
25
    const RULE_IMAGE_WIDTH = 'imagewidth';
26
    const RULE_IMAGE_RATIO = 'imageratio';
27
28
    /**
29
     * @var ContainerInterface
30
     */
31
    protected $container;
32
33
    /**
34
     * Prefix to be added to the file.
35
     * It can be a subfolder (if it ends with '/', a string to be used as prefix)
36
     * or a callback that returns a string
37
     *
38
     * @var string|callback
39
     */
40
    protected $prefix = '';
41
42
    /**
43
     * When uploading a file that has the same name as a file that is
44
     * already in the container should it overwrite it or use another name
45
     *
46
     * @var boolean
47
     */
48
    protected $overwrite = false;
49
50
    /**
51
     * Whether or not the uploaded files are auto confirmed
52
     *
53
     * @var boolean
54
     */
55
    protected $autoconfirm = false;
56
57
    /**
58
     * @var \Sirius\Validation\ValueValidator
59
     */
60
    protected $validator;
61
    
62
    /**
63
     * @var function|callback
64
     */
65
    protected $sanitizerCallback;
66
67
    /**
68
     * @param $directoryOrContainer
69
     * @param  array                               $options
70
     * @param  ValueValidator                      $validator
71 15
     * @throws InvalidContainerException
72
     */
73 15
    public function __construct($directoryOrContainer, $options = [], ValueValidator $validator = null)
74 15
    {
75 15
        $container = $directoryOrContainer;
76 15
        if (is_string($directoryOrContainer)) {
77 15
            $container = new LocalContainer($directoryOrContainer);
78 1
        }
79
        if (!$container instanceof ContainerInterface) {
80 15
            throw new InvalidContainerException('Destination container for uploaded files is not valid');
81
        }
82
        $this->container = $container;
83 15
84 15
        // create the validator
85 15
        if (!$validator) {
86 15
            $validator = new ValueValidator();
87
        }
88
        $this->validator = $validator;
89
90 15
        // set options
91 15
        $availableOptions = [
92 15
            static::OPTION_PREFIX => 'setPrefix',
93 15
            static::OPTION_OVERWRITE => 'setOverwrite',
94 15
            static::OPTION_AUTOCONFIRM => 'setAutoconfirm'
95 15
        ];
96 15
        foreach ($availableOptions as $key => $method) {
97 15
            if (isset($options[$key])) {
98 15
                $this->{$method}($options[$key]);
99 15
            }
100
        }
101
    }
102
103
    /**
104
     * Enable/disable upload overwrite
105
     *
106
     * @param  bool                   $overwrite
107 15
     *
108
     * @return Handler
109 15
     */
110
    public function setOverwrite($overwrite)
111 15
    {
112
        $this->overwrite = (bool) $overwrite;
113
114
        return $this;
115
    }
116
117
    /**
118
     * File prefix for the upload. Can be
119
     * - a folder (if it ends with /)
120
     * - a string to be used as prefix
121
     * - a function that returns a string
122
     *
123 15
     * @param  string|callable        $prefix
124
     *
125 15
     * @return Handler
126
     */
127 15
    public function setPrefix($prefix)
128
    {
129
        $this->prefix = $prefix;
130
131
        return $this;
132
    }
133
134
    /**
135
     * Enable/disable upload autoconfirmation
136
     * Autoconfirmation does not require calling `confirm()`
137 15
     *
138
     * @param  boolean                $autoconfirm
139 15
     *
140
     * @return Handler
141 15
     */
142
    public function setAutoconfirm($autoconfirm)
143
    {
144
        $this->autoconfirm = (bool) $autoconfirm;
145
146
        return $this;
147
    }
148
    
149
    /**
150
     * Set the sanitizer function for cleaning up the file names
151 2
     *
152
     * @param callable $callback
153 2
     *
154 1
     * @return Handler
155
     * @throws \InvalidArgumentException
156 1
     */
157 1
    public function setSanitizerCallback($callback)
158
    {
159
        if (!is_callable($callback)) {
160
            throw new \InvalidArgumentException('The $callback parameter is not a valid callable entity');
161
        }
162
        $this->sanitizerCallback = $callback;
163
        return $this;
164
    }
165
166
    /**
167
     * Add validation rule (extension|size|width|height|ratio)
168
     *
169 2
     * @param  string                 $name
170
     * @param  mixed                  $options
171
     * @param  string                 $errorMessageTemplate
172 2
     * @param  string $label
173 2
     *
174 2
     * @return Handler
175 2
     */
176 2
    public function addRule($name, $options = null, $errorMessageTemplate = null, $label = null):Handler
177
    {
178 2
        $predefinedRules = [
179
            static::RULE_EXTENSION,
180 2
            static::RULE_IMAGE,
181 2
            static::RULE_SIZE,
182 2
            static::RULE_IMAGE_WIDTH,
183 2
            static::RULE_IMAGE_HEIGHT,
184
            static::RULE_IMAGE_RATIO
185 2
        ];
186
        // convert to a name that is known by the default RuleFactory
187
        if (in_array($name, $predefinedRules)) {
188
            $name = 'upload' . $name;
189
        }
190
        $this->validator->add($name, $options, $errorMessageTemplate, $label);
191
192
        return $this;
193
    }
194 12
195
    /**
196 12
     * Processes a file upload and returns an upload result file/collection
197
     *
198 12
     * @param  array                         $files
199 11
     * @return Result\Collection|Result\File|ResultInterface
200 12
     */
201
    public function process($files = []):ResultInterface
202 12
    {
203 8
        $files = Helper::normalizeFiles($files);
204
205
        foreach ($files as $k => $file) {
206 4
            $files[$k] = $this->processSingleFile($file);
207
        }
208
209
        if (count($files) == 1) {
210
            return new Result\File(array_pop($files), $this->container);
211
        }
212
213
        return new Result\Collection($files, $this->container);
214
    }
215
216
    /**
217
     * Processes a single uploaded file
218 11
     * - sanitize the name
219
     * - validates the file
220
     * - if valid, moves the file to the container
221 11
     *
222
     * @param  array $file
223
     * @return array
224 11
     */
225
    protected function processSingleFile(array $file):array
226 11
    {
227
        // store it for future reference
228 11
        $file['original_name'] = $file['name'];
229 2
230
        // sanitize the file name
231
        $file['name'] = $this->sanitizeFileName($file['name']);
232
233 9
        $file = $this->validateFile($file);
234 9
        // if there are messages the file is not valid
235
        if (isset($file['messages']) && $file['messages']) {
236 9
            return $file;
237 9
        }
238 9
239
        // add the prefix
240
        $prefix = '';
241 9
        if (is_callable($this->prefix)) {
242 9
            $prefix = (string) call_user_func($this->prefix, $file['name']);
243
        } elseif (is_string($this->prefix)) {
244
            $prefix = (string) $this->prefix;
245 1
        }
246 1
247 11
        // if overwrite is not allowed, check if the file is already in the container
248
        if (!$this->overwrite) {
249
            if ($this->container->has($prefix . $file['name'])) {
250 9
                // add the timestamp to ensure the file is unique
251
                // method is not bulletproof but it's pretty safe
252
                $file['name'] = time() . '_' . $file['name'];
253
            }
254
        }
255
256 9
        // attempt to move the uploaded file into the container
257
        if (!$this->container->moveUploadedFile($file['tmp_name'], $prefix . $file['name'])) {
258 9
            $file['name'] = false;
259 8
260 8
            return $file;
261
        }
262 9
263 11
        $file['name'] = $prefix . $file['name'];
264
        // create the lock file if autoconfirm is disabled
265
        if (!$this->autoconfirm) {
266
            $this->container->save($file['name'] . '.lock', (string) time());
267
        }
268
269
        return $file;
270
    }
271 11
272
    /**
273 11
     * Validates a file according to the rules configured on the handler
274 2
     *
275 2
     * @param $file
276
     * @return mixed
277 11
     */
278
    protected function validateFile($file)
279
    {
280
        if (!$this->validator->validate($file)) {
281
            $file['messages'] = $this->validator->getMessages();
282
        }
283
284
        return $file;
285
    }
286
287 11
    /**
288
     * Sanitize the name of the uploaded file by stripping away bad characters
289 11
     * and replacing "invalid" characters with underscore _
290 1
     *
291
     * @param  string $name
292 10
     * @return string
293
     */
294
    protected function sanitizeFileName($name)
295
    {
296
        if ($this->sanitizerCallback) {
297
            return call_user_func($this->sanitizerCallback, $name);
298
        }
299
        return preg_replace('/[^A-Za-z0-9\.]+/', '_', $name);
300
    }
301
}
302