Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
23.85% covered (danger)
23.85%
31 / 130
35.71% covered (danger)
35.71%
5 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
plupload
23.85% covered (danger)
23.85%
31 / 130
35.71% covered (danger)
35.71%
5 / 14
641.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 handle_upload
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 configure
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 is_active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_multipart
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 emit_error
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 generate_filter_string
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 generate_resize_string
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 get_chunk_size
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 temporary_filepath
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 integrate_uploaded_file
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
156
 prepare_temporary_directory
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 set_default_directories
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 set_upload_directories
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb\plupload;
15
16/**
17* This class handles all server-side plupload functions
18*/
19class plupload
20{
21    /**
22    * @var string
23    */
24    protected $phpbb_root_path;
25
26    /**
27    * @var \phpbb\config\config
28    */
29    protected $config;
30
31    /**
32    * @var \phpbb\request\request
33    */
34    protected $request;
35
36    /**
37    * @var \phpbb\user
38    */
39    protected $user;
40
41    /**
42    * @var \bantu\IniGetWrapper\IniGetWrapper
43    */
44    protected $php_ini;
45
46    /**
47    * @var \phpbb\mimetype\guesser
48    */
49    protected $mimetype_guesser;
50
51    /**
52    * Final destination for uploaded files, i.e. the "files" directory.
53    * @var string
54    */
55    protected $upload_directory;
56
57    /**
58    * Temporary upload directory for plupload uploads.
59    * @var string
60    */
61    protected $temporary_directory;
62
63    /**
64    * Constructor.
65    *
66    * @param string $phpbb_root_path
67    * @param \phpbb\config\config $config
68    * @param \phpbb\request\request $request
69    * @param \phpbb\user $user
70    * @param \bantu\IniGetWrapper\IniGetWrapper $php_ini
71    * @param \phpbb\mimetype\guesser $mimetype_guesser
72    */
73    public function __construct($phpbb_root_path, \phpbb\config\config $config, \phpbb\request\request $request, \phpbb\user $user, \bantu\IniGetWrapper\IniGetWrapper $php_ini, \phpbb\mimetype\guesser $mimetype_guesser)
74    {
75        $this->phpbb_root_path = $phpbb_root_path;
76        $this->config = $config;
77        $this->request = $request;
78        $this->user = $user;
79        $this->php_ini = $php_ini;
80        $this->mimetype_guesser = $mimetype_guesser;
81
82        $this->set_default_directories();
83    }
84
85    /**
86    * Plupload allows for chunking so we must check for that and assemble
87    * the whole file first before performing any checks on it.
88    *
89    * @param string $form_name The name of the file element in the upload form
90    *
91    * @return array|null    null if there are no chunks to piece together
92    *                        otherwise array containing the path to the
93    *                        pieced-together file and its size
94    */
95    public function handle_upload($form_name)
96    {
97        $chunks_expected = $this->request->variable('chunks', 0);
98
99        // If chunking is disabled or we are not using plupload, just return
100        // and handle the file as usual
101        if ($chunks_expected < 2)
102        {
103            return null;
104        }
105
106        $file_name = $this->request->variable('name', '');
107        $chunk = $this->request->variable('chunk', 0);
108
109        $this->user->add_lang('plupload');
110        $this->prepare_temporary_directory();
111
112        $file_path = $this->temporary_filepath($file_name);
113        $this->integrate_uploaded_file($form_name, $chunk, $file_path);
114
115        // If we are done with all the chunks, strip the .part suffix and then
116        // handle the resulting file as normal, otherwise die and await the
117        // next chunk.
118        if ($chunk == $chunks_expected - 1)
119        {
120            rename("{$file_path}.part", $file_path);
121
122            // Reset upload directories to defaults once completed
123            $this->set_default_directories();
124
125            // Need to modify some of the $_FILES values to reflect the new file
126            return array(
127                'tmp_name' => $file_path,
128                'name' => $this->request->variable('real_filename', '', true),
129                'size' => filesize($file_path),
130                'type' => $this->mimetype_guesser->guess($file_path, $file_name),
131            );
132        }
133        else
134        {
135            $json_response = new \phpbb\json_response();
136            $json_response->send(array(
137                'jsonrpc' => '2.0',
138                'id' => 'id',
139                'result' => null,
140            ));
141            return null;
142        }
143    }
144
145    /**
146    * Fill in the plupload configuration options in the template
147    *
148    * @param \phpbb\cache\service        $cache
149    * @param \phpbb\template\template    $template
150    * @param string                        $s_action The URL to submit the POST data to
151    * @param int                        $forum_id The ID of the forum
152    * @param int                        $max_files Maximum number of files allowed. 0 for unlimited.
153    *
154    * @return void
155    */
156    public function configure(\phpbb\cache\service $cache, \phpbb\template\template $template, $s_action, $forum_id, $max_files)
157    {
158        $filters = $this->generate_filter_string($cache, $forum_id);
159        $chunk_size = $this->get_chunk_size();
160        $resize = $this->generate_resize_string();
161
162        $template->assign_vars(array(
163            'S_RESIZE'            => $resize,
164            'S_PLUPLOAD'        => true,
165            'FILTERS'            => $filters,
166            'CHUNK_SIZE'        => $chunk_size,
167            'S_PLUPLOAD_URL'    => html_entity_decode($s_action, ENT_COMPAT),
168            'MAX_ATTACHMENTS'    => $max_files,
169            'ATTACH_ORDER'        => ($this->config['display_order']) ? 'asc' : 'desc',
170            'L_TOO_MANY_ATTACHMENTS'    => $this->user->lang('TOO_MANY_ATTACHMENTS', $max_files),
171        ));
172
173        $this->user->add_lang('plupload');
174    }
175
176    /**
177    * Checks whether the page request was sent by plupload or not
178    *
179    * @return bool
180    */
181    public function is_active()
182    {
183        return $this->request->header('X-PHPBB-USING-PLUPLOAD', false);
184    }
185
186    /**
187    * Returns whether the current HTTP request is a multipart request.
188    *
189    * @return bool
190    */
191    public function is_multipart()
192    {
193        $content_type = $this->request->server('CONTENT_TYPE');
194
195        return strpos($content_type, 'multipart') === 0;
196    }
197
198    /**
199    * Sends an error message back to the client via JSON response
200    *
201    * @param int $code        The error code
202    * @param string $msg    The translation string of the message to be sent
203    *
204    * @return void
205    */
206    public function emit_error($code, $msg)
207    {
208        $json_response = new \phpbb\json_response();
209        $json_response->send(array(
210            'jsonrpc' => '2.0',
211            'id' => 'id',
212            'error' => array(
213                'code' => $code,
214                'message' => $this->user->lang($msg),
215            ),
216        ));
217    }
218
219    /**
220     * Looks at the list of allowed extensions and generates a string
221     * appropriate for use in configuring plupload with
222     *
223     * @param \phpbb\cache\service    $cache        Cache service object
224     * @param int                    $forum_id    The forum identifier
225     *
226     * @return string
227     */
228    public function generate_filter_string(\phpbb\cache\service $cache, int $forum_id)
229    {
230        $groups = [];
231        $filters = [];
232
233        $attach_extensions = $cache->obtain_attach_extensions($forum_id);
234        unset($attach_extensions['_allowed_']);
235
236        // Re-arrange the extension array to $groups[$group_name][]
237        foreach ($attach_extensions as $extension => $extension_info)
238        {
239            $groups[$extension_info['group_name']]['extensions'][] = $extension;
240            $groups[$extension_info['group_name']]['max_file_size'] = (int) $extension_info['max_filesize'];
241        }
242
243        foreach ($groups as $group => $group_info)
244        {
245            $filters[] = sprintf(
246                "{title: '%s', extensions: '%s', max_file_size: %s}",
247                addslashes(ucfirst(strtolower($group))),
248                addslashes(implode(',', $group_info['extensions'])),
249                $group_info['max_file_size']
250            );
251        }
252
253        return implode(',', $filters);
254    }
255
256    /**
257    * Generates a string that is used to tell plupload to automatically resize
258    * files before uploading them.
259    *
260    * @return string
261    */
262    public function generate_resize_string()
263    {
264        $resize = '';
265        if ($this->config['img_max_height'] > 0 && $this->config['img_max_width'] > 0)
266        {
267            $preserve_headers_value = $this->config['img_strip_metadata'] ? 'false' : 'true';
268            $resize = sprintf(
269                'resize: {width: %d, height: %d, quality: %d, preserve_headers: %s},',
270                (int) $this->config['img_max_width'],
271                (int) $this->config['img_max_height'],
272                (int) $this->config['img_quality'],
273                $preserve_headers_value
274            );
275        }
276
277        return $resize;
278    }
279
280    /**
281     * Checks various php.ini values to determine the maximum chunk
282     * size a file should be split into for upload.
283     *
284     * The intention is to calculate a value which reflects whatever
285     * the most restrictive limit is set to.  And to then set the chunk
286     * size to half that value, to ensure any required transfer overhead
287     * and POST data remains well within the limit.  Or, if all of the
288     * limits are set to unlimited, the chunk size will also be unlimited.
289     *
290     * @return int
291     *
292     * @access public
293     */
294    public function get_chunk_size()
295    {
296        $max = 0;
297
298        $limits = [
299            $this->php_ini->getBytes('memory_limit'),
300            $this->php_ini->getBytes('upload_max_filesize'),
301            $this->php_ini->getBytes('post_max_size'),
302        ];
303
304        foreach ($limits as $limit_type)
305        {
306            if ($limit_type > 0)
307            {
308                $max = ($max !== 0) ? min($limit_type, $max) : $limit_type;
309            }
310        }
311
312        return (int) floor($max / 2);
313    }
314
315    protected function temporary_filepath($file_name)
316    {
317        // Must preserve the extension for plupload to work.
318        return sprintf(
319            '%s/%s_%s%s',
320            $this->temporary_directory,
321            $this->config['plupload_salt'],
322            md5($file_name),
323            \phpbb\files\filespec::get_extension($file_name)
324        );
325    }
326
327    /**
328    * Checks whether the chunk we are about to deal with was actually uploaded
329    * by PHP and actually exists, if not, it generates an error
330    *
331    * @param string $form_name The name of the file in the form data
332    * @param int $chunk Chunk number
333    * @param string $file_path File path
334    *
335    * @return void
336    */
337    protected function integrate_uploaded_file($form_name, $chunk, $file_path)
338    {
339        $is_multipart = $this->is_multipart();
340        $upload = $this->request->file($form_name);
341        if ($is_multipart && (!isset($upload['tmp_name']) || !is_uploaded_file($upload['tmp_name'])))
342        {
343            $this->emit_error(103, 'PLUPLOAD_ERR_MOVE_UPLOADED');
344        }
345
346        $tmp_file = $this->temporary_filepath($upload['tmp_name']);
347
348        if (!phpbb_is_writable($this->temporary_directory) || !move_uploaded_file($upload['tmp_name'], $tmp_file))
349        {
350            $this->emit_error(103, 'PLUPLOAD_ERR_MOVE_UPLOADED');
351        }
352
353        $out = fopen("{$file_path}.part", $chunk == 0 ? 'wb' : 'ab');
354        if (!$out)
355        {
356            $this->emit_error(102, 'PLUPLOAD_ERR_OUTPUT');
357        }
358
359        $in = fopen(($is_multipart) ? $tmp_file : 'php://input', 'rb');
360        if (!$in)
361        {
362            $this->emit_error(101, 'PLUPLOAD_ERR_INPUT');
363        }
364
365        while ($buf = fread($in, 4096))
366        {
367            fwrite($out, $buf);
368        }
369
370        fclose($in);
371        fclose($out);
372
373        if ($is_multipart)
374        {
375            unlink($tmp_file);
376        }
377    }
378
379    /**
380    * Creates the temporary directory if it does not already exist.
381    *
382    * @return void
383    */
384    protected function prepare_temporary_directory()
385    {
386        if (!file_exists($this->temporary_directory))
387        {
388            mkdir($this->temporary_directory);
389
390            copy(
391                $this->upload_directory . '/index.htm',
392                $this->temporary_directory . '/index.htm'
393            );
394        }
395    }
396
397    /**
398    * Sets the default directories for uploads
399    *
400    * @return void
401    */
402    protected function set_default_directories()
403    {
404        $this->upload_directory = $this->phpbb_root_path . $this->config['upload_path'];
405        $this->temporary_directory = $this->upload_directory . '/plupload';
406    }
407
408    /**
409    * Sets the upload directories to the specified paths
410    *
411    * @param string $upload_directory Upload directory
412    * @param string $temporary_directory Temporary directory
413    *
414    * @return void
415    */
416    public function set_upload_directories($upload_directory, $temporary_directory)
417    {
418        $this->upload_directory = $upload_directory;
419        $this->temporary_directory = $temporary_directory;
420    }
421}