plupload.module in Plupload integration 7
Same filename and directory in other branches
Implementation of plupload.module.
File
plupload.moduleView source
<?php
/**
* @file
* Implementation of plupload.module.
*/
/**
* Implements hook_menu().
*/
function plupload_menu() {
$items['plupload-handle-uploads'] = array(
'title' => 'Handles uploads',
'page callback' => 'plupload_handle_uploads',
'type' => MENU_CALLBACK,
'access callback' => 'plupload_upload_access',
'access arguments' => array(
'access content',
),
);
$items['plupload-test'] = array(
'title' => 'Test Plupload',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'plupload_test',
),
// @todo: change this to something appropriate, not sure what.
'access arguments' => array(
'Administer site configuration',
),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Verifies the token for this request.
*/
function plupload_upload_access() {
foreach (func_get_args() as $permission) {
if (!user_access($permission)) {
return FALSE;
}
}
return !empty($_REQUEST['plupload_token']) && drupal_valid_token($_REQUEST['plupload_token'], 'plupload-handle-uploads');
}
/**
* Form callback function for test page visible at URL "plupload-test".
*/
function plupload_test($form, &$form_state) {
$form['pud'] = array(
'#type' => 'plupload',
'#title' => 'Plupload',
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => 'Submit',
);
return $form;
}
/**
* Submit callback for plupload_test form.
*/
function plupload_test_submit($form, &$form_state) {
$saved_files = array();
$scheme = variable_get('file_default_scheme', 'public') . '://';
// We can't use file_save_upload() because of
// http://www.jacobsingh.name/content/tight-coupling-no-not
// file_uri_to_object();
foreach ($form_state['values']['pud'] as $uploaded_file) {
if ($uploaded_file['status'] == 'done') {
$source = $uploaded_file['tmppath'];
$destination = file_stream_wrapper_uri_normalize($scheme . $uploaded_file['name']);
// Rename it to its original name, and put it in its final home.
// Note - not using file_move here because if we call file_get_mime
// (in file_uri_to_object) while it has a .tmp extension, it horks.
$destination = file_unmanaged_move($source, $destination, FILE_EXISTS_RENAME);
$file = plupload_file_uri_to_object($destination);
file_save($file);
$saved_files[] = $file;
}
else {
// @todo: move this to element validate or something and clean up t().
form_set_error('pud', "Upload of {$uploaded_file['name']} failed");
}
}
}
/**
* Implements hook_element_info().
*/
function plupload_element_info() {
$types = array();
$module_path = drupal_get_path('module', 'plupload');
$types['plupload'] = array(
'#input' => TRUE,
'#attributes' => array(
'class' => array(
'plupload-element',
),
),
// @todo
// '#element_validate' => array('file_managed_file_validate'),
'#theme_wrappers' => array(
'form_element',
),
'#theme' => 'container',
'#value_callback' => 'plupload_element_value',
'#attached' => array(
'library' => array(
array(
'plupload',
'plupload',
),
),
'js' => array(
$module_path . '/plupload.js',
),
'css' => array(
$module_path . '/plupload.css',
),
),
'#process' => array(
'plupload_element_process',
),
'#element_validate' => array(
'plupload_element_validate',
),
'#pre_render' => array(
'plupload_element_pre_render',
),
);
return $types;
}
/**
* Validate callback for plupload form element.
*/
function plupload_element_value(&$element, $input = FALSE, $form_state = NULL) {
$id = $element['#id'];
// If a unique identifier added with '--', we need to exclude it
if (preg_match('/(.*)(--[0-9]+)$/', $id, $reg)) {
$id = $reg[1];
}
$files = array();
foreach ($form_state['input'] as $key => $value) {
if (preg_match('/' . $id . '_([0-9]+)_(.*)/', $key, $reg)) {
$i = $reg[1];
$key = $reg[2];
// Only add the keys we expect.
if (!in_array($key, array(
'tmpname',
'name',
'status',
))) {
continue;
}
// Munge the submitted file names for security.
//
// Similar munging is normally done by file_save_upload(), but submit
// handlers for forms containing plupload elements can't use
// file_save_upload(), for reasons discussed in plupload_test_submit().
// So we have to do this for them.
//
// Note that we do the munging here in the value callback function
// (rather than during form validation or elsewhere) because we want to
// actually modify the submitted values rather than reject them outright;
// file names that require munging can be innocent and do not necessarily
// indicate an attempted exploit. Actual validation of the file names is
// performed later, in plupload_element_validate().
if (in_array($key, array(
'tmpname',
'name',
))) {
// Find the whitelist of extensions to use when munging. If there are
// none, we'll be adding default ones in plupload_element_process(), so
// use those here.
if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
$extensions = $element['#upload_validators']['file_validate_extensions'][0];
}
else {
$validators = _plupload_default_upload_validators();
$extensions = $validators['file_validate_extensions'][0];
}
$value = file_munge_filename($value, $extensions, FALSE);
// To prevent directory traversal issues, make sure the file name does
// not contain any directory components in it. (This more properly
// belongs in the form validation step, but it's simpler to do here so
// that we don't have to deal with the temporary file names during form
// validation and can just focus on the final file name.)
//
// This step is necessary since this module allows a large amount of
// flexibility in where its files are placed (for example, they could
// be intended for public://subdirectory rather than public://, and we
// don't want an attacker to be able to get them back into the top
// level of public:// in that case).
$value = rtrim(drupal_basename($value), '.');
// Based on the same feture from file_save_upload().
if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\\.(php|pl|py|cgi|asp|js)(\\.|$)/i', $value) && substr($value, -4) != '.txt') {
$value .= '.txt';
// The .txt extension may not be in the allowed list of extensions.
// We have to add it here or else the file upload will fail.
if (!empty($extensions)) {
$element['#upload_validators']['file_validate_extensions'][0] .= ' txt';
drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array(
'%filename' => $value,
)));
}
}
}
// The temporary file name has to be processed further so it matches what
// was used when the file was written; see plupload_handle_uploads().
if ($key == 'tmpname') {
$value = _plupload_fix_temporary_filename($value);
// We also define an extra key 'tmppath' which is useful so that submit
// handlers do not need to know which directory plupload stored the
// temporary files in before trying to copy them.
$files[$i]['tmppath'] = variable_get('plupload_temporary_uri', 'temporary://') . $value;
}
elseif ($key == 'name') {
if (module_exists('transliteration') && variable_get('transliteration_file_uploads', TRUE)) {
$value = transliteration_clean_filename($value);
}
}
// Store the final value in the array we will return.
$files[$i][$key] = $value;
}
}
return $files;
}
/**
* Process callback (#process) for plupload form element.
*/
function plupload_element_process($element) {
// Start session if not there yet. We need session if we want security
// tokens to work properly.
if (!drupal_session_started()) {
drupal_session_start();
}
if (!isset($element['#upload_validators'])) {
$element['#upload_validators'] = array();
}
$element['#upload_validators'] += _plupload_default_upload_validators();
return $element;
}
/**
* Element validation handler for a Plupload element.
*/
function plupload_element_validate($element, &$form_state) {
foreach ($element['#value'] as $file_info) {
// Here we create a $file object for a file that doesn't exist yet,
// because saving the file to its destination is done in a submit handler.
// Using tmp path will give validators access to the actual file on disk and
// filesize information. We manually modify filename and mime to allow
// extension checks.
$file = plupload_file_uri_to_object($file_info['tmppath']);
$destination = variable_get('file_default_scheme', 'public') . '://' . $file_info['name'];
$destination = file_stream_wrapper_uri_normalize($destination);
$file->filename = drupal_basename($destination);
$file->filemime = file_get_mimetype($destination);
foreach (file_validate($file, $element['#upload_validators']) as $error_message) {
$message = t('The specified file %name could not be uploaded.', array(
'%name' => $file->filename,
));
form_error($element, $message . ' ' . $error_message);
}
}
}
/**
* Pre render (#pre_render) callback to attach JS settings for the element.
*/
function plupload_element_pre_render($element) {
$settings = isset($element['#plupload_settings']) ? $element['#plupload_settings'] : array();
// The Plupload library supports client-side validation of file extension, so
// pass along the information for it to do that. However, as with all client-
// side validation, this is a UI enhancement only, and not a replacement for
// server-side validation.
if (empty($settings['filters']) && isset($element['#upload_validators']['file_validate_extensions'][0])) {
$settings['filters'][] = array(
// @todo Some runtimes (e.g., flash) require a non-empty title for each
// filter, but I don't know what this title is used for. Seems a shame
// to hard-code it, but what's a good way to avoid that?
'title' => t('Allowed files'),
'extensions' => str_replace(' ', ',', $element['#upload_validators']['file_validate_extensions'][0]),
);
}
// Check for autoupload and autosubmit settings and add appropriate callback.
if (!empty($element['#autoupload'])) {
$settings['init']['FilesAdded'] = 'Drupal.plupload.filesAddedCallback';
if (!empty($element['#autosubmit'])) {
$settings['init']['UploadComplete'] = 'Drupal.plupload.uploadCompleteCallback';
}
}
// Add a specific submit element that we want to click if one is specified.
if (!empty($element['#submit_element'])) {
$settings['submit_element'] = $element['#submit_element'];
}
// Check if there are event callbacks and append them to current ones, if any.
if (!empty($element['#event_callbacks'])) {
// array_merge() only accepts parameters of type array.
if (!isset($settings['init'])) {
$settings['init'] = array();
}
$settings['init'] = array_merge($settings['init'], $element['#event_callbacks']);
}
if (empty($element['#description'])) {
$element['#description'] = '';
}
$element['#description'] = theme('file_upload_help', array(
'description' => $element['#description'],
'upload_validators' => $element['#upload_validators'],
));
$element['#attached']['js'][] = array(
'type' => 'setting',
'data' => array(
'plupload' => array(
$element['#id'] => $settings,
),
),
);
return $element;
}
/**
* Returns the path to the plupload library.
*/
function _plupload_library_path() {
return variable_get('plupload_library_path', module_exists('libraries') ? libraries_get_path('plupload') : 'sites/all/libraries/plupload');
}
/**
* Implements hook_library().
*/
function plupload_library() {
$library_path = _plupload_library_path();
$libraries['plupload'] = array(
'title' => 'Plupload',
'website' => 'http://www.plupload.com',
'version' => '1.5.1.1',
'js' => array(
// @todo - only add gears JS if gears is an enabled runtime.
// $library_path . '/js/gears_init.js' => array(),
$library_path . '/js/jquery.plupload.queue/jquery.plupload.queue.js' => array(),
$library_path . '/js/plupload.full.js' => array(),
0 => array(
'type' => 'setting',
'data' => array(
'plupload' => array(
// Element-specific settings get keyed by the element id (see
// plupload_element_pre_render()), so put default settings in
// '_default' (Drupal element ids do not have underscores, because
// they have hyphens instead).
'_default' => array(
// @todo Provide a settings page for configuring these.
'runtimes' => 'html5,flash,html4',
'url' => url('plupload-handle-uploads', array(
'query' => array(
'plupload_token' => drupal_get_token('plupload-handle-uploads'),
),
)),
'max_file_size' => file_upload_max_size() . 'b',
'chunk_size' => parse_size(ini_get('post_max_size')) . 'b',
'unique_names' => TRUE,
'flash_swf_url' => file_create_url($library_path . '/js/plupload.flash.swf'),
'silverlight_xap_url' => file_create_url($library_path . '/js/plupload.silverlight.xap'),
),
),
),
),
),
);
if (module_exists('locale')) {
$module_path = drupal_get_path('module', 'plupload');
$libraries['plupload']['js'][$module_path . '/js/i18n.js'] = array(
'scope' => 'footer',
);
}
return $libraries;
}
/**
* Callback that handles and saves uploaded files.
*
* This will respond to the URL on which plupoad library will upload files.
*/
function plupload_handle_uploads() {
// @todo: Implement file_validate_size();
// Added a variable for this because in HA environments, temporary may need
// to be a shared location for this to work.
$temp_directory = variable_get('plupload_temporary_uri', 'temporary://');
$writable = file_prepare_directory($temp_directory, FILE_CREATE_DIRECTORY);
if (!$writable) {
die('{"jsonrpc" : "2.0", "error" : {"code": 104, "message": "Failed to open temporary directory."}, "id" : "id"}');
}
// Try to make sure this is private via htaccess.
file_create_htaccess($temp_directory, TRUE);
// Chunk it?
$chunk = isset($_REQUEST["chunk"]) ? $_REQUEST["chunk"] : 0;
// Get and clean the filename.
$file_name = isset($_REQUEST["name"]) ? $_REQUEST["name"] : '';
$file_name = _plupload_fix_temporary_filename($file_name);
// Check the file name for security reasons; it must contain letters, numbers
// and underscores followed by a (single) ".tmp" extension. Since this check
// is more stringent than the one performed in plupload_element_value(), we
// do not need to run the checks performed in that function here. This is
// fortunate, because it would be difficult for us to get the correct list of
// allowed extensions to pass in to file_munge_filename() from this point in
// the code (outside the form API).
if (empty($file_name) || !preg_match('/^\\w+\\.tmp$/', $file_name)) {
die('{"jsonrpc" : "2.0", "error" : {"code": 105, "message": "Invalid temporary file name."}, "id" : "id"}');
}
// Look for the content type header.
if (isset($_SERVER["HTTP_CONTENT_TYPE"])) {
$content_type = $_SERVER["HTTP_CONTENT_TYPE"];
}
if (isset($_SERVER["CONTENT_TYPE"])) {
$content_type = $_SERVER["CONTENT_TYPE"];
}
// Is this a multipart upload?.
if (strpos($content_type, "multipart") !== FALSE) {
if (isset($_FILES['file']['tmp_name']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
// Open temp file.
$out = fopen($temp_directory . $file_name, $chunk == 0 ? "wb" : "ab");
if ($out) {
// Read binary input stream and append it to temp file.
$in = fopen($_FILES['file']['tmp_name'], "rb");
if ($in) {
while ($buff = fread($in, 4096)) {
fwrite($out, $buff);
}
fclose($in);
}
else {
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
}
fclose($out);
drupal_unlink($_FILES['file']['tmp_name']);
}
else {
die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
}
}
else {
die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}');
}
}
else {
// Open temp file.
$out = fopen($temp_directory . $file_name, $chunk == 0 ? "wb" : "ab");
if ($out) {
// Read binary input stream and append it to temp file.
$in = fopen("php://input", "rb");
if ($in) {
while ($buff = fread($in, 4096)) {
fwrite($out, $buff);
}
fclose($in);
}
else {
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
}
fclose($out);
}
else {
die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
}
}
// Return JSON-RPC response.
die('{"jsonrpc" : "2.0", "result" : null, "id" : "id"}');
}
/**
* Returns a file object which can be passed to file_save().
*
* @param string $uri
* A string containing the URI, path, or filename.
*
* @return boolean
* A file object, or FALSE on error.
*
* @todo Replace with calls to this function with file_uri_to_object() when
* http://drupal.org/node/685818 is fixed in core.
*/
function plupload_file_uri_to_object($uri) {
global $user;
$uri = file_stream_wrapper_uri_normalize($uri);
$wrapper = file_stream_wrapper_get_instance_by_uri($uri);
$file = new StdClass();
$file->uid = $user->uid;
$file->filename = drupal_basename($uri);
$file->uri = $uri;
$file->filemime = file_get_mimetype($uri);
// This is gagged because some uris will not support it.
$file->filesize = @filesize($uri);
$file->timestamp = REQUEST_TIME;
$file->status = FILE_STATUS_PERMANENT;
return $file;
}
/**
* Fix the temporary filename provided by the plupload library.
*
* Newer versions of the plupload JavaScript library upload temporary files
* with names that contain the intended final prefix of the uploaded file
* (e.g., ".jpg" or ".png"). Older versions of the plupload library always use
* ".tmp" as the temporary file extension.
*
* We prefer the latter behavior, since although the plupload temporary
* directory where these files live is always expected to be private (and we
* protect it via .htaccess; see plupload_handle_uploads()), in case it ever
* isn't we don't want people to be able to upload files with an arbitrary
* extension into that directory.
*
* This function therefore fixes the plupload temporary filenames so that they
* will always use a ".tmp" extension.
*
* @param string $filename
* The original temporary filename provided by the plupload library.
*
* @return string
* The corrected temporary filename, with a ".tmp" extension replacing the
* original one.
*/
function _plupload_fix_temporary_filename($filename) {
$pos = strpos($filename, '.');
if ($pos !== FALSE) {
$filename = substr_replace($filename, '.tmp', $pos);
}
return $filename;
}
/**
* Helper function to add defaults to $element['#upload_validators'].
*/
function _plupload_default_upload_validators() {
return array(
// See file_save_upload() for details.
'file_validate_extensions' => array(
'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp',
),
);
}
Functions
Name | Description |
---|---|
plupload_element_info | Implements hook_element_info(). |
plupload_element_pre_render | Pre render (#pre_render) callback to attach JS settings for the element. |
plupload_element_process | Process callback (#process) for plupload form element. |
plupload_element_validate | Element validation handler for a Plupload element. |
plupload_element_value | Validate callback for plupload form element. |
plupload_file_uri_to_object | Returns a file object which can be passed to file_save(). |
plupload_handle_uploads | Callback that handles and saves uploaded files. |
plupload_library | Implements hook_library(). |
plupload_menu | Implements hook_menu(). |
plupload_test | Form callback function for test page visible at URL "plupload-test". |
plupload_test_submit | Submit callback for plupload_test form. |
plupload_upload_access | Verifies the token for this request. |
_plupload_default_upload_validators | Helper function to add defaults to $element['#upload_validators']. |
_plupload_fix_temporary_filename | Fix the temporary filename provided by the plupload library. |
_plupload_library_path | Returns the path to the plupload library. |