Completed
Branch FET/event-question-group-refac... (8c9768)
by
unknown
27:04 queued 17:53
created

FilesDataHandler::organizeFilesData()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 2
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
namespace EventEspresso\core\services\request\files;
4
5
use EventEspresso\core\services\collections\Collection;
6
use EventEspresso\core\services\request\Request;
7
use InvalidArgumentException;
8
use UnexpectedValueException;
9
10
/**
11
 * Class FilesDataHandler
12
 *
13
 * Helper for dealing with PHP's $_FILES. Instead of working with a sometimes puzzling array, with file info
14
 * dispersed throughout, creates a single collection of FileSubmissionInterface objects. This collection is a
15
 * one-dimensional list, where identifiers are the HTML input names.
16
 * Eg, access all file info for a file input named "file1" using
17
 *
18
 * ```
19
 * $data_handler = LoaderFactory::getLoader()->load(' EventEspresso\core\services\request\files\FilesDataHandler');
20
 * $file = $data_handler->getFileObject('file1);
21
 * ```
22
 *
23
 * and for a file input named "my[great][file][input]", use the same code but change the last line to:
24
 *
25
 * ```
26
 * $file = $data_handler->getFileObject('my[great][file][input]');
27
 * ```
28
 *
29
 * In both cases, $file will be a FileSubmissionInterface object, containing the file's name, temporary filepath, size,
30
 * error code, and helpers to get its mime type and extension.
31
 *
32
 *
33
 *
34
 * @package     Event Espresso
35
 * @author         Mike Nelson
36
 * @since         4.9.80.p
37
 *
38
 */
