plupload.module 18 KB

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