X-Git-Url: http://www.aleph1.co.uk/gitweb/?p=yaffs-website;a=blobdiff_plain;f=web%2Fmodules%2Fcontrib%2Ffilefield_sources%2Ffilefield_sources.module;fp=web%2Fmodules%2Fcontrib%2Ffilefield_sources%2Ffilefield_sources.module;h=5562a1271f353821e526e3d9a2b7ea5af1dea997;hp=0000000000000000000000000000000000000000;hb=8acec36f19c470dfcda1ae2336826a782f41874c;hpb=e0411c4e83ba0d079034db83c3f7f55be24a0e35 diff --git a/web/modules/contrib/filefield_sources/filefield_sources.module b/web/modules/contrib/filefield_sources/filefield_sources.module new file mode 100644 index 000000000..5562a1271 --- /dev/null +++ b/web/modules/contrib/filefield_sources/filefield_sources.module @@ -0,0 +1,771 @@ + 'element', + 'function' => 'theme_filefield_sources_element', + ); + + $theme['filefield_sources_list'] = array( + 'variables' => array('element' => NULL, 'sources' => NULL), + 'function' => 'theme_filefield_sources_list', + ); + + return $theme; +} + +/** + * Implements hook_field_widget_third_party_settings_form(). + * + * Add file field sources settings form to supported field widget forms. + * + * @see \Drupal\field_ui\FormDisplayOverview + */ +function filefield_sources_field_widget_third_party_settings_form(WidgetInterface $plugin, FieldDefinitionInterface $field_definition, $form_mode, $form, FormStateInterface $form_state) { + $element = array(); + if (in_array($plugin->getPluginId(), \Drupal::moduleHandler()->invokeAll('filefield_sources_widgets'))) { + $element = filefield_sources_form($plugin, $form_state); + } + return $element; +} + +/** + * Implements hook_field_widget_settings_summary_alter(). + * + * Add file field sources information to the field widget settings summary. + * + * @see \Drupal\field_ui\FormDisplayOverview + */ +function filefield_sources_field_widget_settings_summary_alter(&$summary, $context) { + $plugin = $context['widget']; + if (in_array($plugin->getPluginId(), \Drupal::moduleHandler()->invokeAll('filefield_sources_widgets'))) { + $settings = $plugin->getThirdPartySetting('filefield_sources', 'filefield_sources'); + $enabled_sources = _filefield_sources_enabled($settings); + $summary[] = t('File field sources:') . ' ' . implode(', ', array_keys($enabled_sources)); + } +} + +/** + * Implements hook_field_widget_form_alter(). + * + * Add file field sources widget's settings to element. + */ +function filefield_sources_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) { + $plugin = $context['widget']; + if (in_array($plugin->getPluginId(), \Drupal::moduleHandler()->invokeAll('filefield_sources_widgets'))) { + $element['#filefield_sources_settings'] = $plugin->getThirdPartySetting('filefield_sources', 'filefield_sources'); + + // Bundle is missing in element. + $items = $context['items']; + $element['#bundle'] = $items->getEntity()->bundle(); + } +} + +/** + * Implements hook_filefield_sources_widgets(). + * + * This returns a list of widgets that are compatible with FileField Sources. + */ +function filefield_sources_filefield_sources_widgets() { + return array('file_generic', 'image_image'); +} + +/** + * Configuration form for editing FileField Sources settings for a widget. + */ +function filefield_sources_form($plugin, FormStateInterface $form_state) { + $settings = $plugin->getThirdPartySetting('filefield_sources', 'filefield_sources'); + + // Backward compatibility: auto-enable 'upload'. + $enabled = _filefield_sources_enabled($settings); + + $form['filefield_sources'] = array( + '#type' => 'details', + '#title' => t('File sources'), + '#weight' => 20, + ); + + $sources = filefield_sources_list(); + $form['filefield_sources']['sources'] = array( + '#type' => 'checkboxes', + '#title' => t('Enabled sources'), + '#options' => $sources, + '#default_value' => $enabled, + '#description' => t('Select the available locations from which this widget may select files.'), + ); + + $params = array($plugin); + $form['filefield_sources'] = array_merge($form['filefield_sources'], filefield_sources_invoke_all('settings', $params)); + + return $form; +} + +/** + * A #process callback to extend the filefield_widget element type. + * + * Add the central JavaScript and CSS files that allow switching between + * different sources. Third-party modules can also add to the list of sources + * by implementing hook_filefield_sources_info(). + */ +function filefield_sources_field_process(&$element, FormStateInterface $form_state, &$complete_form) { + static $js_added; + + // Check if we are processing file field sources. + if (!isset($element['#filefield_sources_settings'])) { + return $element; + } + + // Do all processing as needed by each source. + $sources = filefield_sources_info(); + $settings = $element['#filefield_sources_settings']; + $enabled_sources = _filefield_sources_enabled($settings); + + $context = array( + 'enabled_sources' => &$enabled_sources, + 'element' => $element, + 'form_state' => $form_state, + ); + + // Allow other modules to alter the sources. + \Drupal::moduleHandler()->alter('filefield_sources_sources', $sources, $context); + + foreach ($sources as $source_name => $source) { + if (empty($enabled_sources[$source_name])) { + unset($sources[$source_name]); + } + // Default upload plugin does not have class. + elseif (isset($source['class'])) { + $callback = array($source['class'], 'process'); + if (is_callable($callback)) { + $element = call_user_func_array($callback, array( + &$element, + $form_state, + &$complete_form, + )); + } + } + } + $element['#filefield_sources'] = $sources; + + // Exit out if not adding any sources. + if (empty($sources)) { + return $element; + } + + // Hide default 'upload' type? + if (!isset($enabled_sources['upload'])) { + foreach (array('upload_button', 'upload') as $field) { + if (isset($element[$field])) { + $element[$field]['#access'] = FALSE; + } + } + } + + // Add class to upload button. + $element['upload_button']['#attributes']['class'][] = 'upload-button'; + + $element['#attached']['library'][] = 'filefield_sources/drupal.filefield_sources'; + + // Check the element for hint text that might need to be added. + foreach (Element::children($element) as $key) { + if (isset($element[$key]['#filefield_sources_hint_text']) && !isset($js_added[$key])) { + $type = str_replace('filefield_', '', $key); + + $element['#attached']['drupalSettings']['fileFieldSources'][$type] = array( + 'hintText' => $element[$key]['#filefield_sources_hint_text'], + ); + + $js_added[$key] = TRUE; + } + } + + // Adjust the Ajax settings so that on upload and remove of any individual + // file, the entire group of file fields is updated together. + // Clone of Drupal\file\Plugin\Field\FieldWidget\FileWidget::process(). + if ($element['#cardinality'] != 1) { + $parents = array_slice($element['#array_parents'], 0, -1); + $new_options = array( + 'query' => array( + 'element_parents' => implode('/', $parents), + ), + ); + $field_element = NestedArray::getValue($complete_form, $parents); + $new_wrapper = $field_element['#id'] . '-ajax-wrapper'; + foreach (Element::children($element) as $key) { + foreach (Element::children($element[$key]) as $subkey) { + if (isset($element[$key][$subkey]['#ajax'])) { + $element[$key][$subkey]['#ajax']['options'] = $new_options; + $element[$key][$subkey]['#ajax']['wrapper'] = $new_wrapper; + $element[$key][$subkey]['#limit_validation_errors'] = array( + array_slice($element['#array_parents'], 0, -2), + ); + } + } + } + unset($element['#prefix'], $element['#suffix']); + } + + // Add the list of sources to the element for toggling between sources. + if (empty($element['fids']['#value'])) { + if (count($enabled_sources) > 1) { + $element['filefield_sources_list'] = array( + '#theme' => 'filefield_sources_list', + '#element' => $element, + '#sources' => $sources, + '#weight' => -20, + ); + } + } + + return $element; +} + +/** + * A #pre_render function to hide sources if a file is currently uploaded. + */ +function filefield_sources_field_pre_render($element) { + // If we already have a file, we don't want to show the upload controls. + if (!empty($element['#value']['fids'])) { + foreach (Element::children($element) as $key) { + if (!empty($element[$key]['#filefield_source'])) { + $element[$key]['#access'] = FALSE; + } + } + } + return $element; +} + +/** + * An #element_validate function to run source validations. + */ +function filefield_sources_field_validate(&$element, FormStateInterface $form_state, &$complete_form) { + // Do all processing as needed by each source. + $sources = filefield_sources_info(); + foreach ($sources as $source) { + if (!isset($source['class'])) { + continue; + } + + $callback = array($source['class'], 'validate'); + if (is_callable($callback)) { + call_user_func_array($callback, array( + $element, + $form_state, + $complete_form, + )); + } + } +} + +/** + * Form submission handler for all FileField Source buttons. + * + * Clone of \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit(), with + * a few changes: + * - Submit button is one level down compare to 'Upload' source's submit + * button. + * - Replace static in static::getWidgetState and static::setWidgetState by + * WidgetBase. + * - Rebuild the form after all. + */ +function filefield_sources_field_submit(&$form, FormStateInterface $form_state) { + // During the form rebuild, formElement() will create field item widget + // elements using re-indexed deltas, so clear out FormState::$input to + // avoid a mismatch between old and new deltas. The rebuilt elements will + // have #default_value set appropriately for the current state of the field, + // so nothing is lost in doing this. + $button = $form_state->getTriggeringElement(); + $parents = array_slice($button['#parents'], 0, -3); + NestedArray::setValue($form_state->getUserInput(), $parents, NULL); + + // Go one level up in the form, to the widgets container. + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -2)); + $field_name = $element['#field_name']; + $parents = $element['#field_parents']; + + $submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -3)); + foreach ($submitted_values as $delta => $submitted_value) { + if (empty($submitted_value['fids'])) { + unset($submitted_values[$delta]); + } + } + + // If there are more files uploaded via the same widget, we have to separate + // them, as we display each file in it's own widget. + $new_values = array(); + foreach ($submitted_values as $delta => $submitted_value) { + if (is_array($submitted_value['fids'])) { + foreach ($submitted_value['fids'] as $fid) { + $new_value = $submitted_value; + $new_value['fids'] = array($fid); + $new_values[] = $new_value; + } + } + else { + $new_value = $submitted_value; + } + } + + // Re-index deltas after removing empty items. + $submitted_values = array_values($new_values); + + // Update form_state values. + NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -3), $submitted_values); + + // Update items. + $field_state = WidgetBase::getWidgetState($parents, $field_name, $form_state); + $field_state['items'] = $submitted_values; + WidgetBase::setWidgetState($parents, $field_name, $form_state, $field_state); + + // We need to rebuild the form, so that uploaded file can be displayed. + $form_state->setRebuild(); +} + +/** + * A #filefield_value_callback to run source value callbacks. + */ +function filefield_sources_field_value(&$element, &$input, FormStateInterface $form_state) { + // Do all processing as needed by each source. + $sources = filefield_sources_info(); + foreach ($sources as $source) { + if (isset($source['class'])) { + $callback = array($source['class'], 'value'); + if (is_callable($callback)) { + call_user_func_array($callback, array(&$element, &$input, $form_state)); + } + } + } +} + +/** + * Call all FileField Source hooks stored in the available include files. + */ +function filefield_sources_invoke_all($method, &$params) { + $return = array(); + foreach (\Drupal::service('filefield_sources')->getDefinitions() as $definition) { + if (!isset($definition['class'])) { + continue; + } + // Get routes defined by each plugin. + $callback = array($definition['class'], $method); + if (is_callable($callback)) { + $result = call_user_func_array($callback, $params); + if (isset($result) && is_array($result)) { + $return = array_merge_recursive($return, $result); + } + elseif (isset($result)) { + $return[] = $result; + } + } + } + return $return; +} + +/** + * Load hook_filefield_sources_info() data from all modules. + */ +function filefield_sources_info($include_default = TRUE) { + $info = \Drupal::service('filefield_sources')->getDefinitions(); + if (isset($info['imce']) && !Imce::access()) { + unset($info['imce']); + } + if ($include_default) { + $info['upload'] = array( + 'name' => t('Upload (default)'), + 'label' => t('Upload'), + 'description' => t('Upload a file from your computer.'), + 'weight' => -10, + ); + } + + uasort($info, '_filefield_sources_sort'); + + return $info; +} + +/** + * Create a list of FileField Sources by name, suitable for a select list. + */ +function filefield_sources_list($include_default = TRUE) { + $info = filefield_sources_info($include_default); + $list = array(); + + foreach ($info as $key => $source) { + $list[$key] = $source['name']; + } + + return $list; +} + +/** + * Save a file into the database after validating it. + * + * This function is identical to the core function file_save_upload() except + * that it accepts an input file path instead of an input file source name. + * + * @see file_save_upload() + */ +function filefield_sources_save_file($filepath, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) { + $user = \Drupal::currentUser(); + + // Begin building file object. + $file = entity_create('file', array( + 'uri' => $filepath, + 'uid' => $user->id(), + 'status' => FILE_EXISTS_RENAME, + )); + $file->setFilename(trim(basename($filepath), '.')); + $file->setMimeType(\Drupal::service('file.mime_type.guesser')->guess($file->getFilename())); + $file->setSize(filesize($filepath)); + + $extensions = ''; + if (isset($validators['file_validate_extensions'])) { + if (isset($validators['file_validate_extensions'][0])) { + // Build the list of non-munged extensions if the caller provided them. + $extensions = $validators['file_validate_extensions'][0]; + } + else { + // If 'file_validate_extensions' is set and the list is empty then the + // caller wants to allow any extension. In this case we have to remove the + // validator or else it will reject all extensions. + unset($validators['file_validate_extensions']); + } + } + else { + // No validator was provided, so add one using the default list. + // Build a default non-munged safe list for file_munge_filename(). + $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'; + $validators['file_validate_extensions'] = array(); + $validators['file_validate_extensions'][0] = $extensions; + } + + if (!empty($extensions)) { + // Munge the filename to protect against possible malicious extension hiding + // within an unknown file type (ie: filename.html.foo). + $file->setFilename(file_munge_filename($file->getFilename(), $extensions)); + } + + // Rename potentially executable files, to help prevent exploits (i.e. will + // rename filename.php.foo and filename.php to filename.php.foo.txt and + // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads' + // evaluates to TRUE. + if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) { + $file->setMimeType('text/plain'); + $file->setFileUri($file->getFileUri() . '.txt'); + $file->setFilename($file->getFilename() . '.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)) { + $validators['file_validate_extensions'][0] .= ' txt'; + drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->getFilename()))); + } + } + + // If the destination is not provided, use the temporary directory. + if (empty($destination)) { + $destination = 'temporary://'; + } + + // Assert that the destination contains a valid stream. + $destination_scheme = file_uri_scheme($destination); + if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) { + drupal_set_message(t('The file could not be uploaded, because the destination %destination is invalid.', array('%destination' => $destination)), 'error'); + return FALSE; + } + + // A URI may already have a trailing slash or look like "public://". + if (substr($destination, -1) != '/') { + $destination .= '/'; + } + + // Ensure the destination is writable. + file_prepare_directory($destination, FILE_CREATE_DIRECTORY); + + // Check if this is actually the same file being "attached" to a file record. + // If so, it acts as a file replace, except no file is actually moved. + $reuse_file = ($destination . $file->getFilename() === $file->getFileUri()); + if ($reuse_file) { + $replace = FILE_EXISTS_REPLACE; + } + + $file->destination = file_destination($destination . $file->getFilename(), $replace); + // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and + // there's an existing file so we need to bail. + if ($file->destination === FALSE) { + 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->getFilename(), '%directory' => $destination)), 'error'); + return FALSE; + } + + // Add in our check of the the file name length. + $validators['file_validate_name_length'] = array(); + + // Call the validation functions specified by this function's caller. + $errors = file_validate($file, $validators); + + // Check for errors. + if (!empty($errors)) { + $message = t('The specified file %name could not be uploaded.', array('%name' => $file->getFilename())); + if (count($errors) > 1) { + $message .= theme('item_list', array('items' => $errors)); + } + else { + $message .= ' ' . array_pop($errors); + } + drupal_set_message($message, 'error'); + return FALSE; + } + + // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary + // directory. This overcomes open_basedir restrictions for future file + // operations. + $file->setFileUri($file->destination); + if (!$reuse_file && !file_unmanaged_copy($filepath, $file->getFileUri(), $replace)) { + drupal_set_message(t('File upload error. Could not move uploaded file.'), 'error'); + \Drupal::logger('filefield_sources')->log(E_NOTICE, 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->getFilename(), '%destination' => $file->getFileUri())); + return FALSE; + } + + // Set the permissions on the new file. + drupal_chmod($file->getFileUri()); + + // If we are replacing an existing file re-use its database record. + if ($replace == FILE_EXISTS_REPLACE) { + $existing_files = file_load_multiple(array(), array('uri' => $file->getFileUri())); + if (count($existing_files)) { + $existing = reset($existing_files); + $file->setOriginalId($existing->id()); + } + } + + // If we made it this far it's safe to record this file in the database. + $file->save(); + return $file; +} + +/** + * Clean up the file name, munging extensions and transliterating. + * + * @param string $filepath + * A string containing a file name or full path. Only the file name will + * actually be modified. + * + * @return string + * A file path with a cleaned-up file name. + */ +function filefield_sources_clean_filename($filepath, $extensions) { + $filename = basename($filepath); + + if (\Drupal::moduleHandler()->moduleExists('transliteration')) { + module_load_include('inc', 'transliteration'); + + $langcode = NULL; + if (!empty($_POST['language'])) { + $languages = language_list(); + $langcode = isset($languages[$_POST['language']]) ? $_POST['language'] : NULL; + } + $filename = transliteration_clean_filename($filename, $langcode); + } + + // Because this transfer mechanism does not use file_save_upload(), we need + // to manually munge the filename to prevent dangerous extensions. + // See file_save_upload(). + if (empty($extensions)) { + $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'; + } + $filename = file_munge_filename($filename, $extensions); + $directory = drupal_dirname($filepath); + return ($directory != '.' ? $directory . '/' : '') . $filename; +} + +/** + * Theme the display of the source element. + */ +function theme_filefield_sources_element($variables) { + $element = $variables['element']; + $source_id = $element['#source_id']; + $method = isset($element['#method']) ? $element['#method'] : 'element'; + $extra_variables = isset($element['#variables']) ? $element['#variables'] : array(); + + $sources = filefield_sources_info(); + if (isset($sources[$source_id]['class'])) { + $callback = array($sources[$source_id]['class'], $method); + if (is_callable($callback)) { + $variables = array_merge($variables, $extra_variables); + return call_user_func_array($callback, array($variables)); + } + } + + return ''; +} + +/** + * Theme the display of the sources list. + */ +function theme_filefield_sources_list($variables) { + $element = $variables['element']; + $sources = $variables['sources']; + + $links = array(); + + foreach ($sources as $name => $source) { + $links[] = '' . $source['label'] . ''; + } + return '
' . implode(' | ', $links) . '
'; +} + +/** + * Validate a file based on the $element['#upload_validators'] property. + */ +function filefield_sources_element_validate($element, $file, FormStateInterface $form_state) { + $validators = $element['#upload_validators']; + $errors = array(); + + // Since this frequently is used to reference existing files, check that + // they exist first in addition to the normal validations. + if (!file_exists($file->getFileUri())) { + $errors[] = t('The file does not exist.'); + } + // Call the validation functions. + else { + foreach ($validators as $function => $args) { + // Add the $file variable to the list of arguments and pass it by + // reference (required for PHP 5.3 and higher). + array_unshift($args, NULL); + $args[0] = &$file; + $errors = array_merge($errors, call_user_func_array($function, $args)); + } + } + + // Check for validation errors. + if (!empty($errors)) { + $message = t('The selected file %name could not be referenced.', array('%name' => $file->filename)); + if (count($errors) > 1) { + $message .= ''; + } + else { + $message .= ' ' . array_pop($errors); + } + $form_state->setError($element, $message); + return 0; + } + + return 1; +} + +/** + * Generate help text based on the $element['#upload_validators'] property. + */ +function filefield_sources_element_validation_help($validators) { + $desc = array(); + foreach ($validators as $callback => $arguments) { + $help_func = $callback . '_help'; + if (function_exists($help_func)) { + $desc[] = call_user_func_array($help_func, $arguments); + } + } + return empty($desc) ? '' : implode('
', $desc); +} + +/** + * Custom sort function for ordering sources. + */ +function _filefield_sources_sort($a, $b) { + $a = (array) $a + array('weight' => 0, 'label' => ''); + $b = (array) $b + array('weight' => 0, 'label' => ''); + return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : strnatcasecmp($a['label'], $b['label'])); +} + +/** + * Helper to return enabled sources for a field. + * + * This provides backward compatibility for 'upload' type. + * + * @see http://drupal.org/node/932994 + */ +function _filefield_sources_enabled($settings) { + if (!isset($settings['sources']['upload'])) { + $settings['sources']['upload'] = 'upload'; + } + + $enabled = array_keys(array_filter($settings['sources'])); + asort($enabled); + return array_combine($enabled, $enabled); +} + +// @todo Remove once https://www.drupal.org/node/1808132 is finished. +if (!function_exists('module_get_weight')) { + /** + * Gets weight of a particular module. + * + * @param string $module + * The name of the module (without the .module extension). + * + * @return int + * The configured weight of the module. + * + * @throws InvalidArgumentException + * Thrown in case the given module is not installed in the system. + */ + function module_get_weight($module) { + $weight = \Drupal::config('core.extension')->get("module.$module"); + if ($weight !== NULL) { + return (int) $weight; + } + $weight = \Drupal::config('core.extension')->get("disabled.module.$module"); + if ($weight !== NULL) { + return (int) $weight; + } + throw new InvalidArgumentException(format_string('The module %module is not installed.', array('%module' => $module))); + } +} + +/** + * Check for CURL extension enabled. + */ +function filefield_sources_curl_enabled() { + return function_exists('curl_version'); +}