filefield_sources.module

Tracking 7.x-1.x branch
  1. drupal
    1. 6 contributions/filefield_sources/filefield_sources.module
    2. 7 contributions/filefield_sources/filefield_sources.module

Extend FileField to allow files from multiple sources.

Functions & methods

NameDescription
filefield_sources_clean_filenameClean up the file name, munging extensions and transliterating.
filefield_sources_element_infoImplements hook_element_info().
filefield_sources_element_validateValidate a file based on the $element['#upload_validators'] property.
filefield_sources_element_validation_helpGenerate help text based on the $element['#upload_validators'] property.
filefield_sources_field_pre_renderA #pre_render function to hide sources if a file is currently uploaded.
filefield_sources_field_processA #process callback to extend the filefield_widget element type.
filefield_sources_field_submitA #submit handler added to all FileField Source buttons.
filefield_sources_field_validateAn #element_validate function to run source validations.
filefield_sources_field_valueA #filefield_value_callback to run source value callbacks.
filefield_sources_field_widget_info_alterA list of settings needed by FileField Sources module on widgets.
filefield_sources_filefield_sources_infoImplements hook_filefield_sources_info().
filefield_sources_filefield_sources_widgetsImplements hook_filefield_sources_widgets().
filefield_sources_formConfiguration form for editing FileField Sources settings for a widget.
filefield_sources_form_field_ui_field_edit_form_alterImplements hook_form_FORM_ID_alter().
filefield_sources_includesLoad all the potential sources.
filefield_sources_infoLoad hook_filefield_sources_info() data from all modules.
filefield_sources_invoke_allCall all FileField Source hooks stored in the available include files.
filefield_sources_listCreate a list of FileField Sources by name, suitable for a select list.
filefield_sources_menuImplements hook_menu().
filefield_sources_save_fileSave a file into the database after validating it.
filefield_sources_themeImplements hook_theme().
theme_filefield_sources_listTheme the display of the sources list.
_filefield_sources_field_accessMenu access callback; Checks user access to edit a file field.
_filefield_sources_sortCustom sort function for ordering sources.

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * Extend FileField to allow files from multiple sources.
  5. */
  6. /**
  7. * Implements hook_menu().
  8. */
  9. function filefield_sources_menu() {
  10. $params = array();
  11. return filefield_sources_invoke_all('menu', $params);
  12. }
  13. /**
  14. * Implements hook_element_info().
  15. */
  16. function filefield_sources_element_info() {
  17. $elements = array();
  18. $elements['managed_file']['#process'] = array('filefield_sources_field_process');
  19. $elements['managed_file']['#pre_render'] = array('filefield_sources_field_pre_render');
  20. $elements['managed_file']['#element_validate'] = array('filefield_sources_field_validate');
  21. $elements['managed_file']['#file_value_callbacks'] = array('filefield_sources_field_value');
  22. return $elements;
  23. }
  24. /**
  25. * Implements hook_theme().
  26. */
  27. function filefield_sources_theme() {
  28. $params = array();
  29. $theme = filefield_sources_invoke_all('theme', $params);
  30. $theme['filefield_sources_list'] = array(
  31. 'arguments' => array('sources' => NULL),
  32. );
  33. return $theme;
  34. }
  35. /**
  36. * Implements hook_filefield_sources_widgets().
  37. *
  38. * This returns a list of widgets that are compatible with FileField Sources.
  39. */
  40. function filefield_sources_filefield_sources_widgets() {
  41. return array('file_generic', 'image_image');
  42. }
  43. /**
  44. * Implements hook_form_FORM_ID_alter().
  45. */
  46. function filefield_sources_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
  47. $instance = $form['#instance'];
  48. if (in_array($instance['widget']['type'], module_invoke_all('filefield_sources_widgets'))) {
  49. if (!empty($form['instance']['widget']['settings'])) {
  50. $form['instance']['widget']['settings'] += filefield_sources_form($instance);
  51. }
  52. else {
  53. $form['instance']['widget']['settings'] = filefield_sources_form($instance);
  54. }
  55. }
  56. }
  57. /**
  58. * A list of settings needed by FileField Sources module on widgets.
  59. */
  60. function filefield_sources_field_widget_info_alter(&$info) {
  61. $settings = array(
  62. 'filefield_sources' => array(),
  63. );
  64. foreach (module_invoke_all('filefield_sources_widgets') as $widget) {
  65. $params = array('save', $widget);
  66. $widget_settings = array_merge($settings, filefield_sources_invoke_all('settings', $params));
  67. if (isset($info[$widget])) {
  68. $info[$widget]['settings']['filefield_sources'] = $widget_settings;
  69. }
  70. }
  71. }
  72. /**
  73. * Configuration form for editing FileField Sources settings for a widget.
  74. */
  75. function filefield_sources_form($instance) {
  76. $settings = $instance['widget']['settings']['filefield_sources'];
  77. $form['filefield_sources'] = array(
  78. '#type' => 'fieldset',
  79. '#title' => t('File sources'),
  80. '#collapsible' => TRUE,
  81. '#collapsed' => TRUE,
  82. '#weight' => 20,
  83. );
  84. $sources = filefield_sources_list(FALSE);
  85. $sources = isset($settings['filefield_sources']) ? array_intersect_key(array_merge($settings['filefield_sources'], $sources), $sources) : $sources;
  86. $form['filefield_sources']['filefield_sources'] = array(
  87. '#type' => 'checkboxes',
  88. '#title' => t('Enabled sources'),
  89. '#options' => $sources,
  90. '#default_value' => $settings['filefield_sources'],
  91. '#description' => t('Select the available locations from which this widget may select files.'),
  92. );
  93. $params = array('form', $instance);
  94. $form['filefield_sources'] = array_merge($form['filefield_sources'], filefield_sources_invoke_all('settings', $params));
  95. return $form;
  96. }
  97. /**
  98. * A #process callback to extend the filefield_widget element type.
  99. *
  100. * Add the central JavaScript and CSS files that allow switching between
  101. * different sources. Third-party modules can also add to the list of sources
  102. * by implementing hook_filefield_sources_info().
  103. */
  104. function filefield_sources_field_process($element, &$form_state, $form) {
  105. static $js_added;
  106. // If not a recognized field instance, do not process.
  107. if (!isset($element['#field_name']) || !($instance = field_widget_instance($element, $form_state)) || !isset($instance['widget']['settings']['filefield_sources']['filefield_sources'])) {
  108. return $element;
  109. }
  110. // Do all processing as needed by each source.
  111. $sources = filefield_sources_info();
  112. $enabled_sources = $instance['widget']['settings']['filefield_sources']['filefield_sources'];
  113. foreach ($sources as $source_name => $source) {
  114. if (empty($enabled_sources[$source_name])) {
  115. unset($sources[$source_name]);
  116. }
  117. elseif (isset($source['process'])) {
  118. $function = $source['process'];
  119. $element = $function($element, $form_state, $form);
  120. }
  121. }
  122. // Exit out if not adding any sources.
  123. if (empty($sources)) {
  124. return $element;
  125. }
  126. // Add basic JS and CSS.
  127. $path = drupal_get_path('module', 'filefield_sources');
  128. $element['#attached']['css'][] = $path . '/filefield_sources.css';
  129. $element['#attached']['js'][] = $path . '/filefield_sources.js';
  130. // Check the element for hint text that might need to be added.
  131. foreach (element_children($element) as $key) {
  132. if (isset($element[$key]['#filefield_sources_hint_text']) && !isset($js_added[$key])) {
  133. $type = str_replace('filefield_', '', $key);
  134. drupal_add_js(array('fileFieldSources' => array($type => array('hintText' => $element[$key]['#filefield_sources_hint_text']))), 'setting');
  135. $js_added[$key] = TRUE;
  136. }
  137. }
  138. // Adjust the AJAX settings so that on upload and remove of any individual
  139. // file, the entire group of file fields is updated together.
  140. // Copied directly from file_field_widget_process().
  141. $field = field_widget_field($element, $form_state);
  142. if ($field['cardinality'] != 1) {
  143. $parents = array_slice($element['#array_parents'], 0, -1);
  144. $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
  145. $field_element = drupal_array_get_nested_value($form, $parents);
  146. $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
  147. foreach (element_children($element) as $key) {
  148. foreach (element_children($element[$key]) as $subkey) {
  149. if (isset($element[$key][$subkey]['#ajax'])) {
  150. $element[$key][$subkey]['#ajax']['path'] = $new_path;
  151. $element[$key][$subkey]['#ajax']['wrapper'] = $new_wrapper;
  152. $element[$key][$subkey]['#limit_validation_errors'] = array($parents);
  153. }
  154. }
  155. }
  156. }
  157. // Add the list of sources to the element for toggling between sources.
  158. if (empty($element['fid']['#value'])) {
  159. $element['filefield_sources_list'] = array(
  160. '#type' => 'markup',
  161. '#markup' => theme('filefield_sources_list', array('element' => $element, 'sources' => $sources)),
  162. '#weight' => -20,
  163. );
  164. }
  165. return $element;
  166. }
  167. /**
  168. * A #pre_render function to hide sources if a file is currently uploaded.
  169. */
  170. function filefield_sources_field_pre_render($element) {
  171. // If we already have a file, we don't want to show the upload controls.
  172. if (!empty($element['#value']['fid'])) {
  173. foreach (element_children($element) as $key) {
  174. if (!empty($element[$key]['#filefield_source'])) {
  175. $element[$key]['#access'] = FALSE;
  176. }
  177. }
  178. }
  179. return $element;
  180. }
  181. /**
  182. * An #element_validate function to run source validations.
  183. */
  184. function filefield_sources_field_validate($element, &$form_state, $form) {
  185. // Do all processing as needed by each source.
  186. $sources = filefield_sources_info();
  187. foreach ($sources as $source) {
  188. if (isset($source['validate'])) {
  189. $function = $source['validate'];
  190. $function($element, $form_state, $form);
  191. }
  192. }
  193. }
  194. /**
  195. * A #submit handler added to all FileField Source buttons.
  196. */
  197. function filefield_sources_field_submit(&$form, &$form_state) {
  198. $parents = array_slice($form_state['triggering_element']['#array_parents'], 0, -3);
  199. $element = drupal_array_get_nested_value($form, $parents);
  200. $field_name = $element['#field_name'];
  201. $langcode = $element['#language'];
  202. // Get exisitng file values.
  203. // File Field items are stored in the field state after ajax reloads starting
  204. // from Drupal 7.8. We try to support all releases by merging the items.
  205. $field_state = field_form_get_state($element['#field_parents'], $field_name, $langcode, $form_state);
  206. $field_values = drupal_array_get_nested_value($form_state['values'], $parents);
  207. if (isset($field_values) && isset($field_state['items'])) {
  208. $field_values += $field_state['items'];
  209. }
  210. elseif (isset($field_state['items'])) {
  211. $field_values = $field_state['items'];
  212. }
  213. if (isset($field_values)) {
  214. // Update sort order according to weight. Note that this is always stored in
  215. // form state. Sort does not work using regular upload, but that is a core
  216. // bug.
  217. usort($field_values, '_field_sort_items_helper');
  218. // Update form_state values.
  219. drupal_array_set_nested_value($form_state['values'], $parents, $field_values);
  220. // Update items.
  221. $field_state['items'] = $field_values;
  222. field_form_set_state($element['#field_parents'], $field_name, $langcode, $form_state, $field_state);
  223. }
  224. // Clear out input as it will need to be rebuildt.
  225. drupal_array_set_nested_value($form_state['input'], $element['#parents'], NULL);
  226. $form_state['rebuild'] = TRUE;
  227. }
  228. /**
  229. * A #filefield_value_callback to run source value callbacks.
  230. */
  231. function filefield_sources_field_value($element, &$item, &$form_state) {
  232. // Do all processing as needed by each source.
  233. $sources = filefield_sources_info();
  234. foreach ($sources as $source) {
  235. if (isset($source['value'])) {
  236. $function = $source['value'];
  237. $function($element, $item);
  238. }
  239. }
  240. }
  241. /**
  242. * Call all FileField Source hooks stored in the available include files.
  243. */
  244. function filefield_sources_invoke_all($method, &$params) {
  245. $return = array();
  246. foreach (filefield_sources_includes() as $source) {
  247. $function = 'filefield_source_' . $source . '_' . $method;
  248. if (function_exists($function)) {
  249. $result = call_user_func_array($function, $params);
  250. if (isset($result) && is_array($result)) {
  251. $return = array_merge_recursive($return, $result);
  252. }
  253. elseif (isset($result)) {
  254. $return[] = $result;
  255. }
  256. }
  257. }
  258. return $return;
  259. }
  260. /**
  261. * Load hook_filefield_sources_info() data from all modules.
  262. */
  263. function filefield_sources_info() {
  264. $info = module_invoke_all('filefield_sources_info');
  265. drupal_alter('filefield_sources_info', $info);
  266. uasort($info, '_filefield_sources_sort');
  267. return $info;
  268. }
  269. /**
  270. * Create a list of FileField Sources by name, suitable for a select list.
  271. */
  272. function filefield_sources_list($include_default = TRUE) {
  273. $info = filefield_sources_info();
  274. $list = array();
  275. if ($include_default) {
  276. $list['upload'] = t('Upload');
  277. }
  278. foreach ($info as $key => $source) {
  279. $list[$key] = $source['name'];
  280. }
  281. return $list;
  282. }
  283. /**
  284. * Implements hook_filefield_sources_info().
  285. */
  286. function filefield_sources_filefield_sources_info() {
  287. $params = array();
  288. return filefield_sources_invoke_all('info', $params);
  289. }
  290. /**
  291. * Load all the potential sources.
  292. */
  293. function filefield_sources_includes($include = TRUE, $enabled_only = TRUE) {
  294. if ($enabled_only) {
  295. $enabled_includes = variable_get('filefield_sources', filefield_sources_includes(FALSE, FALSE));
  296. }
  297. $includes = array();
  298. $directory = drupal_get_path('module', 'filefield_sources') . '/sources';
  299. foreach (file_scan_directory($directory, '/\.inc$/') as $file) {
  300. if (!$enabled_only || in_array($file->name, $enabled_includes)) {
  301. $includes[] = $file->name;
  302. if ($include) {
  303. include_once(DRUPAL_ROOT . '/' . $file->uri);
  304. }
  305. }
  306. }
  307. return $includes;
  308. }
  309. /**
  310. * Save a file into the database after validating it.
  311. *
  312. * This function is identical to the core function file_save_upload() except
  313. * that it accepts an input file path instead of an input file source name.
  314. *
  315. * @see file_save_upload().
  316. */
  317. function filefield_sources_save_file($filepath, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) {
  318. global $user;
  319. // Begin building file object.
  320. $file = new stdClass();
  321. $file->uid = $user->uid;
  322. $file->status = 0;
  323. $file->filename = trim(basename($filepath), '.');
  324. $file->uri = $filepath;
  325. $file->filemime = file_get_mimetype($file->filename);
  326. $file->filesize = filesize($filepath);
  327. $extensions = '';
  328. if (isset($validators['file_validate_extensions'])) {
  329. if (isset($validators['file_validate_extensions'][0])) {
  330. // Build the list of non-munged extensions if the caller provided them.
  331. $extensions = $validators['file_validate_extensions'][0];
  332. }
  333. else {
  334. // If 'file_validate_extensions' is set and the list is empty then the
  335. // caller wants to allow any extension. In this case we have to remove the
  336. // validator or else it will reject all extensions.
  337. unset($validators['file_validate_extensions']);
  338. }
  339. }
  340. else {
  341. // No validator was provided, so add one using the default list.
  342. // Build a default non-munged safe list for file_munge_filename().
  343. $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
  344. $validators['file_validate_extensions'] = array();
  345. $validators['file_validate_extensions'][0] = $extensions;
  346. }
  347. if (!empty($extensions)) {
  348. // Munge the filename to protect against possible malicious extension hiding
  349. // within an unknown file type (ie: filename.html.foo).
  350. $file->filename = file_munge_filename($file->filename, $extensions);
  351. }
  352. // Rename potentially executable files, to help prevent exploits (i.e. will
  353. // rename filename.php.foo and filename.php to filename.php.foo.txt and
  354. // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
  355. // evaluates to TRUE.
  356. if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) {
  357. $file->filemime = 'text/plain';
  358. $file->uri .= '.txt';
  359. $file->filename .= '.txt';
  360. // The .txt extension may not be in the allowed list of extensions. We have
  361. // to add it here or else the file upload will fail.
  362. if (!empty($extensions)) {
  363. $validators['file_validate_extensions'][0] .= ' txt';
  364. drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->filename)));
  365. }
  366. }
  367. // If the destination is not provided, use the temporary directory.
  368. if (empty($destination)) {
  369. $destination = 'temporary://';
  370. }
  371. // Assert that the destination contains a valid stream.
  372. $destination_scheme = file_uri_scheme($destination);
  373. if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) {
  374. drupal_set_message(t('The file could not be uploaded, because the destination %destination is invalid.', array('%destination' => $destination)), 'error');
  375. return FALSE;
  376. }
  377. // A URI may already have a trailing slash or look like "public://".
  378. if (substr($destination, -1) != '/') {
  379. $destination .= '/';
  380. }
  381. // Ensure the destination is writable.
  382. file_prepare_directory($destination, FILE_CREATE_DIRECTORY);
  383. $file->destination = file_destination($destination . $file->filename, $replace);
  384. // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and
  385. // there's an existing file so we need to bail.
  386. if ($file->destination === FALSE) {
  387. drupal_set_message(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $file->filename, '%directory' => $destination)), 'error');
  388. return FALSE;
  389. }
  390. // Add in our check of the the file name length.
  391. $validators['file_validate_name_length'] = array();
  392. // Call the validation functions specified by this function's caller.
  393. $errors = file_validate($file, $validators);
  394. // Check for errors.
  395. if (!empty($errors)) {
  396. $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename));
  397. if (count($errors) > 1) {
  398. $message .= theme('item_list', array('items' => $errors));
  399. }
  400. else {
  401. $message .= ' ' . array_pop($errors);
  402. }
  403. drupal_set_message($message, 'error');
  404. return FALSE;
  405. }
  406. // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary
  407. // directory. This overcomes open_basedir restrictions for future file
  408. // operations.
  409. $file->uri = $file->destination;
  410. if (!file_unmanaged_copy($filepath, $file->uri, $replace)) {
  411. drupal_set_message(t('File upload error. Could not move uploaded file.'), 'error');
  412. watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri));
  413. return FALSE;
  414. }
  415. // Set the permissions on the new file.
  416. drupal_chmod($file->uri);
  417. // If we are replacing an existing file re-use its database record.
  418. if ($replace == FILE_EXISTS_REPLACE) {
  419. $existing_files = file_load_multiple(array(), array('uri' => $file->uri));
  420. if (count($existing_files)) {
  421. $existing = reset($existing_files);
  422. $file->fid = $existing->fid;
  423. }
  424. }
  425. // If we made it this far it's safe to record this file in the database.
  426. return file_save($file);
  427. }
  428. /**
  429. * Clean up the file name, munging extensions and transliterating.
  430. *
  431. * @param $filepath
  432. * A string containing a file name or full path. Only the file name will
  433. * actually be modified.
  434. * @return
  435. * A file path with a cleaned-up file name.
  436. */
  437. function filefield_sources_clean_filename($filepath, $extensions) {
  438. global $user;
  439. $filename = basename($filepath);
  440. if (module_exists('transliteration')) {
  441. module_load_include('inc', 'transliteration');
  442. $langcode = NULL;
  443. if (!empty($_POST['language'])) {
  444. $languages = language_list();
  445. $langcode = isset($languages[$_POST['language']]) ? $_POST['language'] : NULL;
  446. }
  447. $filename = transliteration_clean_filename($filename, $langcode);
  448. }
  449. // Because this transfer mechanism does not use file_save_upload(), we need
  450. // to manually munge the filename to prevent dangerous extensions.
  451. // See file_save_upload().
  452. if (empty($extensions)) {
  453. $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
  454. }
  455. $filename = file_munge_filename($filename, $extensions);
  456. $directory = drupal_dirname($filepath);
  457. return ($directory != '.' ? $directory . '/' : '') . $filename;
  458. }
  459. /**
  460. * Theme the display of the sources list.
  461. */
  462. function theme_filefield_sources_list($variables) {
  463. $element = $variables['element'];
  464. $sources = $variables['sources'];
  465. $links = array();
  466. // Add the default "Upload" since it's not in our list.
  467. $default['upload'] = array(
  468. 'label' => t('Upload'),
  469. 'description' => t('Upload a file from your computer.'),
  470. );
  471. $sources = array_merge($default, $sources);
  472. foreach ($sources as $name => $source) {
  473. $links[] = '<a href="#" onclick="return false;" title="' . $source['description'] . '" id="' . $element['#id'] . '-' . $name . '-source" class="filefield-source filefield-source-' . $name . '">' . $source['label'] . '</a>';
  474. }
  475. return '<div class="filefield-sources-list">' . implode(' | ', $links) . '</div>';
  476. }
  477. /**
  478. * Validate a file based on the $element['#upload_validators'] property.
  479. */
  480. function filefield_sources_element_validate($element, $file) {
  481. $validators = $element['#upload_validators'];
  482. $errors = array();
  483. // Since this frequently is used to reference existing files, check that
  484. // they exist first in addition to the normal validations.
  485. if (!file_exists($file->uri)) {
  486. $errors[] = t('The file does not exist.');
  487. }
  488. // Call the validation functions.
  489. else {
  490. foreach ($validators as $function => $args) {
  491. // Add the $file variable to the list of arguments and pass it by
  492. // reference (required for PHP 5.3 and higher).
  493. array_unshift($args, NULL);
  494. $args[0] = &$file;
  495. $errors = array_merge($errors, call_user_func_array($function, $args));
  496. }
  497. }
  498. // Check for validation errors.
  499. if (!empty($errors)) {
  500. $message = t('The selected file %name could not be referenced.', array('%name' => $file->filename));
  501. if (count($errors) > 1) {
  502. $message .= '<ul><li>' . implode('</li><li>', $errors) . '</li></ul>';
  503. }
  504. else {
  505. $message .= ' ' . array_pop($errors);
  506. }
  507. form_error($element, $message);
  508. return 0;
  509. }
  510. return 1;
  511. }
  512. /**
  513. * Generate help text based on the $element['#upload_validators'] property.
  514. */
  515. function filefield_sources_element_validation_help($validators) {
  516. $desc = array();
  517. foreach ($validators as $callback => $arguments) {
  518. $help_func = $callback . '_help';
  519. if (function_exists($help_func)) {
  520. $desc[] = call_user_func_array($help_func, $arguments);
  521. }
  522. }
  523. return empty($desc) ? '' : implode('<br />', $desc);
  524. }
  525. /**
  526. * Menu access callback; Checks user access to edit a file field.
  527. */
  528. function _filefield_sources_field_access($entity_type, $bundle_name, $field_name) {
  529. $field = field_info_field($field_name);
  530. return field_access('edit', $field, $entity_type);
  531. }
  532. /**
  533. * Custom sort function for ordering sources.
  534. */
  535. function _filefield_sources_sort($a, $b) {
  536. $a = (array)$a + array('weight' => 0, 'label' => '');
  537. $b = (array)$b + array('weight' => 0, 'label' => '');
  538. return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : strnatcasecmp($a['label'], $b['label']));
  539. }