diff options
| -rw-r--r-- | phpBB/assets/javascript/plupload.js | 281 | ||||
| -rw-r--r-- | phpBB/config/services.yml | 9 | ||||
| -rw-r--r-- | phpBB/includes/functions_posting.php | 19 | ||||
| -rw-r--r-- | phpBB/includes/functions_upload.php | 31 | ||||
| -rw-r--r-- | phpBB/includes/message_parser.php | 49 | ||||
| -rw-r--r-- | phpBB/includes/ucp/ucp_pm_compose.php | 10 | ||||
| -rw-r--r-- | phpBB/install/install_install.php | 4 | ||||
| -rw-r--r-- | phpBB/install/schemas/schema_data.sql | 2 | ||||
| -rw-r--r-- | phpBB/language/en/plupload.php | 65 | ||||
| -rw-r--r-- | phpBB/phpbb/db/migration/data/v310/alpha1.php | 1 | ||||
| -rw-r--r-- | phpBB/phpbb/db/migration/data/v310/plupload.php | 32 | ||||
| -rw-r--r-- | phpBB/phpbb/plupload/plupload.php | 374 | ||||
| -rw-r--r-- | phpBB/posting.php | 7 | ||||
| -rw-r--r-- | phpBB/styles/prosilver/template/overall_footer.html | 1 | ||||
| -rw-r--r-- | phpBB/styles/prosilver/template/overall_header.html | 5 | ||||
| -rw-r--r-- | phpBB/styles/prosilver/template/plupload.html | 48 | ||||
| -rw-r--r-- | phpBB/styles/prosilver/theme/plupload.css | 11 | ||||
| -rw-r--r-- | tests/functional/fileupload_form_test.php | 19 | ||||
| -rw-r--r-- | tests/functional/plupload_test.php | 149 | 
19 files changed, 1108 insertions, 9 deletions
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 = $('<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 @@ -44,10 +44,16 @@ 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 @@ -1050,6 +1050,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  	*/  	function parse_message($message = '') @@ -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 @@ +<?php +/** +* +* plupload [English] +* +* @package language +* @copyright (c) 2010-2013 Moxiecode Systems AB +* @copyright (c) 2012 phpBB Group +* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 +* +*/ + +/** +* DO NOT CHANGE +*/ +if (!defined('IN_PHPBB')) +{ +	exit; +} + +if (empty($lang) || !is_array($lang)) +{ +	$lang = array(); +} + +// DEVELOPERS PLEASE NOTE +// +// All language files should use UTF-8 as their encoding and the files must not contain a BOM. +// +// Placeholders can now contain order information, e.g. instead of +// 'Page %s of %s' you can (and should) write 'Page %1$s of %2$s', this allows +// translators to re-order the output of data while ensuring it remains correct +// +// You do not need this where single placeholders are used, e.g. 'Message %d' is fine +// equally where a string contains only two placeholders which are used to wrap text +// in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine + +$lang = array_merge($lang, array( +	'PLUPLOAD_ADD_FILES'		=> '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 @@ +<?php +/** +* +* @package migration +* @copyright (c) 2013 phpBB Group +* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 +* +*/ + +namespace phpbb\db\migration\data\v310; + +class plupload extends \phpbb\db\migration\migration +{ +	public function effectively_installed() +	{ +		return isset($this->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 @@ +<?php +/** +* +* @package phpBB3 +* @copyright (c) 2013 phpBB Group +* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 +* +*/ + +namespace phpbb\plupload; + +/** + * @ignore + */ +if (!defined('IN_PHPBB')) +{ +	exit; +} + +/** +* This class handles all server-side plupload functions +* +* @package \phpbb\plupload\plupload +*/ +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 \phpbb\php\ini +	*/ +	protected $php_ini; + +	/** +	* 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 \phpbb\php\ini $php_ini +	* +	* @return null +	*/ +	public function __construct($phpbb_root_path, \phpbb\config\config $config, \phpbb\request\request_interface $request, \phpbb\user $user, \phpbb\php\ini $php_ini) +	{ +		$this->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 @@  <!-- EVENT overall_footer_after -->  {$SCRIPTS} +<!-- IF S_PLUPLOAD --><!-- INCLUDE plupload.html --><!-- ENDIF -->  </body>  </html> 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 @@  	<link href="{T_THEME_PATH}/bidi.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" />  <!-- ENDIF --> +<!-- IF S_PLUPLOAD --> +	<link href="{T_ASSETS_PATH}/plupload/jquery.plupload.queue/css/jquery.plupload.queue.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" /> +	<link href="{T_THEME_PATH}/plupload.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" /> +<!-- ENDIF --> +  <!--[if lte IE 8]>  	<link href="{T_THEME_PATH}/tweaks.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" />  <![endif]--> 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 @@ +<script type="text/javascript" src="{T_ASSETS_PATH}/plupload/plupload.js"></script> +<script type="text/javascript" src="{T_ASSETS_PATH}/plupload/plupload.html5.js"></script> +<script type="text/javascript" src="{T_ASSETS_PATH}/plupload/jquery.plupload.queue/jquery.plupload.queue.js"></script> +<script type="text/javascript"> +//<![CDATA[ +phpbb.plupload = { +	i18n: { +		'Select files': '{LA_PLUPLOAD_SELECT_FILES}', +		'Add files to the upload queue and click the start button.': '{LA_PLUPLOAD_ADD_FILES_TO_QUEUE}', +		'Filename': '{LA_PLUPLOAD_FILENAME}', +		'Status': '{LA_PLUPLOAD_STATUS}', +		'Size': '{LA_PLUPLOAD_SIZE}', +		'Add files': '{LA_PLUPLOAD_ADD_FILES}', +		'Stop current upload': '{LA_PLUPLOAD_STOP_CURRENT_UPLOAD}', +		'Start uploading queue': '{LA_PLUPLOAD_START_CURRENT_UPLOAD}', +		'Uploaded %d/%d files': '{LA_PLUPLOAD_UPLOADED}', +		'N/A': '{LA_PLUPLOAD_NOT_APPLICABLE}', +		'Drag files here.': '{LA_PLUPLOAD_DRAG}', +		'File extension error.': '{LA_PLUPLOAD_EXTENSION_ERROR}', +		'File size error.': '{LA_PLUPLOAD_SIZE_ERROR}', +		'Init error.': '{LA_PLUPLOAD_INIT_ERROR}', +		'HTTP Error.': '{LA_PLUPLOAD_HTTP_ERROR}', +		'Security error.': '{LA_PLUPLOAD_SECURITY_ERROR}', +		'Generic error.': '{LA_PLUPLOAD_GENERIC_ERROR}', +		'IO error.': '{LA_PLUPLOAD_IO_ERROR}', +		'Stop Upload': '{LA_PLUPLOAD_STOP_UPLOAD}', +		'Start upload': '{LA_PLUPLOAD_START_UPLOAD}', +		'%d files queued': '{LA_PLUPLOAD_FILES_QUEUED}' +	}, +	config: { +		runtimes: 'html5', +		url: '{S_PLUPLOAD_URL}', +		max_file_size: '{FILESIZE}b', +		chunk_size: '{CHUNK_SIZE}b', +		unique_names: true, +		filters: [{FILTERS}], +		{S_RESIZE} +		headers: {'X-PHPBB-USING-PLUPLOAD': '1'}, +		file_data_name: 'fileupload', +		multipart_params: {'add_file': '{LA_ADD_FILE}'}, +		img_path: '{T_ASSETS_PATH}/plupload/jquery.plupload.queue/img', +		element_hook: '#attach-panel .inner', +		form_hook: '#postform' +	} +}; +//]]> +</script> +<script type="text/javascript" src="{T_ASSETS_PATH}/javascript/plupload.js"></script> 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 @@ +<?php +/** + * + * @package testing + * @copyright (c) 2012 phpBB Group + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 + * + */ + +/** + * @group functional + */ +class phpbb_functional_plupload_test extends phpbb_functional_test_case +{ +	const CHUNKS = 4; +	private $path; + +	protected function set_extension_group_permission($val) +	{ +		$db = $this->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']); +	} +}  | 
