diff --git a/phpBB/assets/javascript/plupload.js b/phpBB/assets/javascript/plupload.js new file mode 100644 index 0000000000..32ff178896 --- /dev/null +++ b/phpBB/assets/javascript/plupload.js @@ -0,0 +1,281 @@ +plupload.addI18n(phpbb.plupload.i18n); +plupload.attachment_data = []; + +/** + * Returns the index of the plupload.attachment_data array where the given + * attach id appears + * + * @param int id The attachment id of the file + * + * @return bool Returns false if the id cannot be found + * @return int Returns the index in the main array where the attachment id + * was found + */ +function phpbb_plupload_find_attachment_idx(id) { + var data = plupload.attachment_data; + for (var i = 0; i < data.length; i++) { + if (data[i].attach_id == id) { + return i; + } + } + + return false; +} + +/** + * Converts an array of objects into an object that PHP would expect as POST + * data + * + * @return object An object in the form 'attachment_data[i][key]': value as + * expected by the server + */ +function phpbb_plupload_attachment_data_serialize() { + var obj = {}; + for (var i = 0; i < plupload.attachment_data.length; i++) { + var datum = plupload.attachment_data[i]; + for (var key in datum) { + if (!datum.hasOwnProperty(key)) { + continue; + } + + obj['attachment_data[' + i + '][' + key + ']'] = datum[key]; + } + } + + return obj; +} + +/** + * Unsets all elements in an object whose keys begin with 'attachment_data[' + * + * @param object The object to be cleared + * + * @return undefined + */ +function phpbb_plupload_clear_params(obj) { + for (var key in obj) { + if (!obj.hasOwnProperty(key) || key.indexOf('attachment_data[') !== 0) { + continue; + } + + delete obj[key]; + } +} + +jQuery(function($) { + $(phpbb.plupload.config.element_hook).pluploadQueue(phpbb.plupload.config); + var uploader = $(phpbb.plupload.config.element_hook).pluploadQueue(); + + // Check the page for already-existing attachment data and add it to the + // array + var form = $(phpbb.plupload.config.form_hook)[0]; + for (var i = 0; i < form.length; i++) { + if (form[i].name.indexOf('attachment_data[') !== 0) { + continue; + } + + var matches = form[i].name.match(/\[(\d+)\]\[([^\]]+)\]/); + var index = matches[1]; + var property = matches[2]; + + if (!plupload.attachment_data[index]) { + plupload.attachment_data[index] = {}; + } + + plupload.attachment_data[index][property] = form[i].value; + uploader.settings.multipart_params[form[i].name] = form[i].value; + } + + /** + * Fires before a given file is about to be uploaded. This allows us to + * send the real filename along with the chunk. This is necessary because + * for some reason the filename is set to 'blob' whenever a file is chunked + * + * @param object up The plupload.Uploader object + * @param object file The plupload.File object that is about to be + * uploaded + * + * @return undefined + */ + uploader.bind('BeforeUpload', function(up, file) { + up.settings.multipart_params = $.extend( + up.settings.multipart_params, + {'real_filename': file.name} + ); + }); + + /** + * Fired when a single chunk of any given file is uploaded. This parses the + * response from the server and checks for an error. If an error occurs it + * is reported to the user and the upload of this particular file is halted + * + * @param object up The plupload.Uploader object + * @param object file The plupload.File object whose chunk has just + * been uploaded + * @param object response The response object from the server + * + * @return undefined + */ + uploader.bind('ChunkUploaded', function(up, file, response) { + if (response.chunk >= response.chunks - 1) { + return; + } + + var json = {}; + try { + json = $.parseJSON(response.response); + } catch (e) { + file.status = plupload.FAILED; + up.trigger('FileUploaded', file, { + response: JSON.stringify({ + error: { + message: 'Error parsing server response.' + } + }) + }); + } + + if (json.error) { + file.status = plupload.FAILED; + up.trigger('FileUploaded', file, { + response: JSON.stringify({ + error: { + message: json.error.message + } + }) + }); + } + }); + + /** + * Fires when an entire file has been uploaded. It checks for errors + * returned by the server otherwise parses the list of attachment data and + * appends it to the next file upload so that the server can maintain state + * with regards to the attachments in a given post + * + * @param object up The plupload.Uploader object + * @param object file The plupload.File object that has just been + * uploaded + * @param string response The response string from the server + * + * @return undefined + */ + uploader.bind('FileUploaded', function(up, file, response) { + var json = {}; + try { + json = $.parseJSON(response.response); + } catch (e) { + file.status = plupload.FAILED; + file.error = 'Error parsing server response.' + } + + if (json.error) { + file.status = plupload.FAILED; + file.error = json.error.message; + } else if (file.status === plupload.DONE) { + plupload.attachment_data = json; + file.attachment_data = json[0]; + up.settings.multipart_params = $.extend( + up.settings.multipart_params, + phpbb_plupload_attachment_data_serialize() + ); + } + }); + + /** + * Fires when the entire queue of files have been uploaded. It resets the + * 'add files' button to allow more files to be uploaded and also attaches + * several events to each row of the currently-uploaded files to facilitate + * deleting any one of the files. + * + * Deleting a file removes it from the queue and fires an ajax event to the + * server to tell it to remove the temporary attachment. The server + * responds with the updated attachment data list so that any future + * uploads can maintain state with the server + * + * @param object up The plupload.Uploader object + * @param array files An array of plupload.File objects that have just + * been uploaded as part of a queue + * + * @return undefined + */ + uploader.bind('UploadComplete', function(up, files) { + $('.plupload_upload_status').css('display', 'none'); + $('.plupload_buttons').css('display', 'block'); + + // Insert a bunch of hidden input elements containing the attachment + // data so that the save/preview/submit buttons work as expected. + var form = $(phpbb.plupload.config.form_hook)[0]; + var data = phpbb_plupload_attachment_data_serialize(); + + // Update already existing hidden inputs + for (var i = 0; i < form.length; i++) { + if (data.hasOwnProperty(form[i].name)) { + form[i].value = data[form[i].name]; + delete data[form[i].name]; + } + } + + // Append new inputs + for (var key in data) { + if (!data.hasOwnProperty(key)) { + continue; + } + + var input = $('') + .attr('type', 'hidden') + .attr('name', key) + .attr('value', data[key]); + $(form).append(input); + } + + files.forEach(function(file) { + if (file.status !== plupload.DONE) { + var click = function(evt) { + alert(file.error); + } + + $('#' + file.id).attr('title', file.error); + $('#' + file.id).click(click); + + return; + } + + var click = function(evt) { + $(evt.target).find('a').addClass('working'); + + // The index is always found because file.attachment_data is + // just an element of plupload.attachment_data + var idx = phpbb_plupload_find_attachment_idx(file.attachment_data.attach_id); + var fields = {}; + fields['delete_file[' + idx + ']'] = 1; + + var always = function() { + $(evt.target).find('a').removeClass('working'); + }; + + var done = function(response) { + up.removeFile(file); + plupload.attachment_data = response; + phpbb_plupload_clear_params(up.settings.multipart_params); + up.settings.multipart_params = $.extend( + up.settings.multipart_params, + phpbb_plupload_attachment_data_serialize() + ); + }; + + $.ajax(phpbb.plupload.config.url, { + type: 'POST', + data: $.extend(fields, phpbb_plupload_attachment_data_serialize()), + headers: {'X-PHPBB-USING-PLUPLOAD': '1'} + }) + .always(always) + .done(done); + }; + + $('#' + file.id) + .addClass('can_delete') + .click(click); + }); + }); +}); diff --git a/phpBB/config/services.yml b/phpBB/config/services.yml index 51ae5c454d..c6490a21d7 100644 --- a/phpBB/config/services.yml +++ b/phpBB/config/services.yml @@ -258,6 +258,15 @@ services: php_ini: class: phpbb\php\ini + plupload: + class: phpbb\plupload\plupload + arguments: + - %core.root_path% + - @config + - @request + - @user + - @php_ini + request: class: phpbb\request\request diff --git a/phpBB/includes/functions_posting.php b/phpBB/includes/functions_posting.php index ce1238d8e0..1bcef7f1f2 100644 --- a/phpBB/includes/functions_posting.php +++ b/phpBB/includes/functions_posting.php @@ -385,8 +385,18 @@ function posting_gen_topic_types($forum_id, $cur_topic_type = POST_NORMAL) /** * Upload Attachment - filedata is generated here * Uses upload class +* +* @param string $form_name The form name of the file upload input +* @param int $forum_id The id of the forum +* @param bool $local Whether the file is local or not +* @param string $local_storage The path to the local file +* @param bool $is_message Whether it is a PM or not +* @param \filespec $local_filedata A filespec object created for the local file +* @param \phpbb\plupload\plupload $plupload The plupload object if one is being used +* +* @return object filespec */ -function upload_attachment($form_name, $forum_id, $local = false, $local_storage = '', $is_message = false, $local_filedata = false) +function upload_attachment($form_name, $forum_id, $local = false, $local_storage = '', $is_message = false, $local_filedata = false, \phpbb\plupload\plupload $plupload = null) { global $auth, $user, $config, $db, $cache; global $phpbb_root_path, $phpEx; @@ -414,7 +424,7 @@ function upload_attachment($form_name, $forum_id, $local = false, $local_storage $extensions = $cache->obtain_attach_extensions((($is_message) ? false : (int) $forum_id)); $upload->set_allowed_extensions(array_keys($extensions['_allowed_'])); - $file = ($local) ? $upload->local_upload($local_storage, $local_filedata) : $upload->form_upload($form_name); + $file = ($local) ? $upload->local_upload($local_storage, $local_filedata) : $upload->form_upload($form_name, $plupload); if ($file->init_error) { @@ -469,6 +479,11 @@ function upload_attachment($form_name, $forum_id, $local = false, $local_storage { $file->remove(); + if ($plupload && $plupload->is_active()) + { + $plupload->emit_error(104, 'ATTACHED_IMAGE_NOT_IMAGE'); + } + // If this error occurs a user tried to exploit an IE Bug by renaming extensions // Since the image category is displaying content inline we need to catch this. trigger_error($user->lang['ATTACHED_IMAGE_NOT_IMAGE']); diff --git a/phpBB/includes/functions_upload.php b/phpBB/includes/functions_upload.php index 4181896eca..04d483e14c 100644 --- a/phpBB/includes/functions_upload.php +++ b/phpBB/includes/functions_upload.php @@ -43,11 +43,17 @@ class filespec var $upload = ''; + /** + * The plupload object + * @var \phpbb\plupload\plupload + */ + protected $plupload; + /** * File Class * @access private */ - function filespec($upload_ary, $upload_namespace) + function filespec($upload_ary, $upload_namespace, \phpbb\plupload\plupload $plupload = null) { if (!isset($upload_ary)) { @@ -80,6 +86,7 @@ class filespec $this->local = (isset($upload_ary['local_mode'])) ? true : false; $this->upload = $upload_namespace; + $this->plupload = $plupload; } /** @@ -161,12 +168,14 @@ class filespec */ function is_uploaded() { - if (!$this->local && !is_uploaded_file($this->filename)) + $is_plupload = $this->plupload && $this->plupload->is_active(); + + if (!$this->local && !$is_plupload && !is_uploaded_file($this->filename)) { return false; } - if ($this->local && !file_exists($this->filename)) + if (($this->local || $is_plupload) && !file_exists($this->filename)) { return false; } @@ -564,16 +573,28 @@ class fileupload * Upload file from users harddisk * * @param string $form_name Form name assigned to the file input field (if it is an array, the key has to be specified) + * @param \phpbb\plupload\plupload $plupload The plupload object + * * @return object $file Object "filespec" is returned, all further operations can be done with this object * @access public */ - function form_upload($form_name) + function form_upload($form_name, \phpbb\plupload\plupload $plupload = null) { global $user, $request; $upload = $request->file($form_name); unset($upload['local_mode']); - $file = new filespec($upload, $this); + + if ($plupload) + { + $result = $plupload->handle_upload($form_name); + if (is_array($result)) + { + $upload = array_merge($upload, $result); + } + } + + $file = new filespec($upload, $this, $plupload); if ($file->init_error) { diff --git a/phpBB/includes/message_parser.php b/phpBB/includes/message_parser.php index 3e348801c7..acd31fd519 100644 --- a/phpBB/includes/message_parser.php +++ b/phpBB/includes/message_parser.php @@ -1049,6 +1049,12 @@ class parse_message extends bbcode_firstpass var $mode; + /** + * The plupload object used for dealing with attachments + * @var \phpbb\plupload\plupload + */ + protected $plupload; + /** * Init - give message here or manually */ @@ -1440,6 +1446,11 @@ class parse_message extends bbcode_firstpass if ($preview || $refresh || sizeof($error)) { + if (isset($this->plupload) && $this->plupload->is_active()) + { + $json_response = new \phpbb\json_response(); + } + // Perform actions on temporary attachments if ($delete_file) { @@ -1484,13 +1495,17 @@ class parse_message extends bbcode_firstpass // Reindex Array $this->attachment_data = array_values($this->attachment_data); + if (isset($this->plupload) && $this->plupload->is_active()) + { + $json_response->send($this->attachment_data); + } } } else if (($add_file || $preview) && $upload_file) { if ($num_attachments < $cfg['max_attachments'] || $auth->acl_gets('m_', 'a_', $forum_id)) { - $filedata = upload_attachment($form_name, $forum_id, false, '', $is_message); + $filedata = upload_attachment($form_name, $forum_id, false, '', $is_message, false, $this->plupload); $error = array_merge($error, $filedata['error']); if (!sizeof($error)) @@ -1521,12 +1536,32 @@ class parse_message extends bbcode_firstpass $this->attachment_data = array_merge(array(0 => $new_entry), $this->attachment_data); $this->message = preg_replace('#\[attachment=([0-9]+)\](.*?)\[\/attachment\]#e', "'[attachment='.(\\1 + 1).']\\2[/attachment]'", $this->message); $this->filename_data['filecomment'] = ''; + + if (isset($this->plupload) && $this->plupload->is_active()) + { + // Send the client the attachment data to maintain state + $json_response->send($this->attachment_data); + } } } else { $error[] = $user->lang('TOO_MANY_ATTACHMENTS', (int) $cfg['max_attachments']); } + + if (!empty($error) && isset($this->plupload) && $this->plupload->is_active()) + { + // If this is a plupload (and thus ajax) request, give the + // client the first error we have + $json_response->send(array( + 'jsonrpc' => '2.0', + 'id' => 'id', + 'error' => array( + 'code' => 105, + 'message' => current($error), + ), + )); + } } } @@ -1687,4 +1722,16 @@ class parse_message extends bbcode_firstpass $poll['poll_max_options'] = ($poll['poll_max_options'] < 1) ? 1 : (($poll['poll_max_options'] > $config['max_poll_options']) ? $config['max_poll_options'] : $poll['poll_max_options']); } + + /** + * Setter function for passing the plupload object + * + * @param \phpbb\plupload\plupload $plupload The plupload object + * + * @return null + */ + public function set_plupload(\phpbb\plupload\plupload $plupload) + { + $this->plupload = $plupload; + } } diff --git a/phpBB/includes/ucp/ucp_pm_compose.php b/phpBB/includes/ucp/ucp_pm_compose.php index e0e7a46494..87dfdf902b 100644 --- a/phpBB/includes/ucp/ucp_pm_compose.php +++ b/phpBB/includes/ucp/ucp_pm_compose.php @@ -21,9 +21,10 @@ if (!defined('IN_PHPBB')) */ function compose_pm($id, $mode, $action, $user_folders = array()) { - global $template, $db, $auth, $user; + global $template, $db, $auth, $user, $cache; global $phpbb_root_path, $phpEx, $config; global $request; + global $phpbb_container; // Damn php and globals - i know, this is horrible // Needed for handle_message_list_actions() @@ -385,6 +386,8 @@ function compose_pm($id, $mode, $action, $user_folders = array()) } $message_parser = new parse_message(); + $plupload = $phpbb_container->get('plupload'); + $message_parser->set_plupload($plupload); $message_parser->message = ($action == 'reply') ? '' : $message_text; unset($message_text); @@ -1099,6 +1102,11 @@ function compose_pm($id, $mode, $action, $user_folders = array()) // Show attachment box for adding attachments if true $allowed = ($auth->acl_get('u_pm_attach') && $config['allow_pm_attach'] && $form_enctype); + if ($allowed) + { + $plupload->configure($cache, $template, $s_action, false); + } + // Attachment entry posting_gen_attachment_entry($attachment_data, $filename_data, $allowed); diff --git a/phpBB/install/install_install.php b/phpBB/install/install_install.php index c273660d08..1a7e1d1094 100644 --- a/phpBB/install/install_install.php +++ b/phpBB/install/install_install.php @@ -1322,6 +1322,10 @@ class install_install extends module SET config_value = '" . md5(mt_rand()) . "' WHERE config_name = 'avatar_salt'", + 'UPDATE ' . $data['table_prefix'] . "config + SET config_value = '" . md5(mt_rand()) . "' + WHERE config_name = 'plupload_salt'", + 'UPDATE ' . $data['table_prefix'] . "users SET username = '" . $db->sql_escape($data['admin_name']) . "', user_password='" . $db->sql_escape(md5($data['admin_pass1'])) . "', user_ip = '" . $db->sql_escape($user_ip) . "', user_lang = '" . $db->sql_escape($data['default_lang']) . "', user_email='" . $db->sql_escape($data['board_email']) . "', user_dateformat='" . $db->sql_escape($lang['default_dateformat']) . "', user_email_hash = " . $db->sql_escape(phpbb_email_hash($data['board_email'])) . ", username_clean = '" . $db->sql_escape(utf8_clean_string($data['admin_name'])) . "' WHERE username = 'Admin'", diff --git a/phpBB/install/schemas/schema_data.sql b/phpBB/install/schemas/schema_data.sql index cd38f86312..094a43c8a0 100644 --- a/phpBB/install/schemas/schema_data.sql +++ b/phpBB/install/schemas/schema_data.sql @@ -222,6 +222,7 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('new_member_post_li INSERT INTO phpbb_config (config_name, config_value) VALUES ('new_member_group_default', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('override_user_style', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('pass_complex', 'PASS_TYPE_ANY'); +INSERT INTO phpbb_config (config_name, config_value) VALUES ('plupload_salt', 'phpbb_plupload'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('pm_edit_time', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('pm_max_boxes', '4'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('pm_max_msgs', '50'); @@ -284,6 +285,7 @@ INSERT INTO phpbb_config (config_name, config_value, is_dynamic) VALUES ('num_fi INSERT INTO phpbb_config (config_name, config_value, is_dynamic) VALUES ('num_posts', '1', 1); INSERT INTO phpbb_config (config_name, config_value, is_dynamic) VALUES ('num_topics', '1', 1); INSERT INTO phpbb_config (config_name, config_value, is_dynamic) VALUES ('num_users', '1', 1); +INSERT INTO phpbb_config (config_name, config_value, is_dynamic) VALUES ('plupload_last_gc', '0', 1); INSERT INTO phpbb_config (config_name, config_value, is_dynamic) VALUES ('rand_seed', '0', 1); INSERT INTO phpbb_config (config_name, config_value, is_dynamic) VALUES ('rand_seed_last_update', '0', 1); INSERT INTO phpbb_config (config_name, config_value, is_dynamic) VALUES ('record_online_date', '0', 1); diff --git a/phpBB/language/en/plupload.php b/phpBB/language/en/plupload.php new file mode 100644 index 0000000000..cfdce9810e --- /dev/null +++ b/phpBB/language/en/plupload.php @@ -0,0 +1,65 @@ + 'Add files', + 'PLUPLOAD_ADD_FILES_TO_QUEUE' => 'Add files to the upload queue and click the start button.', + 'PLUPLOAD_DRAG' => 'Drag files here.', + 'PLUPLOAD_ERR_INPUT' => 'Failed to open input stream.', + 'PLUPLOAD_ERR_MOVE_UPLOADED' => 'Failed to move uploaded file.', + 'PLUPLOAD_ERR_OUTPUT' => 'Failed to open output stream.', + 'PLUPLOAD_EXTENSION_ERROR' => 'File extension error.', + 'PLUPLOAD_FILENAME' => 'Filename', + 'PLUPLOAD_FILES_QUEUED' => '%d files queued', + 'PLUPLOAD_GENERIC_ERROR' => 'Generic error.', + 'PLUPLOAD_HTTP_ERROR' => 'HTTP error.', + 'PLUPLOAD_INIT_ERROR' => 'Init error.', + 'PLUPLOAD_IO_ERROR' => 'IO error.', + 'PLUPLOAD_NOT_APPLICABLE' => 'N/A', + 'PLUPLOAD_SECURITY_ERROR' => 'Security error.', + 'PLUPLOAD_SELECT_FILES' => 'Select files', + 'PLUPLOAD_SIZE' => 'Size', + 'PLUPLOAD_SIZE_ERROR' => 'File size error.', + 'PLUPLOAD_STATUS' => 'Status', + 'PLUPLOAD_START_UPLOAD' => 'Start upload', + 'PLUPLOAD_START_CURRENT_UPLOAD' => 'Start uploading queue', + 'PLUPLOAD_STOP_UPLOAD' => 'Stop upload', + 'PLUPLOAD_STOP_CURRENT_UPLOAD' => 'Stop current upload', + // Note: This string is formatted independently by plupload and so does not + // use the same formatting rules as normal phpBB translation strings + 'PLUPLOAD_UPLOADED' => 'Uploaded %d/%d files', +)); diff --git a/phpBB/phpbb/db/migration/data/v310/alpha1.php b/phpBB/phpbb/db/migration/data/v310/alpha1.php index bd4861b1f5..403e301e64 100644 --- a/phpBB/phpbb/db/migration/data/v310/alpha1.php +++ b/phpBB/phpbb/db/migration/data/v310/alpha1.php @@ -28,6 +28,7 @@ class alpha1 extends \phpbb\db\migration\migration '\phpbb\db\migration\data\v310\namespaces', '\phpbb\db\migration\data\v310\notifications_cron', '\phpbb\db\migration\data\v310\notification_options_reconvert', + '\phpbb\db\migration\data\v310\plupload', '\phpbb\db\migration\data\v310\signature_module_auth', '\phpbb\db\migration\data\v310\softdelete_mcp_modules', '\phpbb\db\migration\data\v310\teampage', diff --git a/phpBB/phpbb/db/migration/data/v310/plupload.php b/phpBB/phpbb/db/migration/data/v310/plupload.php new file mode 100644 index 0000000000..1787c6dafc --- /dev/null +++ b/phpBB/phpbb/db/migration/data/v310/plupload.php @@ -0,0 +1,32 @@ +config['plupload_last_gc']) && + isset($this->config['plupload_salt']); + } + + static public function depends_on() + { + return array('\phpbb\db\migration\data\310\dev'); + } + + public function update_data() + { + return array( + array('config.add', array('plupload_last_gc', 0)), + array('config.add', array('plupload_salt', unique_id())), + ); + } +} diff --git a/phpBB/phpbb/plupload/plupload.php b/phpBB/phpbb/plupload/plupload.php new file mode 100644 index 0000000000..6eb5adf864 --- /dev/null +++ b/phpBB/phpbb/plupload/plupload.php @@ -0,0 +1,374 @@ +phpbb_root_path = $phpbb_root_path; + $this->config = $config; + $this->request = $request; + $this->user = $user; + $this->php_ini = $php_ini; + + $this->upload_directory = $this->phpbb_root_path . $this->config['upload_path']; + $this->temporary_directory = $this->upload_directory . '/plupload'; + } + + /** + * 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); + + $file_info = new \Symfony\Component\HttpFoundation\File\File($file_path); + + // 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', ''), + 'size' => filesize($file_path), + 'type' => $file_info->getMimeType($file_path), + ); + } + 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 + * + * @return null + */ + public function configure(\phpbb\cache\service $cache, \phpbb\template\template $template, $s_action, $forum_id) + { + $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' => htmlspecialchars_decode($s_action), + )); + + $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 + * @param string $forum_id The ID of the forum + * + * @return string + */ + public function generate_filter_string(\phpbb\cache\service $cache, $forum_id) + { + $attach_extensions = $cache->obtain_attach_extensions($forum_id); + unset($attach_extensions['_allowed_']); + $groups = array(); + + // Re-arrange the extension array to $groups[$group_name][] + foreach ($attach_extensions as $extension => $extension_info) + { + if (!isset($groups[$extension_info['group_name']])) + { + $groups[$extension_info['group_name']] = array(); + } + + $groups[$extension_info['group_name']][] = $extension; + } + + $filters = array(); + foreach ($groups as $group => $extensions) + { + $filters[] = sprintf( + "{title: '%s', extensions: '%s'}", + addslashes(ucfirst(strtolower($group))), + addslashes(implode(',', $extensions)) + ); + } + + 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) + { + $resize = sprintf( + 'resize: {width: %d, height: %d, quality: 100},', + (int) $this->config['img_max_height'], + (int) $this->config['img_max_width'] + ); + } + + return $resize; + } + + /** + * Checks various php.ini values and the maximum file size to determine + * the maximum size chunks a file can be split up into for upload + * + * @return int + */ + public function get_chunk_size() + { + $max = min( + $this->php_ini->get_bytes('upload_max_filesize'), + $this->php_ini->get_bytes('post_max_size'), + max(1, $this->php_ini->get_bytes('memory_limit')), + $this->config['max_filesize'] + ); + + // Use half of the maximum possible to leave plenty of room for other + // POST data. + 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), + \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 + * + * @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 (!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' + ); + } + } +} diff --git a/phpBB/posting.php b/phpBB/posting.php index 1609382551..396b320eac 100644 --- a/phpBB/posting.php +++ b/phpBB/posting.php @@ -452,6 +452,8 @@ if ($mode == 'edit') $orig_poll_options_size = sizeof($post_data['poll_options']); $message_parser = new parse_message(); +$plupload = $phpbb_container->get('plupload'); +$message_parser->set_plupload($plupload); if (isset($post_data['post_text'])) { @@ -1551,6 +1553,11 @@ if (($mode == 'post' || ($mode == 'edit' && $post_id == $post_data['topic_first_ // Show attachment box for adding attachments if true $allowed = ($auth->acl_get('f_attach', $forum_id) && $auth->acl_get('u_attach') && $config['allow_attachments'] && $form_enctype); +if ($allowed) +{ + $plupload->configure($cache, $template, $s_action, $forum_id); +} + // Attachment entry posting_gen_attachment_entry($attachment_data, $filename_data, $allowed); diff --git a/phpBB/styles/prosilver/template/overall_footer.html b/phpBB/styles/prosilver/template/overall_footer.html index e26c94f367..b26a4c1610 100644 --- a/phpBB/styles/prosilver/template/overall_footer.html +++ b/phpBB/styles/prosilver/template/overall_footer.html @@ -65,6 +65,7 @@ {$SCRIPTS} + diff --git a/phpBB/styles/prosilver/template/overall_header.html b/phpBB/styles/prosilver/template/overall_header.html index 6ca5c86fa4..92baf6ee51 100644 --- a/phpBB/styles/prosilver/template/overall_header.html +++ b/phpBB/styles/prosilver/template/overall_header.html @@ -33,6 +33,11 @@ + + + + + diff --git a/phpBB/styles/prosilver/template/plupload.html b/phpBB/styles/prosilver/template/plupload.html new file mode 100644 index 0000000000..564c1b5c36 --- /dev/null +++ b/phpBB/styles/prosilver/template/plupload.html @@ -0,0 +1,48 @@ + + + + + diff --git a/phpBB/styles/prosilver/theme/plupload.css b/phpBB/styles/prosilver/theme/plupload.css new file mode 100644 index 0000000000..16c26822b5 --- /dev/null +++ b/phpBB/styles/prosilver/theme/plupload.css @@ -0,0 +1,11 @@ +.plupload_filelist li.can_delete:hover { + cursor: pointer; +} + +.plupload_filelist li.can_delete:hover a { + background: url('../../../assets/plupload/jquery.plupload.queue/img/delete.gif'); +} + +.plupload_filelist li a.working { + background: url('../../../assets/plupload/jquery.plupload.queue/img/throbber.gif'); +} diff --git a/tests/functional/fileupload_form_test.php b/tests/functional/fileupload_form_test.php index 998c402fa3..ad01d7b2df 100644 --- a/tests/functional/fileupload_form_test.php +++ b/tests/functional/fileupload_form_test.php @@ -22,6 +22,25 @@ class phpbb_functional_fileupload_form_test extends phpbb_functional_test_case $this->login(); } + public function tearDown() + { + $iterator = new DirectoryIterator(__DIR__ . '/../../phpBB/files/'); + foreach ($iterator as $fileinfo) + { + if ( + $fileinfo->isDot() + || $fileinfo->isDir() + || $fileinfo->getFilename() === 'index.htm' + || $fileinfo->getFilename() === '.htaccess' + ) + { + continue; + } + + unlink($fileinfo->getPathname()); + } + } + private function upload_file($filename, $mimetype) { $file = array( diff --git a/tests/functional/plupload_test.php b/tests/functional/plupload_test.php new file mode 100644 index 0000000000..6dd9224839 --- /dev/null +++ b/tests/functional/plupload_test.php @@ -0,0 +1,149 @@ +get_db(); + $query = " + UPDATE phpbb_extension_groups + SET allow_in_pm = '$val' + WHERE group_name = 'IMAGES' + "; + $db->sql_query($query); + } + + public function setUp() + { + parent::setUp(); + $this->set_extension_group_permission(1); + $this->path = __DIR__ . '/fixtures/files/'; + $this->add_lang('posting'); + $this->login(); + } + + public function tearDown() + { + $this->set_extension_group_permission(0); + $iterator = new DirectoryIterator(__DIR__ . '/../../phpBB/files/'); + foreach ($iterator as $fileinfo) + { + if ( + $fileinfo->isDot() + || $fileinfo->isDir() + || $fileinfo->getFilename() === 'index.htm' + || $fileinfo->getFilename() === '.htaccess' + ) + { + continue; + } + + unlink($fileinfo->getPathname()); + } + } + + public function get_urls() + { + return array( + array('posting.php?mode=reply&f=2&t=1'), + array('ucp.php?i=pm&mode=compose'), + ); + } + + /** + * @dataProvider get_urls + */ + public function test_chunked_upload($url) + { + $chunk_size = ceil(filesize($this->path . 'valid.jpg') / self::CHUNKS); + $handle = fopen($this->path . 'valid.jpg', 'rb'); + + for ($i = 0; $i < self::CHUNKS; $i++) + { + $chunk = fread($handle, $chunk_size); + file_put_contents($this-> path . 'chunk', $chunk); + + $file = array( + 'tmp_name' => $this->path . 'chunk', + 'name' => 'blob', + 'type' => 'application/octet-stream', + 'size' => strlen($chunk), + 'error' => UPLOAD_ERR_OK, + ); + + self::$client->setServerParameter('HTTP_X_PHPBB_USING_PLUPLOAD', '1'); + + $crawler = self::$client->request( + 'POST', + $url . '&sid=' . $this->sid, + array( + 'chunk' => $i, + 'chunks' => self::CHUNKS, + 'name' => md5('valid') . '.jpg', + 'real_filename' => 'valid.jpg', + 'add_file' => $this->lang('ADD_FILE'), + ), + array('fileupload' => $file), + array('X-PHPBB-USING-PLUPLOAD' => '1') + ); + + if ($i < self::CHUNKS - 1) + { + $this->assertContains('{"jsonrpc":"2.0","id":"id","result":null}', self::$client->getResponse()->getContent()); + } + else + { + $response = json_decode(self::$client->getResponse()->getContent(), true); + $this->assertEquals('valid.jpg', $response[0]['real_filename']); + } + + unlink($this->path . 'chunk'); + } + + fclose($handle); + } + + /** + * @dataProvider get_urls + */ + public function test_normal_upload($url) + { + $file = array( + 'tmp_name' => $this->path . 'valid.jpg', + 'name' => 'valid.jpg', + 'type' => 'image/jpeg', + 'size' => filesize($this->path . 'valid.jpg'), + 'error' => UPLOAD_ERR_OK, + ); + + $crawler = self::$client->request( + 'POST', + $url . '&sid=' . $this->sid, + array( + 'chunk' => '0', + 'chunks' => '1', + 'name' => md5('valid') . '.jpg', + 'real_filename' => 'valid.jpg', + 'add_file' => $this->lang('ADD_FILE'), + ), + array('fileupload' => $file), + array('X-PHPBB-USING-PLUPLOAD' => '1') + ); + + $response = json_decode(self::$client->getResponse()->getContent(), true); + $this->assertEquals('valid.jpg', $response[0]['real_filename']); + } +}