plupload.module 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <?php
  2. /**
  3. * @file
  4. * Implementation of plupload.module.
  5. */
  6. /**
  7. * Implements hook_menu().
  8. */
  9. function plupload_menu() {
  10. $items['plupload-handle-uploads'] = array(
  11. 'title' => 'Handles uploads',
  12. 'page callback' => 'plupload_handle_uploads',
  13. 'type' => MENU_CALLBACK,
  14. 'access callback' => 'plupload_upload_access',
  15. 'access arguments' => array('access content'),
  16. );
  17. $items['plupload-test'] = array(
  18. 'title' => 'Test Plupload',
  19. 'page callback' => 'drupal_get_form',
  20. 'page arguments' => array('plupload_test'),
  21. // @todo: change this to something appropriate, not sure what.
  22. 'access arguments' => array('Administer site configuration'),
  23. 'type' => MENU_CALLBACK,
  24. );
  25. return $items;
  26. }
  27. /**
  28. * Verifies the token for this request.
  29. */
  30. function plupload_upload_access() {
  31. foreach (func_get_args() as $permission) {
  32. if (!user_access($permission)) {
  33. return FALSE;
  34. }
  35. }
  36. return !empty($_REQUEST['plupload_token']) && drupal_valid_token($_REQUEST['plupload_token'], 'plupload-handle-uploads');
  37. }
  38. /**
  39. * Form callback function for test page visible at URL "plupload-test".
  40. */
  41. function plupload_test($form, &$form_state) {
  42. $form['pud'] = array(
  43. '#type' => 'plupload',
  44. '#title' => 'Plupload',
  45. // '#validators' => array(...);
  46. );
  47. $form['submit'] = array(
  48. '#type' => 'submit',
  49. '#value' => 'Submit',
  50. );
  51. return $form;
  52. }
  53. /**
  54. * Submit callback for plupload_test form.
  55. */
  56. function plupload_test_submit($form, &$form_state) {
  57. $saved_files = array();
  58. $scheme = variable_get('file_default_scheme', 'public') . '://';
  59. // We can't use file_save_upload() because of
  60. // http://www.jacobsingh.name/content/tight-coupling-no-not
  61. // file_uri_to_object();
  62. foreach ($form_state['values']['pud'] as $uploaded_file) {
  63. if ($uploaded_file['status'] == 'done') {
  64. $source = $uploaded_file['tmppath'];
  65. $destination = file_stream_wrapper_uri_normalize($scheme . $uploaded_file['name']);
  66. // Rename it to its original name, and put it in its final home.
  67. // Note - not using file_move here because if we call file_get_mime
  68. // (in file_uri_to_object) while it has a .tmp extension, it horks.
  69. $destination = file_unmanaged_move($source, $destination, FILE_EXISTS_RENAME);
  70. $file = plupload_file_uri_to_object($destination);
  71. file_save($file);
  72. $saved_files[] = $file;
  73. }
  74. else {
  75. // @todo: move this to element validate or something and clean up t().
  76. form_set_error('pud', "Upload of {$uploaded_file['name']} failed");
  77. }
  78. }
  79. }
  80. /**
  81. * Implements hook_element_info().
  82. */
  83. function plupload_element_info() {
  84. $types = array();
  85. $module_path = drupal_get_path('module', 'plupload');
  86. $types['plupload'] = array(
  87. '#input' => TRUE,
  88. '#attributes' => array('class' => array('plupload-element')),
  89. // @todo
  90. // '#element_validate' => array('file_managed_file_validate'),
  91. '#theme_wrappers' => array('form_element'),
  92. '#theme' => 'container',
  93. '#value_callback' => 'plupload_element_value',
  94. '#attached' => array(
  95. 'library' => array(array('plupload', 'plupload')),
  96. 'js' => array($module_path . '/plupload.js'),
  97. 'css' => array($module_path . '/plupload.css'),
  98. ),
  99. '#process' => array('plupload_element_process'),
  100. '#element_validate' => array('plupload_element_validate'),
  101. '#pre_render' => array('plupload_element_pre_render'),
  102. );
  103. return $types;
  104. }
  105. /**
  106. * Validate callback for plupload form element.
  107. */
  108. function plupload_element_value(&$element, $input = FALSE, $form_state = NULL) {
  109. $id = $element['#id'];
  110. // If a unique identifier added with '--', we need to exclude it
  111. if(preg_match('/(.*)(--[0-9]+)$/', $id, $reg)) {
  112. $id = $reg[1];
  113. }
  114. $files = array();
  115. foreach ($form_state['input'] as $key => $value) {
  116. if (preg_match('/' . $id . '_([0-9]+)_(.*)/', $key, $reg)) {
  117. $i = $reg[1];
  118. $key = $reg[2];
  119. // Only add the keys we expect.
  120. if (!in_array($key, array('tmpname', 'name', 'status'))) {
  121. continue;
  122. }
  123. // Munge the submitted file names for security.
  124. //
  125. // Similar munging is normally done by file_save_upload(), but submit
  126. // handlers for forms containing plupload elements can't use
  127. // file_save_upload(), for reasons discussed in plupload_test_submit().
  128. // So we have to do this for them.
  129. //
  130. // Note that we do the munging here in the value callback function
  131. // (rather than during form validation or elsewhere) because we want to
  132. // actually modify the submitted values rather than reject them outright;
  133. // file names that require munging can be innocent and do not necessarily
  134. // indicate an attempted exploit. Actual validation of the file names is
  135. // performed later, in plupload_element_validate().
  136. if (in_array($key, array('tmpname', 'name'))) {
  137. // Find the whitelist of extensions to use when munging. If there are
  138. // none, we'll be adding default ones in plupload_element_process(), so
  139. // use those here.
  140. if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
  141. $extensions = $element['#upload_validators']['file_validate_extensions'][0];
  142. }
  143. else {
  144. $validators = _plupload_default_upload_validators();
  145. $extensions = $validators['file_validate_extensions'][0];
  146. }
  147. $value = file_munge_filename($value, $extensions, FALSE);
  148. // To prevent directory traversal issues, make sure the file name does
  149. // not contain any directory components in it. (This more properly
  150. // belongs in the form validation step, but it's simpler to do here so
  151. // that we don't have to deal with the temporary file names during form
  152. // validation and can just focus on the final file name.)
  153. //
  154. // This step is necessary since this module allows a large amount of
  155. // flexibility in where its files are placed (for example, they could
  156. // be intended for public://subdirectory rather than public://, and we
  157. // don't want an attacker to be able to get them back into the top
  158. // level of public:// in that case).
  159. $value = rtrim(drupal_basename($value), '.');
  160. // Based on the same feture from file_save_upload().
  161. if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $value) && (substr($value, -4) != '.txt')) {
  162. $value .= '.txt';
  163. // The .txt extension may not be in the allowed list of extensions.
  164. // We have to add it here or else the file upload will fail.
  165. if (!empty($extensions)) {
  166. $element['#upload_validators']['file_validate_extensions'][0] .= ' txt';
  167. drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $value)));
  168. }
  169. }
  170. }
  171. // The temporary file name has to be processed further so it matches what
  172. // was used when the file was written; see plupload_handle_uploads().
  173. if ($key == 'tmpname') {
  174. $value = _plupload_fix_temporary_filename($value);
  175. // We also define an extra key 'tmppath' which is useful so that submit
  176. // handlers do not need to know which directory plupload stored the
  177. // temporary files in before trying to copy them.
  178. $files[$i]['tmppath'] = variable_get('plupload_temporary_uri', 'temporary://') . $value;
  179. }
  180. elseif ($key == 'name') {
  181. if (module_exists('transliteration') && variable_get('transliteration_file_uploads', TRUE)) {
  182. $value = transliteration_clean_filename($value);
  183. }
  184. }
  185. // Store the final value in the array we will return.
  186. $files[$i][$key] = $value;
  187. }
  188. }
  189. return $files;
  190. }
  191. /**
  192. * Process callback (#process) for plupload form element.
  193. */
  194. function plupload_element_process($element) {
  195. // Start session if not there yet. We need session if we want security
  196. // tokens to work properly.
  197. if (!drupal_session_started()) {
  198. drupal_session_start();
  199. }
  200. if (!isset($element['#upload_validators'])) {
  201. $element['#upload_validators'] = array();
  202. }
  203. $element['#upload_validators'] += _plupload_default_upload_validators();
  204. return $element;
  205. }
  206. /**
  207. * Element validation handler for a Plupload element.
  208. */
  209. function plupload_element_validate($element, &$form_state) {
  210. foreach ($element['#value'] as $file_info) {
  211. // Here we create a $file object for a file that doesn't exist yet,
  212. // because saving the file to its destination is done in a submit handler.
  213. // Using tmp path will give validators access to the actual file on disk and
  214. // filesize information. We manually modify filename and mime to allow
  215. // extension checks.
  216. $file = plupload_file_uri_to_object($file_info['tmppath']);
  217. $destination = variable_get('file_default_scheme', 'public') . '://' . $file_info['name'];
  218. $destination = file_stream_wrapper_uri_normalize($destination);
  219. $file->filename = drupal_basename($destination);
  220. $file->filemime = file_get_mimetype($destination);
  221. foreach (file_validate($file, $element['#upload_validators']) as $error_message) {
  222. $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
  223. form_error($element, $message . ' ' . $error_message);
  224. }
  225. }
  226. }
  227. /**
  228. * Pre render (#pre_render) callback to attach JS settings for the element.
  229. */
  230. function plupload_element_pre_render($element) {
  231. $settings = isset($element['#plupload_settings']) ? $element['#plupload_settings'] : array();
  232. // The Plupload library supports client-side validation of file extension, so
  233. // pass along the information for it to do that. However, as with all client-
  234. // side validation, this is a UI enhancement only, and not a replacement for
  235. // server-side validation.
  236. if (empty($settings['filters']) && isset($element['#upload_validators']['file_validate_extensions'][0])) {
  237. $settings['filters'][] = array(
  238. // @todo Some runtimes (e.g., flash) require a non-empty title for each
  239. // filter, but I don't know what this title is used for. Seems a shame
  240. // to hard-code it, but what's a good way to avoid that?
  241. 'title' => t('Allowed files'),
  242. 'extensions' => str_replace(' ', ',', $element['#upload_validators']['file_validate_extensions'][0]),
  243. );
  244. }
  245. // Check for autoupload and autosubmit settings and add appropriate callback.
  246. if (!empty($element['#autoupload'])) {
  247. $settings['init']['FilesAdded'] = 'Drupal.plupload.filesAddedCallback';
  248. if (!empty($element['#autosubmit'])) {
  249. $settings['init']['UploadComplete'] = 'Drupal.plupload.uploadCompleteCallback';
  250. }
  251. }
  252. // Add a specific submit element that we want to click if one is specified.
  253. if (!empty($element['#submit_element'])) {
  254. $settings['submit_element'] = $element['#submit_element'];
  255. }
  256. // Check if there are event callbacks and append them to current ones, if any.
  257. if (!empty($element['#event_callbacks'])) {
  258. // array_merge() only accepts parameters of type array.
  259. if (!isset($settings['init'])) {
  260. $settings['init'] = array();
  261. }
  262. $settings['init'] = array_merge($settings['init'], $element['#event_callbacks']);
  263. }
  264. if (empty($element['#description'])) {
  265. $element['#description'] = '';
  266. }
  267. $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators']));
  268. $element['#attached']['js'][] = array(
  269. 'type' => 'setting',
  270. 'data' => array('plupload' => array($element['#id'] => $settings)),
  271. );
  272. return $element;
  273. }
  274. /**
  275. * Returns the path to the plupload library.
  276. */
  277. function _plupload_library_path() {
  278. return variable_get('plupload_library_path', module_exists('libraries') ? libraries_get_path('plupload') : 'sites/all/libraries/plupload');
  279. }
  280. /**
  281. * Implements hook_library().
  282. */
  283. function plupload_library() {
  284. $library_path = _plupload_library_path();
  285. $libraries['plupload'] = array(
  286. 'title' => 'Plupload',
  287. 'website' => 'http://www.plupload.com',
  288. 'version' => '1.5.1.1',
  289. 'js' => array(
  290. // @todo - only add gears JS if gears is an enabled runtime.
  291. // $library_path . '/js/gears_init.js' => array(),
  292. $library_path . '/js/jquery.plupload.queue/jquery.plupload.queue.js' => array(),
  293. $library_path . '/js/plupload.full.js' => array(),
  294. 0 => array(
  295. 'type' => 'setting',
  296. 'data' => array(
  297. 'plupload' => array(
  298. // Element-specific settings get keyed by the element id (see
  299. // plupload_element_pre_render()), so put default settings in
  300. // '_default' (Drupal element ids do not have underscores, because
  301. // they have hyphens instead).
  302. '_default' => array(
  303. // @todo Provide a settings page for configuring these.
  304. 'runtimes' => 'html5,flash,html4',
  305. 'url' => url('plupload-handle-uploads', array('query' => array('plupload_token' => drupal_get_token('plupload-handle-uploads')))),
  306. 'max_file_size' => file_upload_max_size() . 'b',
  307. 'chunk_size' => parse_size(ini_get('post_max_size')) . 'b',
  308. 'unique_names' => TRUE,
  309. 'flash_swf_url' => file_create_url($library_path . '/js/plupload.flash.swf'),
  310. 'silverlight_xap_url' => file_create_url($library_path . '/js/plupload.silverlight.xap'),
  311. ),
  312. ),
  313. ),
  314. ),
  315. ),
  316. );
  317. if (module_exists('locale')) {
  318. $module_path = drupal_get_path('module', 'plupload');
  319. $libraries['plupload']['js'][$module_path . '/js/i18n.js'] = array('scope' => 'footer');
  320. }
  321. return $libraries;
  322. }
  323. /**
  324. * Callback that handles and saves uploaded files.
  325. *
  326. * This will respond to the URL on which plupoad library will upload files.
  327. */
  328. function plupload_handle_uploads() {
  329. // @todo: Implement file_validate_size();
  330. // Added a variable for this because in HA environments, temporary may need
  331. // to be a shared location for this to work.
  332. $temp_directory = variable_get('plupload_temporary_uri', 'temporary://');
  333. $writable = file_prepare_directory($temp_directory, FILE_CREATE_DIRECTORY);
  334. if (!$writable) {
  335. die('{"jsonrpc" : "2.0", "error" : {"code": 104, "message": "Failed to open temporary directory."}, "id" : "id"}');
  336. }
  337. // Try to make sure this is private via htaccess.
  338. file_create_htaccess($temp_directory, TRUE);
  339. // Chunk it?
  340. $chunk = isset($_REQUEST["chunk"]) ? $_REQUEST["chunk"] : 0;
  341. // Get and clean the filename.
  342. $file_name = isset($_REQUEST["name"]) ? $_REQUEST["name"] : '';
  343. $file_name = _plupload_fix_temporary_filename($file_name);
  344. // Check the file name for security reasons; it must contain letters, numbers
  345. // and underscores followed by a (single) ".tmp" extension. Since this check
  346. // is more stringent than the one performed in plupload_element_value(), we
  347. // do not need to run the checks performed in that function here. This is
  348. // fortunate, because it would be difficult for us to get the correct list of
  349. // allowed extensions to pass in to file_munge_filename() from this point in
  350. // the code (outside the form API).
  351. if (empty($file_name) || !preg_match('/^\w+\.tmp$/', $file_name)) {
  352. die('{"jsonrpc" : "2.0", "error" : {"code": 105, "message": "Invalid temporary file name."}, "id" : "id"}');
  353. }
  354. // Look for the content type header.
  355. if (isset($_SERVER["HTTP_CONTENT_TYPE"])) {
  356. $content_type = $_SERVER["HTTP_CONTENT_TYPE"];
  357. }
  358. if (isset($_SERVER["CONTENT_TYPE"])) {
  359. $content_type = $_SERVER["CONTENT_TYPE"];
  360. }
  361. // Is this a multipart upload?.
  362. if (strpos($content_type, "multipart") !== FALSE) {
  363. if (isset($_FILES['file']['tmp_name']) && is_uploaded_file($_FILES['file']['tmp_name'])) {
  364. // Open temp file.
  365. $out = fopen($temp_directory . $file_name, $chunk == 0 ? "wb" : "ab");
  366. if ($out) {
  367. // Read binary input stream and append it to temp file.
  368. $in = fopen($_FILES['file']['tmp_name'], "rb");
  369. if ($in) {
  370. while ($buff = fread($in, 4096)) {
  371. fwrite($out, $buff);
  372. }
  373. fclose($in);
  374. }
  375. else {
  376. die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
  377. }
  378. fclose($out);
  379. drupal_unlink($_FILES['file']['tmp_name']);
  380. }
  381. else {
  382. die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
  383. }
  384. }
  385. else {
  386. die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}');
  387. }
  388. }
  389. else {
  390. // Open temp file.
  391. $out = fopen($temp_directory . $file_name, $chunk == 0 ? "wb" : "ab");
  392. if ($out) {
  393. // Read binary input stream and append it to temp file.
  394. $in = fopen("php://input", "rb");
  395. if ($in) {
  396. while ($buff = fread($in, 4096)) {
  397. fwrite($out, $buff);
  398. }
  399. fclose($in);
  400. }
  401. else {
  402. die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
  403. }
  404. fclose($out);
  405. }
  406. else {
  407. die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
  408. }
  409. }
  410. // Return JSON-RPC response.
  411. die('{"jsonrpc" : "2.0", "result" : null, "id" : "id"}');
  412. }
  413. /**
  414. * Returns a file object which can be passed to file_save().
  415. *
  416. * @param string $uri
  417. * A string containing the URI, path, or filename.
  418. *
  419. * @return boolean
  420. * A file object, or FALSE on error.
  421. *
  422. * @todo Replace with calls to this function with file_uri_to_object() when
  423. * http://drupal.org/node/685818 is fixed in core.
  424. */
  425. function plupload_file_uri_to_object($uri) {
  426. global $user;
  427. $uri = file_stream_wrapper_uri_normalize($uri);
  428. $wrapper = file_stream_wrapper_get_instance_by_uri($uri);
  429. $file = new StdClass();
  430. $file->uid = $user->uid;
  431. $file->filename = drupal_basename($uri);
  432. $file->uri = $uri;
  433. $file->filemime = file_get_mimetype($uri);
  434. // This is gagged because some uris will not support it.
  435. $file->filesize = @filesize($uri);
  436. $file->timestamp = REQUEST_TIME;
  437. $file->status = FILE_STATUS_PERMANENT;
  438. return $file;
  439. }
  440. /**
  441. * Fix the temporary filename provided by the plupload library.
  442. *
  443. * Newer versions of the plupload JavaScript library upload temporary files
  444. * with names that contain the intended final prefix of the uploaded file
  445. * (e.g., ".jpg" or ".png"). Older versions of the plupload library always use
  446. * ".tmp" as the temporary file extension.
  447. *
  448. * We prefer the latter behavior, since although the plupload temporary
  449. * directory where these files live is always expected to be private (and we
  450. * protect it via .htaccess; see plupload_handle_uploads()), in case it ever
  451. * isn't we don't want people to be able to upload files with an arbitrary
  452. * extension into that directory.
  453. *
  454. * This function therefore fixes the plupload temporary filenames so that they
  455. * will always use a ".tmp" extension.
  456. *
  457. * @param string $filename
  458. * The original temporary filename provided by the plupload library.
  459. *
  460. * @return string
  461. * The corrected temporary filename, with a ".tmp" extension replacing the
  462. * original one.
  463. */
  464. function _plupload_fix_temporary_filename($filename) {
  465. $pos = strpos($filename, '.');
  466. if ($pos !== FALSE) {
  467. $filename = substr_replace($filename, '.tmp', $pos);
  468. }
  469. return $filename;
  470. }
  471. /**
  472. * Helper function to add defaults to $element['#upload_validators'].
  473. */
  474. function _plupload_default_upload_validators() {
  475. return array(
  476. // See file_save_upload() for details.
  477. 'file_validate_extensions' => array('jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'),
  478. );
  479. }