Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
35.71% covered (danger)
35.71%
5 / 14
CRAP
24.79% covered (danger)
24.79%
30 / 121
plupload
0.00% covered (danger)
0.00%
0 / 1
35.71% covered (danger)
35.71%
5 / 14
619.33
24.79% covered (danger)
24.79%
30 / 121
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
8 / 8
 handle_upload
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 20
 configure
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 13
 is_active
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 is_multipart
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 2
 emit_error
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 7
 generate_filter_string
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 14
 generate_resize_string
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 get_chunk_size
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
8 / 8
 temporary_filepath
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 6
 integrate_uploaded_file
0.00% covered (danger)
0.00%
0 / 1
156.00
0.00% covered (danger)
0.00%
0 / 20
 prepare_temporary_directory
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 6
 set_default_directories
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 set_upload_directories
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 3
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\plupload;
/**
* This class handles all server-side plupload functions
*/
class plupload
{
    /**
    * @var string
    */
    protected $phpbb_root_path;
    /**
    * @var \phpbb\config\config
    */
    protected $config;
    /**
    * @var \phpbb\request\request_interface
    */
    protected $request;
    /**
    * @var \phpbb\user
    */
    protected $user;
    /**
    * @var \bantu\IniGetWrapper\IniGetWrapper
    */
    protected $php_ini;
    /**
    * @var \phpbb\mimetype\guesser
    */
    protected $mimetype_guesser;
    /**
    * Final destination for uploaded files, i.e. the "files" directory.
    * @var string
    */
    protected $upload_directory;
    /**
    * Temporary upload directory for plupload uploads.
    * @var string
    */
    protected $temporary_directory;
    /**
    * Constructor.
    *
    * @param string $phpbb_root_path
    * @param \phpbb\config\config $config
    * @param \phpbb\request\request_interface $request
    * @param \phpbb\user $user
    * @param \bantu\IniGetWrapper\IniGetWrapper $php_ini
    * @param \phpbb\mimetype\guesser $mimetype_guesser
    */
    public function __construct($phpbb_root_path, \phpbb\config\config $config, \phpbb\request\request_interface $request, \phpbb\user $user, \bantu\IniGetWrapper\IniGetWrapper $php_ini, \phpbb\mimetype\guesser $mimetype_guesser)
    {
        $this->phpbb_root_path = $phpbb_root_path;
        $this->config = $config;
        $this->request = $request;
        $this->user = $user;
        $this->php_ini = $php_ini;
        $this->mimetype_guesser = $mimetype_guesser;
        $this->set_default_directories();
    }
    /**
    * Plupload allows for chunking so we must check for that and assemble
    * the whole file first before performing any checks on it.
    *
    * @param string $form_name The name of the file element in the upload form
    *
    * @return array|null    null if there are no chunks to piece together
    *                        otherwise array containing the path to the
    *                        pieced-together file and its size
    */
    public function handle_upload($form_name)
    {
        $chunks_expected = $this->request->variable('chunks', 0);
        // If chunking is disabled or we are not using plupload, just return
        // and handle the file as usual
        if ($chunks_expected < 2)
        {
            return;
        }
        $file_name = $this->request->variable('name', '');
        $chunk = $this->request->variable('chunk', 0);
        $this->user->add_lang('plupload');
        $this->prepare_temporary_directory();
        $file_path = $this->temporary_filepath($file_name);
        $this->integrate_uploaded_file($form_name, $chunk, $file_path);
        // If we are done with all the chunks, strip the .part suffix and then
        // handle the resulting file as normal, otherwise die and await the
        // next chunk.
        if ($chunk == $chunks_expected - 1)
        {
            rename("{$file_path}.part", $file_path);
            // Reset upload directories to defaults once completed
            $this->set_default_directories();
            // Need to modify some of the $_FILES values to reflect the new file
            return array(
                'tmp_name' => $file_path,
                'name' => $this->request->variable('real_filename', '', true),
                'size' => filesize($file_path),
                'type' => $this->mimetype_guesser->guess($file_path, $file_name),
            );
        }
        else
        {
            $json_response = new \phpbb\json_response();
            $json_response->send(array(
                'jsonrpc' => '2.0',
                'id' => 'id',
                'result' => null,
            ));
        }
    }
    /**
    * Fill in the plupload configuration options in the template
    *
    * @param \phpbb\cache\service        $cache
    * @param \phpbb\template\template    $template
    * @param string                        $s_action The URL to submit the POST data to
    * @param int                        $forum_id The ID of the forum
    * @param int                        $max_files Maximum number of files allowed. 0 for unlimited.
    *
    * @return null
    */
    public function configure(\phpbb\cache\service $cache, \phpbb\template\template $template, $s_action, $forum_id, $max_files)
    {
        $filters = $this->generate_filter_string($cache, $forum_id);
        $chunk_size = $this->get_chunk_size();
        $resize = $this->generate_resize_string();
        $template->assign_vars(array(
            'S_RESIZE'            => $resize,
            'S_PLUPLOAD'        => true,
            'FILTERS'            => $filters,
            'CHUNK_SIZE'        => $chunk_size,
            'S_PLUPLOAD_URL'    => html_entity_decode($s_action, ENT_COMPAT),
            'MAX_ATTACHMENTS'    => $max_files,
            'ATTACH_ORDER'        => ($this->config['display_order']) ? 'asc' : 'desc',
            'L_TOO_MANY_ATTACHMENTS'    => $this->user->lang('TOO_MANY_ATTACHMENTS', $max_files),
        ));
        $this->user->add_lang('plupload');
    }
    /**
    * Checks whether the page request was sent by plupload or not
    *
    * @return bool
    */
    public function is_active()
    {
        return $this->request->header('X-PHPBB-USING-PLUPLOAD', false);
    }
    /**
    * Returns whether the current HTTP request is a multipart request.
    *
    * @return bool
    */
    public function is_multipart()
    {
        $content_type = $this->request->server('CONTENT_TYPE');
        return strpos($content_type, 'multipart') === 0;
    }
    /**
    * Sends an error message back to the client via JSON response
    *
    * @param int $code        The error code
    * @param string $msg    The translation string of the message to be sent
    *
    * @return null
    */
    public function emit_error($code, $msg)
    {
        $json_response = new \phpbb\json_response();
        $json_response->send(array(
            'jsonrpc' => '2.0',
            'id' => 'id',
            'error' => array(
                'code' => $code,
                'message' => $this->user->lang($msg),
            ),
        ));
    }
    /**
     * Looks at the list of allowed extensions and generates a string
     * appropriate for use in configuring plupload with
     *
     * @param \phpbb\cache\service    $cache        Cache service object
     * @param string                $forum_id    The forum identifier
     *
     * @return string
     */
    public function generate_filter_string(\phpbb\cache\service $cache, $forum_id)
    {
        $groups = [];
        $filters = [];
        $attach_extensions = $cache->obtain_attach_extensions($forum_id);
        unset($attach_extensions['_allowed_']);
        // Re-arrange the extension array to $groups[$group_name][]
        foreach ($attach_extensions as $extension => $extension_info)
        {
            $groups[$extension_info['group_name']]['extensions'][] = $extension;
            $groups[$extension_info['group_name']]['max_file_size'] = (int) $extension_info['max_filesize'];
        }
        foreach ($groups as $group => $group_info)
        {
            $filters[] = sprintf(
                "{title: '%s', extensions: '%s', max_file_size: %s}",
                addslashes(ucfirst(strtolower($group))),
                addslashes(implode(',', $group_info['extensions'])),
                $group_info['max_file_size']
            );
        }
        return implode(',', $filters);
    }
    /**
    * Generates a string that is used to tell plupload to automatically resize
    * files before uploading them.
    *
    * @return string
    */
    public function generate_resize_string()
    {
        $resize = '';
        if ($this->config['img_max_height'] > 0 && $this->config['img_max_width'] > 0)
        {
            $preserve_headers_value = $this->config['img_strip_metadata'] ? 'false' : 'true';
            $resize = sprintf(
                'resize: {width: %d, height: %d, quality: %d, preserve_headers: %s},',
                (int) $this->config['img_max_width'],
                (int) $this->config['img_max_height'],
                (int) $this->config['img_quality'],
                $preserve_headers_value
            );
        }
        return $resize;
    }
    /**
     * Checks various php.ini values to determine the maximum chunk
     * size a file should be split into for upload.
     *
     * The intention is to calculate a value which reflects whatever
     * the most restrictive limit is set to.  And to then set the chunk
     * size to half that value, to ensure any required transfer overhead
     * and POST data remains well within the limit.  Or, if all of the
     * limits are set to unlimited, the chunk size will also be unlimited.
     *
     * @return int
     *
     * @access public
     */
    public function get_chunk_size()
    {
        $max = 0;
        $limits = [
            $this->php_ini->getBytes('memory_limit'),
            $this->php_ini->getBytes('upload_max_filesize'),
            $this->php_ini->getBytes('post_max_size'),
        ];
        foreach ($limits as $limit_type)
        {
            if ($limit_type > 0)
            {
                $max = ($max !== 0) ? min($limit_type, $max) : $limit_type;
            }
        }
        return floor($max / 2);
    }
    protected function temporary_filepath($file_name)
    {
        // Must preserve the extension for plupload to work.
        return sprintf(
            '%s/%s_%s%s',
            $this->temporary_directory,
            $this->config['plupload_salt'],
            md5($file_name),
            \phpbb\files\filespec::get_extension($file_name)
        );
    }
    /**
    * Checks whether the chunk we are about to deal with was actually uploaded
    * by PHP and actually exists, if not, it generates an error
    *
    * @param string $form_name The name of the file in the form data
    * @param int $chunk Chunk number
    * @param string $file_path File path
    *
    * @return null
    */
    protected function integrate_uploaded_file($form_name, $chunk, $file_path)
    {
        $is_multipart = $this->is_multipart();
        $upload = $this->request->file($form_name);
        if ($is_multipart && (!isset($upload['tmp_name']) || !is_uploaded_file($upload['tmp_name'])))
        {
            $this->emit_error(103, 'PLUPLOAD_ERR_MOVE_UPLOADED');
        }
        $tmp_file = $this->temporary_filepath($upload['tmp_name']);
        if (!phpbb_is_writable($this->temporary_directory) || !move_uploaded_file($upload['tmp_name'], $tmp_file))
        {
            $this->emit_error(103, 'PLUPLOAD_ERR_MOVE_UPLOADED');
        }
        $out = fopen("{$file_path}.part", $chunk == 0 ? 'wb' : 'ab');
        if (!$out)
        {
            $this->emit_error(102, 'PLUPLOAD_ERR_OUTPUT');
        }
        $in = fopen(($is_multipart) ? $tmp_file : 'php://input', 'rb');
        if (!$in)
        {
            $this->emit_error(101, 'PLUPLOAD_ERR_INPUT');
        }
        while ($buf = fread($in, 4096))
        {
            fwrite($out, $buf);
        }
        fclose($in);
        fclose($out);
        if ($is_multipart)
        {
            unlink($tmp_file);
        }
    }
    /**
    * Creates the temporary directory if it does not already exist.
    *
    * @return null
    */
    protected function prepare_temporary_directory()
    {
        if (!file_exists($this->temporary_directory))
        {
            mkdir($this->temporary_directory);
            copy(
                $this->upload_directory . '/index.htm',
                $this->temporary_directory . '/index.htm'
            );
        }
    }
    /**
    * Sets the default directories for uploads
    *
    * @return null
    */
    protected function set_default_directories()
    {
        $this->upload_directory = $this->phpbb_root_path . $this->config['upload_path'];
        $this->temporary_directory = $this->upload_directory . '/plupload';
    }
    /**
    * Sets the upload directories to the specified paths
    *
    * @param string $upload_directory Upload directory
    * @param string $temporary_directory Temporary directory
    *
    * @return null
    */
    public function set_upload_directories($upload_directory, $temporary_directory)
    {
        $this->upload_directory = $upload_directory;
        $this->temporary_directory = $temporary_directory;
    }
}