'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', // '#validators' => array(...); ); $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']; $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(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')) { $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 = 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]), ); } // 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']; } 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'), ), // The plupload.js integration file in the module folder can do // additional browser checking to remove unsupported runtimes. // This is in addition to what is done by the Plupload library. '_requirements' => array( 'html5' => array( // The Plupload library recognizes Firefox 3.5 as supporting // HTML 5, but Firefox 3.5 does not support the HTML 5 // "multiple" attribute for file input controls. This makes the // html5 runtime much less appealing, so we treat all Firefox // versions less than 3.6 as ineligible for the html5 runtime. 'mozilla' => '1.9.2', ), ), ), ), ), ), ); 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 = 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'), ); }