39
class FilesDataHandler
40
{
41
    /**
42
     * @var Request
43
     */
44
    protected $request;
45
46
    /**
47
     * @var CollectionInterface | FileSubmissionInterface[]
48
     */
49
    protected $file_objects;
50
51
    /**
52
     * @var bool
53
     */
54
    protected $initialized = false;
55
56
    /**
57
     * FilesDataHandler constructor.
58
     * @param Request $request
59
     */
60
    public function __construct(Request $request)
61
    {
62
        $this->request = $request;
63
    }
64
65
    /**
66
     * @since 4.9.80.p
67
     * @return CollectionInterface | FileSubmissionInterface[]
68
     * @throws UnexpectedValueException
69
     * @throws InvalidArgumentException
70
     */
71
    protected function getFileObjects()
72
    {
73
        $this->initialize();
74
        return $this->file_objects;
75
    }
76
77
    /**
78
     * Sets up the file objects from the request's $_FILES data.
79
     * @since 4.9.80.p
80
     * @throws UnexpectedValueException
81
     * @throws InvalidArgumentException
82
     * @throws \EventEspresso\core\exceptions\InvalidInterfaceException
83
     */
84
    protected function initialize()
85
    {
86
        if ($this->initialized) {
87
            return;
88
        }
89
        $this->file_objects = new Collection(
90
            // collection interface
91
            'EventEspresso\core\services\request\files\FileSubmissionInterface',
92
            // collection name
93
            'submitted_files'
94
        );
95
        $files_raw_data = $this->request->filesParams();
96
        if (empty($files_raw_data)) {
97
            return;
98
        }
99
        if ($this->isStrangeFilesArray($files_raw_data)) {
100
            $data = $this->fixFilesDataArray($files_raw_data);
101
        } else {
102
            $data = $files_raw_data;
103
        }
104
        $this->createFileObjects($data);
105
        $this->initialized = true;
106
    }
107
108
    /**
109
     * Detects if $_FILES is a weird multi-dimensional array that needs fixing or not.
110
     * @since 4.9.80.p
111
     * @param $files_data
112
     * @return bool
113
     * @throws UnexpectedValueException
114
     */
115
    protected function isStrangeFilesArray($files_data)
116
    {
117 View Code Duplication
        if (!is_array($files_data)) {
118
            throw new UnexpectedValueException(
119
                sprintf(
120
                    esc_html__(
121
                        'Unexpected PHP $_FILES data format. "%1$s" was expected to be an array.',
122
                        'event_espresso'
123
                    ),
124
                    (string) $files_data
125
                )
126
            );
127
        }
128
        $first_value = reset($files_data);
129 View Code Duplication
        if (!is_array($first_value)) {
130
            throw new UnexpectedValueException(
131
                sprintf(
132
                    esc_html__(
133
                        'Unexpected PHP $_FILES data format. "%1$s" was expected to be an array.',
134
                        'event_espresso'
135
                    ),
136
                    (string) $first_value
137
                )
138
            );
139
        }
140
        $first_sub_array_item = reset($first_value);
141
        if (is_array($first_sub_array_item)) {
142
            // not just a 2d array
143
            return true;
144
        }
145
        // yep, just 2d array
146
        return false;
147
    }
148
149
    /**
150
     * Takes into account that $_FILES does a weird thing when you have hierarchical form names (eg `<input type="file"
151
     * name="my[hierarchical][form]">`): it leaves the top-level form part alone, but replaces the SECOND part with
152
     * "name", "size", "tmp_name", etc. So that file's data is located at "my[name][hierarchical][form]",
153
     * "my[size][hierarchical][form]", "my[tmp_name][hierarchical][form]", etc. It's really weird.
154
     * @since 4.9.80.p
155
     * @param $files_data
156
     * @return array
157
     */
158
    protected function fixFilesDataArray($files_data)
159
    {
160
        $sane_files_array = [];
161
        foreach ($files_data as $top_level_name => $top_level_children) {
162
            $sub_array = [];
163
            $sane_files_array[ $top_level_name ] = [];
164
            foreach ($top_level_children as $file_data_part => $second_level_children) {
165
                foreach ($second_level_children as $next_level_name => $sub_values) {
166
                    $sub_array[ $next_level_name ] = $this->organizeFilesData($sub_values, $file_data_part);
167
                }
168
                $sane_files_array[ $top_level_name ] = array_replace_recursive(
169
                    $sub_array,
170
                    $sane_files_array[ $top_level_name ]
171
                );
172
            }
173
        }
174
        return $sane_files_array;
175
    }
176
177
    /**
178
     * Recursively explores the array until it finds a leaf node, and tacks `$type` as a final index in front of it.
179
     * @since 4.9.80.p
180
     * @param $data array|string
181
     * @param $type 'name', 'tmp_name', 'size', or 'error'
182
     * @param $name string
183
     * @return array|string
184
     */
185
    protected function organizeFilesData($data, $type)
186
    {
187
        if (! is_array($data)) {
188
            return [
189
                $type => $data
190
            ];
191
        }
192
        $organized_data = [];
193
        foreach ($data as $input_name_part => $sub_inputs_or_value) {
194
            if (is_array($sub_inputs_or_value)) {
195
                $organized_data[ $input_name_part ] = $this->organizeFilesData($sub_inputs_or_value, $type);
196
            } else {
197
                $organized_data[ $input_name_part ][ $type ] = $sub_inputs_or_value;
198
            }
199
        }
200
        return $organized_data;
201
    }
202
203
    /**
204
     * Takes the organized $_FILES array (where all file info is located at the same spot as you'd expect an input
205
     * to be in $_GET or $_POST, with all the file's data located side-by-side in an array) and creates a
206
     * multi-dimensional array of FileSubmissionInterface objects. Stores it in `$this->file_objects`.
207
     * @since 4.9.80.p
208
     * @param array $organized_files $_FILES but organized like $_POST
209
     * @param array $name_parts_so_far for multidimensional HTML form names,
210
     * @throws UnexpectedValueException
211
     * @throws InvalidArgumentException
212
     */
213
    protected function createFileObjects($organized_files, $name_parts_so_far = [])
214
    {
215 View Code Duplication
        if (!is_array($organized_files)) {
216
            throw new UnexpectedValueException(
217
                sprintf(
218
                    esc_html__(
219
                        'Unexpected PHP $organized_files data format. "%1$s" was expected to be an array.',
220
                        'event_espresso'
221
                    ),
222
                    (string) $organized_files
223
                )
224
            );
225
        }
226
        foreach ($organized_files as $key => $value) {
227
            $this_input_name_parts = $name_parts_so_far;
228
            array_push(
229
                $this_input_name_parts,
230
                $key
231
            );
232
            if (isset($value['name'], $value['tmp_name'], $value['size'])) {
233
                $html_name = $this->inputNameFromParts($this_input_name_parts);
234
                $this->file_objects->add(
235
                    new FileSubmission(
236
                        $value['name'],
237
                        $value['tmp_name'],
238
                        $value['size'],
239
                        $value['error']
240
                    ),
241
                    $html_name
242
                );
243
            } else {
244
                $this->createFileObjects($value, $this_input_name_parts);
245
            }
246
        }
247
    }
248
249
    /**
250
     * Takes the input name parts, like `['my', 'great', 'file', 'input1']`
251
     * and returns the HTML name for it, "my[great][file][input1]"
252
     * @since 4.9.80.p
253
     * @param $parts
254
     * @throws UnexpectedValueException
255
     */
256
    protected function inputNameFromParts($parts)
257
    {
258
        if (!is_array($parts)) {
259
            throw new UnexpectedValueException(esc_html__('Name parts should be an array.', 'event_espresso'));
260
        }
261
        $generated_string = '';
262
        foreach ($parts as $part) {
263
            if ($generated_string === '') {
264
                $generated_string = (string) $part;
265
            } else {
266
                $generated_string .= '[' . (string) $part . ']';
267
            }
268
        }
269
        return $generated_string;
270
    }
271
272
    /**
273
     * Gets the input by the indicated $name_parts.
274
     * Eg if you're looking for an input named "my[great][file][input1]", $name_parts
275
     * should be `['my', 'great', 'file', 'input1']`.
276
     * Alternatively, you could use `FileDataHandler::getFileObject('my[great][file][input1]');`
277
     * @since 4.9.80.p
278
     * @param $name_parts
279
     * @throws UnexpectedValueException
280
     * @return FileSubmissionInterface
281
     */
282
    public function getFileObjectFromNameParts($name_parts)
283
    {
284
        return $this->getFileObjects()->get($this->inputNameFromParts($name_parts));
285
    }
286
287
    /**
288
     * Gets the FileSubmissionInterface corresponding to the HTML name provided.
289
     * @since 4.9.80.p
290
     * @param $html_name
291
     * @return mixed
292
     * @throws InvalidArgumentException
293
     * @throws UnexpectedValueException
294
     */
295
    public function getFileObject($html_name)
296
    {
297
        return $this->getFileObjects()->get($html_name);
298
    }
299
}
300
// End of file FilesDataHandler.php
301
// Location: EventEspresso\core\services\request\files/FilesDataHandler.php
302