diff --git a/README.txt b/README.txt index ba64a4d..58304d5 100644 --- a/README.txt +++ b/README.txt @@ -17,19 +17,20 @@ Author: Nathan Haug (quicksketch) Installation ------------ 1) Place this module directory in your modules folder (this will usually be - "sites/all/modules/"). + "modules/"). 2) Enable the module within your Drupal site. 3) Add or configure an existing file or image field. To configure a typical node - field, visit Admin -> Structure -> Content types and click "manage fields" - on a type you'd like to modify. Add a new file field or edit an existing one. + field, visit Manage -> Structure -> Content types and click + "Manage form display" on a type you'd like to modify. Add a new file field or + edit an existing one. While editing the file or image field, you'll have new options available - under a "File sources" fieldset. You can enable the desired sources for that + under a "File sources" details. You can enable the desired sources for that particular field. -4) Create a piece of content that uses your file file and try it out. +4) Create a piece of content that uses your file and try it out. Support ------- diff --git a/config/schema/filefield_sources.data_types.schema.yml b/config/schema/filefield_sources.data_types.schema.yml new file mode 100644 index 0000000..1d0e67d --- /dev/null +++ b/config/schema/filefield_sources.data_types.schema.yml @@ -0,0 +1,5 @@ +# Basic data types for FileField Sources. + +filefield_sources_source: + type: boolean + label: 'Plugin' diff --git a/config/schema/filefield_sources.schema.yml b/config/schema/filefield_sources.schema.yml new file mode 100644 index 0000000..a4bb0e7 --- /dev/null +++ b/config/schema/filefield_sources.schema.yml @@ -0,0 +1,9 @@ +field.widget.third_party.filefield_sources: + type: mapping + label: 'filefield_sources entity form display settings' + mapping: + filefield_sources: + type: sequence + label: 'filefield_sources settings' + sequence: + - type: filefield_sources.setting.[%key] diff --git a/config/schema/filefield_sources.setting.source_attach.schema.yml b/config/schema/filefield_sources.setting.source_attach.schema.yml new file mode 100644 index 0000000..e94e164 --- /dev/null +++ b/config/schema/filefield_sources.setting.source_attach.schema.yml @@ -0,0 +1,13 @@ +filefield_sources.setting.source_attach: + type: mapping + label: 'File attach settings' + mapping: + path: + type: string + label: 'File attach path' + absolute: + type: integer + label: 'File attach location' + attach_mode: + type: string + label: 'Attach method' \ No newline at end of file diff --git a/config/schema/filefield_sources.setting.source_reference.schema.yml b/config/schema/filefield_sources.setting.source_reference.schema.yml new file mode 100644 index 0000000..354679b --- /dev/null +++ b/config/schema/filefield_sources.setting.source_reference.schema.yml @@ -0,0 +1,7 @@ +filefield_sources.setting.source_reference: + type: mapping + label: 'Autocomplete reference options' + mapping: + autocomplete: + type: string + label: 'Match file name' \ No newline at end of file diff --git a/config/schema/filefield_sources.setting.sources.schema.yml b/config/schema/filefield_sources.setting.sources.schema.yml new file mode 100644 index 0000000..cd50a51 --- /dev/null +++ b/config/schema/filefield_sources.setting.sources.schema.yml @@ -0,0 +1,5 @@ +filefield_sources.setting.sources: + type: sequence + label: 'filefield_sources source' + sequence: + - type: filefield_sources.source.[%key] diff --git a/config/schema/filefield_sources.source.attach.schema.yml b/config/schema/filefield_sources.source.attach.schema.yml new file mode 100644 index 0000000..ab7a53f --- /dev/null +++ b/config/schema/filefield_sources.source.attach.schema.yml @@ -0,0 +1,3 @@ +filefield_sources.source.attach: + type: filefield_sources_source + label: 'File attach' diff --git a/config/schema/filefield_sources.source.clipboard.schema.yml b/config/schema/filefield_sources.source.clipboard.schema.yml new file mode 100644 index 0000000..e400679 --- /dev/null +++ b/config/schema/filefield_sources.source.clipboard.schema.yml @@ -0,0 +1,3 @@ +filefield_sources.source.clipboard: + type: filefield_sources_source + label: 'Clipboard' diff --git a/config/schema/filefield_sources.source.imce.schema.yml b/config/schema/filefield_sources.source.imce.schema.yml new file mode 100644 index 0000000..ec3b5a3 --- /dev/null +++ b/config/schema/filefield_sources.source.imce.schema.yml @@ -0,0 +1,3 @@ +filefield_sources.source.imce: + type: filefield_sources_source + label: 'File browser' diff --git a/config/schema/filefield_sources.source.reference.schema.yml b/config/schema/filefield_sources.source.reference.schema.yml new file mode 100644 index 0000000..4f77b13 --- /dev/null +++ b/config/schema/filefield_sources.source.reference.schema.yml @@ -0,0 +1,3 @@ +filefield_sources.source.reference: + type: filefield_sources_source + label: 'Reference existing' diff --git a/config/schema/filefield_sources.source.remote.schema.yml b/config/schema/filefield_sources.source.remote.schema.yml new file mode 100644 index 0000000..a26f8dc --- /dev/null +++ b/config/schema/filefield_sources.source.remote.schema.yml @@ -0,0 +1,3 @@ +filefield_sources.source.remote: + type: filefield_sources_source + label: 'Remote URL' diff --git a/config/schema/filefield_sources.source.upload.schema.yml b/config/schema/filefield_sources.source.upload.schema.yml new file mode 100644 index 0000000..35208c7 --- /dev/null +++ b/config/schema/filefield_sources.source.upload.schema.yml @@ -0,0 +1,5 @@ +# Schema for the filefield_sources source plugins. + +filefield_sources.source.upload: + type: filefield_sources_source + label: 'Upload' diff --git a/css/filefield_sources.css b/css/filefield_sources.css new file mode 100644 index 0000000..4754fde --- /dev/null +++ b/css/filefield_sources.css @@ -0,0 +1,44 @@ +/* Generic display for all sources. */ + +div.filefield-source input.form-text, +div.filefield-source select.form-select { + display: inline; + width: 20em; +} + +div.filefield-source .form-item { + white-space: normal; +} + +div.filefield-source .hint { + color: #999; +} + +div.filefield-sources-list a.active { + font-weight: bold; +} + +/* Clipboard source. */ +div.filefield-source-clipboard-capture { + border: 1px solid #ccc; + width: 20em; + height: 1.4em; + padding: 2px; + display: inline-block; + vertical-align: top; + overflow: hidden; +} +div.filefield-source-clipboard-capture img { + display: none; +} + +/* Reference source. */ +div.filefield-source-reference-item { + font-size: 90%; +} + +/* Remote source. */ +div.filefield-source-remote input.form-text { + /* Helps with display consistency since references has a background. */ + background-image: inherit; +} diff --git a/filefield_sources.api.php b/filefield_sources.api.php index 7f13b58..51142cf 100644 --- a/filefield_sources.api.php +++ b/filefield_sources.api.php @@ -1,10 +1,11 @@ t('File attach from Flickr'), - 'label' => t('Flickr'), - 'description' => t('Select a file from Flickr.'), - // This callback function does all the heavy-work of creating a form element - // to choose a Flickr photo and populate a field. For an example, see - // filefield_source_remote_process(). - 'process' => 'mymodule_filefield_source_flickr_process', - // This callback function then takes the value of that field and saves the - // file locally. For an example, see filefield_source_remote_value(). - 'value' => 'mymodule_filefield_source_flickr_value', - 'weight' => 3, - // This optional setting will ensure that your code is included when needed - // if your value, process, or other callbacks are located in a file other - // than your .module file. - 'file' => 'include/mymodule.flickr_source.inc', - ); - return $sources; -} diff --git a/filefield_sources.css b/filefield_sources.css deleted file mode 100644 index 0d5c8c1..0000000 --- a/filefield_sources.css +++ /dev/null @@ -1,45 +0,0 @@ - -/* Generic display for all sources. */ - -div.filefield-source input.form-text, -div.filefield-source select.form-select { - display: inline; - width: 20em; -} - -div.filefield-source .form-item { - white-space: normal; -} - -div.filefield-source .hint { - color: #999; -} - -div.filefield-sources-list a.active { - font-weight: bold; -} - -/* Clipboard source. */ -div.filefield-source-clipboard-capture { - border: 1px solid #CCC; - width: 20em; - height: 1.4em; - padding: 2px; - display: inline-block; - vertical-align: top; - overflow: hidden; -} -div.filefield-source-clipboard-capture img { - display: none; -} - -/* Reference source. */ -div.filefield-source-reference-item { - font-size: 90%; -} - -/* Remote source. */ -div.filefield-source-remote input.form-text { - /* Helps with display consistency since references has a background. */ - background-image: inherit; -} diff --git a/filefield_sources.info b/filefield_sources.info deleted file mode 100644 index 5ef07c4..0000000 --- a/filefield_sources.info +++ /dev/null @@ -1,5 +0,0 @@ -name = File Field Sources -description = Extends File fields to allow referencing of existing files, remote files, and server files. -dependencies[] = file -package = Fields -core = 7.x diff --git a/filefield_sources.info.yml b/filefield_sources.info.yml new file mode 100644 index 0000000..784e504 --- /dev/null +++ b/filefield_sources.info.yml @@ -0,0 +1,7 @@ +name: File Field Sources +type: module +description: 'Extends File fields to allow referencing of existing files, remote files, and server files.' +package: Fields +core: 8.x +dependencies: + - file diff --git a/filefield_sources.install b/filefield_sources.install index 03a19d8..8dfb84c 100644 --- a/filefield_sources.install +++ b/filefield_sources.install @@ -6,37 +6,18 @@ */ /** - * Implementation of hook_install(). + * Implements hook_install(). */ function filefield_sources_install() { // FileField Sources needs to load after both ImageField and FileField. - db_query("UPDATE {system} SET weight = 5 WHERE type = 'module' AND name = 'filefield_sources'"); -} - -/** - * Enable FileField Sources on all current fields (as was the previous default). - */ -function filefield_sources_update_6001() { - $ret = array(); - - drupal_load('module', 'content'); - module_load_include('inc', 'content', 'includes/content.crud'); - - foreach (content_fields() as $field) { - foreach (node_get_types('types') as $node_type => $type_info) { - if ($type_field = content_fields($field['field_name'], $node_type)) { - $type_field['widget']['filefield_sources'] = array( - 'imce' => 'imce', - 'reference' => 'reference', - 'remote' => 'remote', - ); - content_field_instance_update($type_field); - } - } + try { + $file_weight = module_get_weight('file'); + $image_weight = module_get_weight('image'); + $weight = max(array($file_weight, $image_weight)); + $weight++; } - - // FileField Sources needs to load after both ImageField and FileField. - $ret[] = update_sql("UPDATE {system} SET weight = 5 WHERE type = 'module' AND name = 'filefield_sources'"); - - return $ret; + catch (Exception $e) { + $weight = 5; + } + module_set_weight('filefield_sources', $weight); } diff --git a/filefield_sources.js b/filefield_sources.js deleted file mode 100644 index fd0d0d4..0000000 --- a/filefield_sources.js +++ /dev/null @@ -1,242 +0,0 @@ -(function ($) { - -/** - * Behavior to add source options to configured fields. - */ -Drupal.behaviors.fileFieldSources = {}; -Drupal.behaviors.fileFieldSources.attach = function(context, settings) { - $('div.filefield-sources-list:not(.filefield-sources-processed)', context).each(function() { - $(this).addClass('filefield-sources-processed'); - var $fileFieldElement = $(this).parents('div.form-managed-file:first'); - $(this).find('a').click(function() { - // Remove the active class. - $(this).parents('div.filefield-sources-list').find('a.active').removeClass('active'); - - // Find the unique FileField Source class name. - var fileFieldSourceClass = this.className.match(/filefield-source-[0-9a-z]+/i)[0]; - - // The default upload element is a special case. - if ($(this).is('.filefield-source-upload')) { - $fileFieldElement.find('div.filefield-sources-list').siblings('input.form-file, input.form-submit').css('display', ''); - $fileFieldElement.find('div.filefield-source').css('display', 'none'); - } - else { - $fileFieldElement.find('div.filefield-sources-list').siblings('input.form-file, input.form-submit').css('display', 'none'); - $fileFieldElement.find('div.filefield-source').not('div.' + fileFieldSourceClass).css('display', 'none'); - $fileFieldElement.find('div.' + fileFieldSourceClass).css('display', ''); - } - - // Add the active class. - $(this).addClass('active'); - Drupal.fileFieldSources.updateHintText($fileFieldElement.get(0)); - }).first().triggerHandler('click'); - - // Clipboard support. - $fileFieldElement.find('.filefield-source-clipboard-capture') - .bind('paste', Drupal.fileFieldSources.pasteEvent) - .bind('focus', Drupal.fileFieldSources.pasteFocus) - .bind('blur', Drupal.fileFieldSources.pasteBlur); - }); - - if (context === document) { - $('form').submit(function() { - Drupal.fileFieldSources.removeHintText(); - }); - } -}; - -/** - * Helper functions used by FileField Sources. - */ -Drupal.fileFieldSources = { - /** - * Update the hint text when clicking between source types. - */ - updateHintText: function(fileFieldElement) { - // Add default value hint text to text fields. - $(fileFieldElement).find('div.filefield-source').each(function() { - var matches = this.className.match(/filefield-source-([a-z]+)/); - var sourceType = matches[1]; - var defaultText = ''; - var textfield = $(this).find('input.form-text:first').get(0); - var defaultText = (Drupal.settings.fileFieldSources && Drupal.settings.fileFieldSources[sourceType]) ? Drupal.settings.fileFieldSources[sourceType].hintText : ''; - - // If the field doesn't exist, just return. - if (!textfield) { - return; - } - - // If this field is not shown, remove its value and be done. - if (!$(this).is(':visible') && textfield.value == defaultText) { - textfield.value = ''; - return; - } - - // Set a default value: - if (textfield.value == '') { - textfield.value = defaultText; - } - - // Set a default class. - if (textfield.value == defaultText) { - $(textfield).addClass('hint'); - } - - $(textfield).focus(hideHintText); - $(textfield).blur(showHintText); - - function showHintText() { - if (this.value == '') { - this.value = defaultText; - $(this).addClass('hint'); - } - } - - function hideHintText() { - if (this.value == defaultText) { - this.value = ''; - $(this).removeClass('hint'); - } - } - }); - }, - - /** - * Delete all hint text from a form before submit. - */ - removeHintText: function() { - $('div.filefield-source input.hint').val('').removeClass('hint'); - }, - - /** - * Clean up the default value on focus. - */ - pasteFocus: function(e) { - // Set default text. - if (!this.defaultText) { - this.defaultText = this.innerHTML; - this.innerHTML = ''; - } - // Remove non-text nodes. - $(this).children().remove(); - }, - - /** - * Restore default value on blur. - */ - pasteBlur: function(e) { - if (this.defaultText && !this.innerHTML) { - this.innerHTML = this.defaultText; - } - }, - - pasteEvent: function(e) { - var clipboardData = null; - var targetElement = this; - - // Chrome. - if (window.event && window.event.clipboardData && window.event.clipboardData.items) { - clipboardData = window.event.clipboardData; - } - // All browsers in the future (hopefully). - else if (e.originalEvent && e.originalEvent.clipboardData && e.originalEvent.clipboardData.items) { - clipboardData = e.originalEvent.clipboardData; - } - // Firefox with content editable pastes as img tag with data href. - else if ($.browser.mozilla) { - Drupal.fileFieldSources.waitForPaste(targetElement); - return true; - } - else { - Drupal.fileFieldSources.pasteError(targetElement, Drupal.t('Paste from clipboard not supported in this browser.')); - return false; - } - - var items = clipboardData.items; - var types = clipboardData.types; - var filename = targetElement.firstChild ? targetElement.firstChild.textContent : ''; - - // Handle files and image content directly in the clipboard. - var fileFound = false; - for (var n = 0; n < items.length; n++) { - if (items[n] && items[n].kind === 'file') { - var fileBlob = items[n].getAsFile(); - var fileReader = new FileReader(); - // Define events to fire after the file is read into memory. - fileReader.onload = function() { - Drupal.fileFieldSources.pasteSubmit(targetElement, filename, this.result); - }; - fileReader.onerror = function() { - Drupal.fileFieldSources.pasteError(targetElement, Drupal.t('Error reading file from clipboard.')); - }; - // Read in the file to fire the above events. - fileReader.readAsDataURL(fileBlob); - fileFound = true; - break; - } - // Handle files that a copy/pasted as a file reference. - //if (types[n] && types[n] === 'Files') { - // TODO: Figure out how to capture copy/paste of entire files from desktop. - //} - } - if (!fileFound) { - Drupal.fileFieldSources.pasteError(targetElement, Drupal.t('No file in clipboard.')); - } - return false; - }, - - /** - * For browsers that don't support native clipboardData attributes. - */ - waitForPaste: function(targetElement) { - if (targetElement.children && targetElement.children.length > 0) { - var filename = targetElement.firstChild ? targetElement.firstChild.textContent : ''; - var tagFound = false; - $(targetElement).find('img[src^="data:image"]').each(function(n, element) { - Drupal.fileFieldSources.pasteSubmit(targetElement, filename, element.src); - tagFound = true; - }); - $(targetElement).html(filename); - if (!tagFound) { - Drupal.fileFieldSources.pasteError(targetElement, Drupal.t('No file in clipboard.')); - } - } - else { - setTimeout(function() { - Drupal.fileFieldSources.waitForPaste(targetElement); - }, 200); - } - }, - - /** - * Set an error on the paste field temporarily then clear it. - */ - pasteError: function(domElement, errorMessage) { - var $description = $(domElement).parents('.filefield-source-clipboard:first').find('.description'); - if (!$description.data('originalDescription')) { - $description.data('originalDescription', $description.html()) - } - $description.html(errorMessage); - var errorTimeout = setTimeout(function() { - $description.html($description.data('originalDescription')); - $(this).unbind('click.pasteError'); - }, 3000); - $(domElement).bind('click.pasteError', function() { - clearTimeout(errorTimeout); - $description.html($description.data('originalDescription')); - $(this).unbind('click.pasteError'); - }); - }, - - /** - * After retreiving a clipboard, post the results to the server. - */ - pasteSubmit: function(targetElement, filename, contents) { - var $wrapper = $(targetElement).parents('.filefield-source-clipboard'); - $wrapper.find('.filefield-source-clipboard-filename').val(filename); - $wrapper.find('.filefield-source-clipboard-contents').val(contents); - $wrapper.find('input.form-submit').trigger('mousedown'); - } -}; - -})(jQuery); \ No newline at end of file diff --git a/filefield_sources.libraries.yml b/filefield_sources.libraries.yml new file mode 100644 index 0000000..7a0c7eb --- /dev/null +++ b/filefield_sources.libraries.yml @@ -0,0 +1,9 @@ +drupal.filefield_sources: + version: VERSION + js: + js/filefield_sources.js: {} + css: + theme: + css/filefield_sources.css: {} + dependencies: + - file/drupal.file diff --git a/filefield_sources.module b/filefield_sources.module index 94dcb97..ad138ad 100644 --- a/filefield_sources.module +++ b/filefield_sources.module @@ -5,101 +5,129 @@ * Extend FileField to allow files from multiple sources. */ -/** - * Implements hook_menu(). - */ -function filefield_sources_menu() { - $params = array(); - return filefield_sources_invoke_all('menu', $params); -} +use Drupal\Core\Form\FormStateInterface; +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Field\WidgetBase; +use Drupal\Core\Field\WidgetInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Render\Element; -/** - * Implements hook_element_info(). - */ -function filefield_sources_element_info() { - $elements = array(); +const FILEFIELD_SOURCE_ATTACH_DEFAULT_PATH = 'file_attach'; +const FILEFIELD_SOURCE_ATTACH_RELATIVE = 0; +const FILEFIELD_SOURCE_ATTACH_ABSOLUTE = 1; +const FILEFIELD_SOURCE_ATTACH_MODE_MOVE = 'move'; +const FILEFIELD_SOURCE_ATTACH_MODE_COPY = 'copy'; + +const FILEFIELD_SOURCE_REFERENCE_HINT_TEXT = 'example.png [fid:123]'; +const FILEFIELD_SOURCE_REMOTE_HINT_TEXT = 'http://example.com/files/file.png'; - $elements['managed_file']['#process'] = array('filefield_sources_field_process'); - $elements['managed_file']['#pre_render'] = array('filefield_sources_field_pre_render'); - $elements['managed_file']['#element_validate'] = array('filefield_sources_field_validate'); - $elements['managed_file']['#file_value_callbacks'] = array('filefield_sources_field_value'); +const FILEFIELD_SOURCE_REFERENCE_STARTS_WITH_AUTOCOMPLETE_TYPE = 'STARTS_WITH'; +const FILEFIELD_SOURCE_REFERENCE_CONTAINS_AUTOCOMPLETE_TYPE = 'CONTAINS'; - return $elements; +/** + * Implements hook_element_info_alter(). + */ +function filefield_sources_element_info_alter(&$type) { + if (isset($type['managed_file'])) { + $type['managed_file']['#process'][] = 'filefield_sources_field_process'; + $type['managed_file']['#pre_render'][] = 'filefield_sources_field_pre_render'; + $type['managed_file']['#element_validate'][] = 'filefield_sources_field_validate'; + $type['managed_file']['#file_value_callbacks'][] = 'filefield_sources_field_value'; + } } /** * Implements hook_theme(). */ function filefield_sources_theme() { - $params = array(); - $theme = filefield_sources_invoke_all('theme', $params); + $theme = array(); + + $theme['filefield_sources_element'] = array( + 'render element' => 'element', + 'function' => 'theme_filefield_sources_element', + ); $theme['filefield_sources_list'] = array( - 'arguments' => array('sources' => NULL), + 'variables' => array('element' => NULL, 'sources' => NULL), + 'function' => 'theme_filefield_sources_list', ); return $theme; } /** - * Implements hook_filefield_sources_widgets(). + * Implements hook_field_widget_third_party_settings_form(). * - * This returns a list of widgets that are compatible with FileField Sources. + * Add file field sources settings form to supported field widget forms. + * + * @see \Drupal\field_ui\FormDisplayOverview */ -function filefield_sources_filefield_sources_widgets() { - return array('file_generic', 'image_image'); +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_form_FORM_ID_alter(). + * 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_form_field_ui_field_edit_form_alter(&$form, &$form_state) { - $instance = $form['#instance']; - if (in_array($instance['widget']['type'], module_invoke_all('filefield_sources_widgets'))) { - if (!empty($form['instance']['widget']['settings'])) { - $form['instance']['widget']['settings'] += filefield_sources_form($instance, $form_state); - } - else { - $form['instance']['widget']['settings'] = filefield_sources_form($instance, $form_state); - } +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)); } } /** - * A list of settings needed by FileField Sources module on widgets. + * Implements hook_field_widget_form_alter(). + * + * Add file field sources widget's settings to element. */ -function filefield_sources_field_widget_info_alter(&$info) { - $settings = array( - 'filefield_sources' => array(), - ); - foreach (module_invoke_all('filefield_sources_widgets') as $widget) { - $params = array('save', $widget); - $widget_settings = array_merge($settings, filefield_sources_invoke_all('settings', $params)); - if (isset($info[$widget])) { - $info[$widget]['settings']['filefield_sources'] = $widget_settings; - } +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($instance, &$form_state) { - $settings = $instance['widget']['settings']['filefield_sources']; +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' => 'fieldset', + '#type' => 'details', '#title' => t('File sources'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, '#weight' => 20, ); $sources = filefield_sources_list(); - $form['filefield_sources']['filefield_sources'] = array( + $form['filefield_sources']['sources'] = array( '#type' => 'checkboxes', '#title' => t('Enabled sources'), '#options' => $sources, @@ -107,15 +135,7 @@ function filefield_sources_form($instance, &$form_state) { '#description' => t('Select the available locations from which this widget may select files.'), ); - // Make sure all includes are loaded for multistep forms. - $sources_info = filefield_sources_info(FALSE); - foreach ($sources_info as $source_name => $source) { - if (isset($source['file'])) { - _filefield_sources_form_include($source['module'], $source['file'], $form_state); - } - } - - $params = array('form', $instance); + $params = array($plugin); $form['filefield_sources'] = array_merge($form['filefield_sources'], filefield_sources_invoke_all('settings', $params)); return $form; @@ -128,28 +148,32 @@ function filefield_sources_form($instance, &$form_state) { * 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, &$form_state, $form) { +function filefield_sources_field_process(&$element, FormStateInterface $form_state, &$complete_form) { static $js_added; - // If not a recognized field instance, do not process. - if (!isset($element['#field_name']) || !($instance = field_widget_instance($element, $form_state)) || !isset($instance['widget']['settings']['filefield_sources']['filefield_sources'])) { + // Check if we are processing file field sources. + if (!isset($element['#filefield_sources_settings'])) { return $element; } + $settings = $element['#filefield_sources_settings']; + $enabled_sources = _filefield_sources_enabled($settings); + // Do all processing as needed by each source. $sources = filefield_sources_info(); - $enabled_sources = _filefield_sources_enabled($instance['widget']['settings']['filefield_sources']); foreach ($sources as $source_name => $source) { if (empty($enabled_sources[$source_name])) { unset($sources[$source_name]); } - else { - if (isset($source['process'])) { - $function = $source['process']; - $element = $function($element, $form_state, $form); - } - if (isset($source['file'])) { - _filefield_sources_form_include($source['module'], $source['file'], $form_state); + // 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, + )); } } } @@ -169,46 +193,60 @@ function filefield_sources_field_process($element, &$form_state, $form) { } } - // Add basic JS and CSS. - $path = drupal_get_path('module', 'filefield_sources'); - $element['#attached']['css'][] = $path . '/filefield_sources.css'; - $element['#attached']['js'][] = $path . '/filefield_sources.js'; + // 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) { + foreach (Element::children($element) as $key) { if (isset($element[$key]['#filefield_sources_hint_text']) && !isset($js_added[$key])) { $type = str_replace('filefield_', '', $key); - drupal_add_js(array('fileFieldSources' => array($type => array('hintText' => $element[$key]['#filefield_sources_hint_text']))), 'setting'); + + $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 + // Adjust the Ajax settings so that on upload and remove of any individual // file, the entire group of file fields is updated together. - // Copied directly from file_field_widget_process(). - $field = field_widget_field($element, $form_state); - if ($field['cardinality'] != 1) { + // Duplicate of Drupal\file\Plugin\Field\FieldWidget\FileWidget::process(). + if ($element['#cardinality'] != 1) { $parents = array_slice($element['#array_parents'], 0, -1); - $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value']; - $field_element = drupal_array_get_nested_value($form, $parents); + $new_path = 'file/ajax'; + $new_options = array( + 'query' => array( + 'element_parents' => implode('/', $parents), + 'form_build_id' => $complete_form['form_build_id']['#value'], + ), + ); + $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) { + foreach (Element::children($element) as $key) { + foreach (Element::children($element[$key]) as $subkey) { if (isset($element[$key][$subkey]['#ajax'])) { $element[$key][$subkey]['#ajax']['path'] = $new_path; + $element[$key][$subkey]['#ajax']['options'] = $new_options; $element[$key][$subkey]['#ajax']['wrapper'] = $new_wrapper; - $element[$key][$subkey]['#limit_validation_errors'] = array($parents); + $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['fid']['#value'])) { + if (empty($element['fids']['#value'])) { if (count($enabled_sources) > 1) { $element['filefield_sources_list'] = array( - '#type' => 'markup', - '#markup' => theme('filefield_sources_list', array('element' => $element, 'sources' => $sources)), + '#theme' => 'filefield_sources_list', + '#element' => $element, + '#sources' => $sources, '#weight' => -20, ); } @@ -222,8 +260,8 @@ function filefield_sources_field_process($element, &$form_state, $form) { */ 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']['fid'])) { - foreach (element_children($element) as $key) { + if (!empty($element['#value']['fids'])) { + foreach (Element::children($element) as $key) { if (!empty($element[$key]['#filefield_source'])) { $element[$key]['#access'] = FALSE; } @@ -235,69 +273,101 @@ function filefield_sources_field_pre_render($element) { /** * An #element_validate function to run source validations. */ -function filefield_sources_field_validate($element, &$form_state, $form) { +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['validate'])) { - $function = $source['validate']; - $function($element, $form_state, $form); + 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, + )); } } } /** * A #submit handler added to all FileField Source buttons. + * + * Duplicate 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, &$form_state) { - - $parents = array_slice($form_state['triggering_element']['#array_parents'], 0, -3); - $element = drupal_array_get_nested_value($form, $parents); +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']; - $langcode = $element['#language']; + $parents = $element['#field_parents']; - // Get exisitng file values. - // File Field items are stored in the field state after ajax reloads starting - // from Drupal 7.8. We try to support all releases by merging the items. - $field_state = field_form_get_state($element['#field_parents'], $field_name, $langcode, $form_state); - $field_values = drupal_array_get_nested_value($form_state['values'], $parents); - - if (isset($field_values) && isset($field_state['items'])) { - $field_values += $field_state['items']; + $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]); + } } - elseif (isset($field_state['items'])) { - $field_values = $field_state['items']; + + // 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; + } } - if (isset($field_values)) { - // Update sort order according to weight. Note that this is always stored in - // form state. Sort does not work using regular upload, but that is a core - // bug. - usort($field_values, '_field_sort_items_helper'); + // Re-index deltas after removing empty items. + $submitted_values = array_values($new_values); - // Update form_state values. - drupal_array_set_nested_value($form_state['values'], $parents, $field_values); + // Update form_state values. + NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -3), $submitted_values); - // Update items. - $field_state['items'] = $field_values; - field_form_set_state($element['#field_parents'], $field_name, $langcode, $form_state, $field_state); - } + // 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); - // Clear out input as it will need to be rebuildt. - drupal_array_set_nested_value($form_state['input'], $element['#parents'], NULL); - $form_state['rebuild'] = TRUE; + // 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, &$item, &$form_state) { +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['value'])) { - $function = $source['value']; - $function($element, $item); + if (isset($source['class'])) { + $callback = array($source['class'], 'value'); + if (is_callable($callback)) { + call_user_func_array($callback, array(&$element, &$input, $form_state)); + } } } } @@ -307,10 +377,14 @@ function filefield_sources_field_value($element, &$item, &$form_state) { */ function filefield_sources_invoke_all($method, &$params) { $return = array(); - foreach (filefield_sources_includes() as $source) { - $function = 'filefield_source_' . $source . '_' . $method; - if (function_exists($function)) { - $result = call_user_func_array($function, $params); + 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); } @@ -326,35 +400,17 @@ function filefield_sources_invoke_all($method, &$params) { * Load hook_filefield_sources_info() data from all modules. */ function filefield_sources_info($include_default = TRUE) { - // Cache the expensive part. - $cache = &drupal_static(__FUNCTION__, array()); - if (empty($cache)) { - $cache['upload'] = array( + $info = \Drupal::service('filefield_sources')->getDefinitions(); + if ($include_default) { + $info['upload'] = array( 'name' => t('Upload (default)'), 'label' => t('Upload'), 'description' => t('Upload a file from your computer.'), 'weight' => -10, ); - - // Add the providing module name to each source. - foreach (module_implements('filefield_sources_info') as $module) { - $function = $module . '_filefield_sources_info'; - $additions = $function(); - foreach ($additions as $source_name => $source_info) { - $additions[$source_name]['module'] = $module; - } - $cache += $additions; - } - - drupal_alter('filefield_sources_info', $cache); - uasort($cache, '_filefield_sources_sort'); } - // Remove the upload option from the returned value if needed. - $info = $cache; - if (!$include_default) { - unset($info['upload']); - } + uasort($info, '_filefield_sources_sort'); return $info; } @@ -374,81 +430,25 @@ function filefield_sources_list($include_default = TRUE) { } /** - * Implements hook_filefield_sources_info(). - */ -function filefield_sources_filefield_sources_info() { - $params = array(); - return filefield_sources_invoke_all('info', $params); -} - -/** - * Load all the potential sources. - */ -function filefield_sources_includes($include = TRUE, $enabled_only = TRUE) { - if ($enabled_only) { - $enabled_includes = variable_get('filefield_sources', filefield_sources_includes(FALSE, FALSE)); - } - - $includes = array(); - $directory = drupal_get_path('module', 'filefield_sources') . '/sources'; - foreach (file_scan_directory($directory, '/\.inc$/') as $file) { - if (!$enabled_only || in_array($file->name, $enabled_includes)) { - $includes[] = $file->name; - if ($include) { - include_once(DRUPAL_ROOT . '/' . $file->uri); - } - } - } - return $includes; -} - -/** - * Check the current user's access to a file through hook_file_download(). - * - * @param $uri - * A file URI as loaded from the database. - * @return - * Boolean TRUE if the user has access, FALSE otherwise. - * - * @see file_download() - * @see hook_file_download(). - */ -function filefield_sources_file_access($uri) { - $headers = array(); - foreach (module_implements('file_download') as $module) { - $function = $module . '_file_download'; - $result = $function($uri); - if ($result == -1) { - // Throw away the headers received so far. - $headers = array(); - break; - } - if (isset($result) && is_array($result)) { - $headers = array_merge($headers, $result); - } - } - return !empty($headers); -} - -/** * 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(). + * @see file_save_upload() */ function filefield_sources_save_file($filepath, $validators = array(), $destination = FALSE, $replace = FILE_EXISTS_RENAME) { - global $user; + $user = \Drupal::currentUser(); // Begin building file object. - $file = new stdClass(); - $file->uid = $user->uid; - $file->status = 0; - $file->filename = trim(basename($filepath), '.'); - $file->uri = $filepath; - $file->filemime = file_get_mimetype($file->filename); - $file->filesize = filesize($filepath); + $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'])) { @@ -474,22 +474,22 @@ function filefield_sources_save_file($filepath, $validators = array(), $destinat if (!empty($extensions)) { // Munge the filename to protect against possible malicious extension hiding // within an unknown file type (ie: filename.html.foo). - $file->filename = file_munge_filename($file->filename, $extensions); + $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 (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { - $file->filemime = 'text/plain'; - $file->uri .= '.txt'; - $file->filename .= '.txt'; + 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->filename))); + drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $file->getFilename()))); } } @@ -513,11 +513,11 @@ function filefield_sources_save_file($filepath, $validators = array(), $destinat // Ensure the destination is writable. file_prepare_directory($destination, FILE_CREATE_DIRECTORY); - $file->destination = file_destination($destination . $file->filename, $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->filename, '%directory' => $destination)), 'error'); + 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; } @@ -529,7 +529,7 @@ function filefield_sources_save_file($filepath, $validators = array(), $destinat // Check for errors. if (!empty($errors)) { - $message = t('The specified file %name could not be uploaded.', array('%name' => $file->filename)); + $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)); } @@ -543,44 +543,44 @@ function filefield_sources_save_file($filepath, $validators = array(), $destinat // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary // directory. This overcomes open_basedir restrictions for future file // operations. - $file->uri = $file->destination; - if (!file_unmanaged_copy($filepath, $file->uri, $replace)) { + $file->setFileUri($file->destination); + if (!file_unmanaged_copy($filepath, $file->getFileUri(), $replace)) { drupal_set_message(t('File upload error. Could not move uploaded file.'), 'error'); - watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri)); + \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->uri); + 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->uri)); + $existing_files = file_load_multiple(array(), array('uri' => $file->getFileUri())); if (count($existing_files)) { $existing = reset($existing_files); - $file->fid = $existing->fid; + $file->setOriginalId($existing->id()); } } // If we made it this far it's safe to record this file in the database. - return file_save($file); + $file->save(); + return $file; } /** * Clean up the file name, munging extensions and transliterating. * - * @param $filepath + * @param string $filepath * A string containing a file name or full path. Only the file name will * actually be modified. - * @return + * + * @return string * A file path with a cleaned-up file name. */ function filefield_sources_clean_filename($filepath, $extensions) { - global $user; - $filename = basename($filepath); - if (module_exists('transliteration')) { + if (\Drupal::moduleHandler()->moduleExists('transliteration')) { module_load_include('inc', 'transliteration'); $langcode = NULL; @@ -603,6 +603,27 @@ function filefield_sources_clean_filename($filepath, $extensions) { } /** + * 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) { @@ -620,13 +641,13 @@ function theme_filefield_sources_list($variables) { /** * Validate a file based on the $element['#upload_validators'] property. */ -function filefield_sources_element_validate($element, $file) { +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->uri)) { + if (!file_exists($file->getFileUri())) { $errors[] = t('The file does not exist.'); } // Call the validation functions. @@ -649,7 +670,7 @@ function filefield_sources_element_validate($element, $file) { else { $message .= ' ' . array_pop($errors); } - form_error($element, $message); + $form_state->setError($element, $message); return 0; } @@ -671,44 +692,54 @@ function filefield_sources_element_validation_help($validators) { } /** - * Menu access callback; Checks user access to edit a file field. - */ -function _filefield_sources_field_access($entity_type, $bundle_name, $field_name) { - $field = field_info_field($field_name); - return field_access('edit', $field, $entity_type); -} - -/** * 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' => ''); + $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'])); } /** - * Ensure that a source include file is loaded into $form_state. - */ -function _filefield_sources_form_include($module, $filepath, &$form_state) { - $last_dot = strrpos($filepath, '.'); - $path = substr($filepath, 0, $last_dot); - $extension = substr($filepath, $last_dot + 1); - form_load_include($form_state, $extension, $module, $path); -} - -/** - * Helper to return enabled sources for a field + * 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['filefield_sources']['upload'])) { - $settings['filefield_sources']['upload'] = 'upload'; + if (!isset($settings['sources']['upload'])) { + $settings['sources']['upload'] = 'upload'; } - $enabled = array_keys(array_filter($settings['filefield_sources'])); - return drupal_map_assoc($enabled); + $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))); + } } diff --git a/filefield_sources.routing.yml b/filefield_sources.routing.yml new file mode 100644 index 0000000..002d37c --- /dev/null +++ b/filefield_sources.routing.yml @@ -0,0 +1,2 @@ +route_callbacks: + - '\Drupal\filefield_sources\Routing\FilefieldSourcesRoutes::routes' diff --git a/filefield_sources.services.yml b/filefield_sources.services.yml new file mode 100644 index 0000000..289a9fc --- /dev/null +++ b/filefield_sources.services.yml @@ -0,0 +1,10 @@ +services: + plugin.manager.filefield_sources: + class: Drupal\filefield_sources\FilefieldSourceManager + parent: default_plugin_manager + filefield_sources: + alias: plugin.manager.filefield_sources + access_check.filefield_sources.field: + class: Drupal\filefield_sources\Access\FieldAccessCheck + tags: + - { name: access_check, applies_to: _access_filefield_sources_field } diff --git a/js/filefield_sources.js b/js/filefield_sources.js new file mode 100644 index 0000000..8d367b1 --- /dev/null +++ b/js/filefield_sources.js @@ -0,0 +1,250 @@ +/** + * @file + * Defines Javascript behaviors for the filefield_sources module. + */ + +(function ($, Drupal) { + +"use strict"; + +// Behavior to add source options to configured fields. +Drupal.behaviors.fileFieldSources = {}; +Drupal.behaviors.fileFieldSources.attach = function(context, settings) { + $('div.filefield-sources-list:not(.filefield-sources-processed)', context).each(function() { + $(this).addClass('filefield-sources-processed'); + var $fileFieldElement = $(this).parents('div.form-managed-file:first'); + $(this).find('a').click(function() { + // Remove the active class. + $(this).parents('div.filefield-sources-list').find('a.active').removeClass('active'); + + // Find the unique FileField Source class name. + var fileFieldSourceClass = this.className.match(/filefield-source-[0-9a-z]+/i)[0]; + + // The default upload element is a special case. + if ($(this).is('.filefield-source-upload')) { + $fileFieldElement.find('div.filefield-sources-list').siblings('input.form-file, input.form-submit').css('display', ''); + $fileFieldElement.find('div.filefield-source').css('display', 'none'); + } + else { + $fileFieldElement.find('div.filefield-sources-list').siblings('input.form-file, input.form-submit').css('display', 'none'); + $fileFieldElement.find('div.filefield-source').not('div.' + fileFieldSourceClass).css('display', 'none'); + $fileFieldElement.find('div.' + fileFieldSourceClass).css('display', ''); + } + + // Add the active class. + $(this).addClass('active'); + Drupal.fileFieldSources.updateHintText($fileFieldElement.get(0)); + }).first().triggerHandler('click'); + + // Clipboard support. + $fileFieldElement.find('.filefield-source-clipboard-capture') + .bind('paste', Drupal.fileFieldSources.pasteEvent) + .bind('focus', Drupal.fileFieldSources.pasteFocus) + .bind('blur', Drupal.fileFieldSources.pasteBlur); + }); + + if (context === document) { + $('form').submit(function() { + Drupal.fileFieldSources.removeHintText(); + }); + } +}; + +// Helper functions used by FileField Sources. +Drupal.fileFieldSources = { + /** + * Update the hint text when clicking between source types. + */ + updateHintText: function(fileFieldElement) { + // Add default value hint text to text fields. + $(fileFieldElement).find('div.filefield-source').each(function() { + var matches = this.className.match(/filefield-source-([a-z]+)/); + var sourceType = matches[1]; + var textfield = $(this).find('input.form-text:first').get(0); + var defaultText = (drupalSettings.fileFieldSources && drupalSettings.fileFieldSources[sourceType]) ? drupalSettings.fileFieldSources[sourceType].hintText : ''; + + // If the field doesn't exist, just return. + if (!textfield) { + return; + } + + // If this field is not shown, remove its value and be done. + if (!$(this).is(':visible') && textfield.value == defaultText) { + textfield.value = ''; + return; + } + + // Set a default value: + if (textfield.value == '') { + textfield.value = defaultText; + } + + // Set a default class. + if (textfield.value == defaultText) { + $(textfield).addClass('hint'); + } + + $(textfield).focus(hideHintText); + $(textfield).blur(showHintText); + + function showHintText() { + if (this.value == '') { + this.value = defaultText; + $(this).addClass('hint'); + } + } + + function hideHintText() { + if (this.value == defaultText) { + this.value = ''; + $(this).removeClass('hint'); + } + } + }); + }, + + /** + * Delete all hint text from a form before submit. + */ + removeHintText: function() { + $('div.filefield-source input.hint').val('').removeClass('hint'); + }, + + /** + * Clean up the default value on focus. + */ + pasteFocus: function(e) { + // Set default text. + if (!this.defaultText) { + this.defaultText = this.innerHTML; + this.innerHTML = ''; + } + // Remove non-text nodes. + $(this).children().remove(); + }, + + /** + * Restore default value on blur. + */ + pasteBlur: function(e) { + if (this.defaultText && !this.innerHTML) { + this.innerHTML = this.defaultText; + } + }, + + pasteEvent: function(e) { + var clipboardData = null; + var targetElement = this; + var userAgent = navigator.userAgent.toLowerCase(); + + // Chrome. + if (window.event && window.event.clipboardData && window.event.clipboardData.items) { + clipboardData = window.event.clipboardData; + } + // All browsers in the future (hopefully). + else if (e.originalEvent && e.originalEvent.clipboardData && e.originalEvent.clipboardData.items) { + clipboardData = e.originalEvent.clipboardData; + } + // Firefox with content editable pastes as img tag with data href. + else if (userAgent.match(/mozilla/) && !userAgent.match(/webkit/)) { + Drupal.fileFieldSources.waitForPaste(targetElement); + return true; + } + else { + Drupal.fileFieldSources.pasteError(targetElement, Drupal.t('Paste from clipboard not supported in this browser.')); + return false; + } + + var items = clipboardData.items; + var types = clipboardData.types; + var filename = targetElement.firstChild ? targetElement.firstChild.textContent : ''; + + // Handle files and image content directly in the clipboard. + var fileFound = false; + for (var n = 0; n < items.length; n++) { + if (items[n] && items[n].kind === 'file') { + var fileBlob = items[n].getAsFile(); + var fileReader = new FileReader(); + // Define events to fire after the file is read into memory. + fileReader.onload = function() { + Drupal.fileFieldSources.pasteSubmit(targetElement, filename, this.result); + }; + fileReader.onerror = function() { + Drupal.fileFieldSources.pasteError(targetElement, Drupal.t('Error reading file from clipboard.')); + }; + // Read in the file to fire the above events. + fileReader.readAsDataURL(fileBlob); + fileFound = true; + break; + } + // Handle files that a copy/pasted as a file reference. + /* if (types[n] && types[n] === 'Files') { + TODO: Figure out how to capture copy/paste of entire files from desktop. + }*/ + } + if (!fileFound) { + Drupal.fileFieldSources.pasteError(targetElement, Drupal.t('No file in clipboard.')); + } + return false; + }, + + /** + * For browsers that don't support native clipboardData attributes. + */ + waitForPaste: function(targetElement) { + if (targetElement.children && targetElement.children.length > 0) { + var filename = targetElement.firstChild ? targetElement.firstChild.textContent : ''; + var tagFound = false; + $(targetElement).find('img[src^="data:image"]').each(function(n, element) { + Drupal.fileFieldSources.pasteSubmit(targetElement, filename, element.src); + tagFound = true; + }); + $(targetElement).html(filename); + if (!tagFound) { + Drupal.fileFieldSources.pasteError(targetElement, Drupal.t('No file in clipboard.')); + } + } + else { + setTimeout(function() { + Drupal.fileFieldSources.waitForPaste(targetElement); + }, 200); + } + }, + + /** + * Set an error on the paste field temporarily then clear it. + */ + pasteError: function(domElement, errorMessage) { + var $description = $(domElement).parents('.filefield-source-clipboard:first').find('.description'); + if (!$description.data('originalDescription')) { + $description.data('originalDescription', $description.html()) + } + $description.html(errorMessage); + var errorTimeout = setTimeout(function() { + $description.html($description.data('originalDescription')); + $(this).unbind('click.pasteError'); + }, 3000); + $(domElement).bind('click.pasteError', function() { + clearTimeout(errorTimeout); + $description.html($description.data('originalDescription')); + $(this).unbind('click.pasteError'); + }); + }, + + /** + * After retreiving a clipboard, post the results to the server. + */ + pasteSubmit: function(targetElement, filename, contents) { + var $wrapper = $(targetElement).parents('.filefield-source-clipboard'); + $wrapper.find('.filefield-source-clipboard-filename').val(filename); + $wrapper.find('.filefield-source-clipboard-contents').val(contents); + $wrapper.find('input.form-submit').trigger('mousedown'); + } +}; + +// Override triggerUploadButton method from file.js. +Drupal.file.triggerUploadButton = function (event) { + $(event.target).closest('.form-managed-file').find('.form-submit.upload-button').trigger('mousedown'); +} + +})(jQuery, Drupal); diff --git a/sources/attach.inc b/sources/attach.inc deleted file mode 100644 index a60f18a..0000000 --- a/sources/attach.inc +++ /dev/null @@ -1,303 +0,0 @@ - t('File attach from server directory'), - 'label' => t('File attach'), - 'description' => t('Select a file from a directory on the server.'), - 'process' => 'filefield_source_attach_process', - 'value' => 'filefield_source_attach_value', - 'weight' => 3, - 'file' => 'includes/attach.inc', - ); - return $source; -} - -/** - * Implements hook_theme(). - */ -function filefield_source_attach_theme() { - return array( - 'filefield_source_attach_element' => array( - 'render element' => 'element', - 'file' => 'sources/attach.inc', - ), - ); -} - -/** - * Implements hook_filefield_source_settings(). - */ -function filefield_source_attach_settings($op, $instance) { - $return = array(); - - if ($op == 'form') { - $settings = $instance['widget']['settings']['filefield_sources']; - - $return['source_attach'] = array( - '#title' => t('File attach settings'), - '#type' => 'fieldset', - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#description' => t('File attach allows for selecting a file from a directory on the server, commonly used in combination with FTP.') . ' ' . t('This file source will ignore file size checking when used.') . '', - '#element_validate' => array('_filefield_source_attach_file_path_validate'), - '#weight' => 3, - ); - $return['source_attach']['path'] = array( - '#type' => 'textfield', - '#title' => t('File attach path'), - '#default_value' => $settings['source_attach']['path'], - '#size' => 60, - '#maxlength' => 128, - '#description' => t('The directory within the File attach location that will contain attachable files.'), - ); - if (module_exists('token')) { - $return['source_attach']['tokens'] = array( - '#theme' => 'token_tree', - '#token_types' => array('user'), - ); - } - $return['source_attach']['absolute'] = array( - '#type' => 'radios', - '#title' => t('File attach location'), - '#options' => array( - 0 => t('Within the files directory'), - 1 => t('Absolute server path'), - ), - '#default_value' => $settings['source_attach']['absolute'], - '#description' => t('The File attach path may be with the files directory (%file_directory) or from the root of your server. If an absolute path is used and it does not start with a "/" your path will be relative to your site directory: %realpath.', array('%file_directory' => drupal_realpath(file_default_scheme() . '://'), '%realpath' => realpath('./'))), - ); - $return['source_attach']['attach_mode'] = array( - '#type' => 'radios', - '#title' => t('Attach method'), - '#options' => array( - 'move' => t('Move the file directly to the final location'), - 'copy' => t('Leave a copy of the file in the attach directory'), - ), - '#default_value' => isset($settings['source_attach']['attach_mode']) ? $settings['source_attach']['attach_mode'] : 'move', - ); - } - elseif ($op == 'save') { - $return['source_attach']['path'] = 'file_attach'; - $return['source_attach']['absolute'] = 0; - $return['source_attach']['attach_mode'] = 'move'; - } - - return $return; -} - -function _filefield_source_attach_file_path_validate($element, &$form_state) { - // Only validate if this source is enabled. - if (!$form_state['values']['instance']['widget']['settings']['filefield_sources']['filefield_sources']['attach']) { - return; - } - - // Strip slashes from the end of the file path. - $filepath = rtrim($element['path']['#value'], '\\/'); - form_set_value($element['path'], $filepath, $form_state); - $filepath = _filefield_source_attach_directory($form_state['values']['instance']); - - // Check that the directory exists and is writable. - if (!file_prepare_directory($filepath, FILE_CREATE_DIRECTORY)) { - form_error($element['path'], t('Specified file attach path must exist or be writable.')); - } -} - -/** - * A #process callback to extend the filefield_widget element type. - */ -function filefield_source_attach_process($element, &$form_state, $form) { - $instance = field_widget_instance($element, $form_state); - $settings = $instance['widget']['settings']['filefield_sources']['source_attach']; - - $element['filefield_attach'] = array( - '#weight' => 100.5, - '#theme' => 'filefield_source_attach_element', - '#filefield_source' => TRUE, // Required for proper theming. - ); - - $path = _filefield_source_attach_directory($instance); - $options = _filefield_source_attach_options($path); - - // If we have built this element before, append the list of options that we - // had previously. This allows files to be deleted after copying them and - // still be considered a valid option during the validation and submit. - if (!isset($form_state['triggering_element']) && isset($form_state['filefield_sources'][$instance['field_name']]['attach_options'])) { - $options = $options + $form_state['filefield_sources'][$instance['field_name']]['attach_options']; - } - // On initial form build and rebuilds after processing input, save the - // original list of options so they can be restored in the line above. - else { - $form_state['filefield_sources'][$instance['field_name']]['attach_options'] = $options; - } - - $description = t('This method may be used to attach files that exceed the file size limit. Files may be attached from the %directory directory on the server, usually uploaded through FTP.', array('%directory' => realpath($path))); - - // Error messages. - if ($options === FALSE || empty($settings['path'])) { - $attach_message = t('A file attach directory could not be located.'); - $attach_description = t('Please check your settings for the %field field.', array('%field' => $instance['label'])); - } - elseif (!count($options)) { - $attach_message = t('There currently are no files to attach.'); - $attach_description = $description; - } - - if (isset($attach_message)) { - $element['filefield_attach']['attach_message'] = array( - '#markup' => $attach_message, - ); - $element['filefield_attach']['#description'] = $attach_description; - } - else { - $validators = $element['#upload_validators']; - if (isset($validators['file_validate_size'])) { - unset($validators['file_validate_size']); - } - $description .= '
' . filefield_sources_element_validation_help($validators); - $element['filefield_attach']['filename'] = array( - '#type' => 'select', - '#options' => $options, - ); - $element['filefield_attach']['#description'] = $description; - } - - $element['filefield_attach']['attach'] = array( - '#name' => implode('_', $element['#array_parents']) . '_attach', - '#type' => 'submit', - '#value' => t('Attach'), - '#validate' => array(), - '#submit' => array('filefield_sources_field_submit'), - '#limit_validation_errors' => array($element['#parents']), - '#ajax' => array( - 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], - 'wrapper' => $element['#id'] . '-ajax-wrapper', - 'method' => 'replace', - 'effect' => 'fade', - ), - ); - - return $element; -} - -function _filefield_source_attach_options($path) { - if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY)) { - drupal_set_message(t('Specified file attach path must exist or be writable.'), 'error'); - return FALSE; - } - - $options = array(); - $file_attach = file_scan_directory($path, '/.*/', array('key' => 'filename'), 0); - - if (count($file_attach)) { - $options = array('' => t('-- Select file --')); - foreach ($file_attach as $filename => $fileinfo) { - $filename = basename($filename); - $options[$fileinfo->uri] = str_replace($path . '/', '', $fileinfo->uri); - } - } - - natcasesort($options); - return $options; -} - -/** - * A #filefield_value_callback function. - */ -function filefield_source_attach_value($element, &$item) { - if (!empty($item['filefield_attach']['filename'])) { - $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']); - $filepath = $item['filefield_attach']['filename']; - - // Check that the destination is writable. - $directory = $element['#upload_location']; - $mode = variable_get('file_chmod_directory', 0775); - - // This first chmod check is for other systems such as S3, which don't work - // with file_prepare_directory(). - if (!drupal_chmod($directory, $mode) && !file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) { - watchdog('file', 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $filepath, '%destination' => drupal_realpath($directory))); - drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $filepath)), 'error'); - return; - } - - // Clean up the file name extensions and transliterate. - $original_filepath = $filepath; - $new_filepath = filefield_sources_clean_filename($filepath, $instance['settings']['file_extensions']); - rename($filepath, $new_filepath); - $filepath = $new_filepath; - - // Run all the normal validations, minus file size restrictions. - $validators = $element['#upload_validators']; - if (isset($validators['file_validate_size'])) { - unset($validators['file_validate_size']); - } - - // Save the file to the new location. - if ($file = filefield_sources_save_file($filepath, $validators, $directory)) { - $item = array_merge($item, (array) $file); - - // Delete the original file if "moving" the file instead of copying. - if ($instance['widget']['settings']['filefield_sources']['source_attach']['attach_mode'] !== 'copy') { - @unlink($filepath); - } - } - - // Restore the original file name if the file still exists. - if (file_exists($filepath) && $filepath != $original_filepath) { - rename($filepath, $original_filepath); - } - - $item['filefield_attach']['filename'] = ''; - } -} - -/** - * Theme the output of the autocomplete field. - */ -function theme_filefield_source_attach_element($variables) { - $element = $variables['element']; - - if (isset($element['attach_message'])) { - $output = $element['attach_message']['#markup']; - } - else { - $select = ''; - $size = !empty($element['filename']['#size']) ? ' size="' . $element['filename']['#size'] . '"' : ''; - _form_set_class($element['filename'], array('form-select')); - $multiple = !empty($element['#multiple']); - $output = ''; - } - $output .= drupal_render($element['attach']); - $element['#children'] = $output; - return '
' . theme('form_element', array('element' => $element)) . '
'; -} - -function _filefield_source_attach_directory($instance, $account = NULL) { - $field = field_info_field($instance['field_name']); - $account = isset($account) ? $account : $GLOBALS['user']; - $path = $instance['widget']['settings']['filefield_sources']['source_attach']['path']; - $absolute = !empty($instance['widget']['settings']['filefield_sources']['source_attach']['absolute']); - - // Replace user level tokens. - // Node level tokens require a lot of complexity like temporary storage - // locations when values don't exist. See the filefield_paths module. - if (module_exists('token')) { - $path = token_replace($path, array('user' => $account)); - } - - return $absolute ? $path : file_default_scheme() . '://' . $path; -} diff --git a/sources/clipboard.inc b/sources/clipboard.inc deleted file mode 100644 index f6528fc..0000000 --- a/sources/clipboard.inc +++ /dev/null @@ -1,187 +0,0 @@ - t('Paste from clipboard (limited browser support)'), - 'label' => t('Clipboard'), - 'description' => t('Allow users to paste a file directly from the clipboard.'), - 'process' => 'filefield_source_clipboard_process', - 'value' => 'filefield_source_clipboard_value', - 'weight' => 1, - 'file' => 'includes/clipboard.inc', - ); - return $source; -} - - -/** - * Implements hook_menu(). - */ -function filefield_source_clipboard_menu() { - $items = array(); - $items['file/clipboard/%/%/%'] = array( - 'page callback' => 'filefield_source_clipboard_page', - 'page arguments' => array(2, 3, 4), - 'access callback' => '_filefield_sources_field_access', - 'access arguments' => array(2, 3, 4), - 'file' => 'sources/clipboard.inc', - 'type' => MENU_CALLBACK, - ); - return $items; -} - -/** - * Implements hook_theme(). - */ -function filefield_source_clipboard_theme() { - return array( - 'filefield_source_clipboard_element' => array( - 'render element' => 'element', - 'file' => 'sources/clipboard.inc', - ), - ); -} - -/** - * A #process callback to extend the filefield_widget element type. - */ -function filefield_source_clipboard_process($element, &$form_state, $form) { - // If settings are needed later: - //$instance = field_widget_instance($element, $form_state); - //$settings = $instance['widget']['settings']['filefield_sources']['source_clipboard']; - - $element['filefield_clipboard'] = array( - '#weight' => 100.5, - '#theme' => 'filefield_source_clipboard_element', - '#filefield_source' => TRUE, // Required for proper theming. - '#filefield_sources_hint_text' => t('Enter filename then paste.'), - '#description' => filefield_sources_element_validation_help($element['#upload_validators']), - ); - - $element['filefield_clipboard']['filename'] = array( - '#type' => 'hidden', - '#attributes' => array('class' => array('filefield-source-clipboard-filename')), - ); - $element['filefield_clipboard']['contents'] = array( - '#type' => 'hidden', - '#attributes' => array('class' => array('filefield-source-clipboard-contents')), - ); - $element['filefield_clipboard']['upload'] = array( - '#type' => 'submit', - '#value' => t('Upload'), - '#ajax' => array( - 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], - 'wrapper' => $element['#id'] . '-ajax-wrapper', - 'effect' => 'fade', - 'progress' => array( - 'type' => 'throbber', - 'message' => t('Transfering file...'), - ), - ), - '#validate' => array(), - '#submit' => array('filefield_sources_field_submit'), - '#limit_validation_errors' => array($element['#parents']), - '#attributes' => array('style' => 'display: none;'), - ); - - return $element; -} - -/** - * A #filefield_value_callback function. - */ -function filefield_source_clipboard_value(&$element, &$item) { - if (isset($item['filefield_clipboard']['contents']) && strlen($item['filefield_clipboard']['contents']) > 0) { - // Check that the destination is writable. - $temporary_directory = 'temporary://'; - if (!file_prepare_directory($temporary_directory, FILE_MODIFY_PERMISSIONS)) { - watchdog('file', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => drupal_realpath($temporary_directory))); - drupal_set_message(t('The file could not be transferred because the temporary directory is not writable.'), 'error'); - return; - } - // Check that the destination is writable. - $directory = $element['#upload_location']; - $mode = variable_get('file_chmod_directory', 0775); - - // This first chmod check is for other systems such as S3, which don't work - // with file_prepare_directory(). - if (!drupal_chmod($directory, $mode) && !file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) { - watchdog('file', 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $url, '%destination' => drupal_realpath($directory))); - drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $url)), 'error'); - return; - } - - // Split the file information in mimetype and base64 encoded binary. - $base64_data = $item['filefield_clipboard']['contents']; - $comma_position = strpos($base64_data, ','); - $semicolon_position = strpos($base64_data, ';'); - $file_contents = base64_decode(substr($base64_data, $comma_position + 1)); - $mimetype = substr($base64_data, 5, $semicolon_position - 5); - - include_once('./includes/file.mimetypes.inc'); - $mime_mapping = file_mimetype_mapping(); - $mime_key = array_search($mimetype, $mime_mapping['mimetypes']); - $extension = array_search($mime_key, $mime_mapping['extensions']); - - $filename = trim($item['filefield_clipboard']['filename']); - $filename = preg_replace('/\.[a-z0-9]{3,4}$/', '', $filename); - $filename = (empty($filename) ? 'paste_' . REQUEST_TIME : $filename). '.' . $extension; - $filepath = file_create_filename($filename, $temporary_directory); - - $copy_success = FALSE; - if ($fp = @fopen($filepath, 'w')) { - fwrite($fp, $file_contents); - fclose($fp); - $copy_success = TRUE; - } - - if ($copy_success && $file = filefield_sources_save_file($filepath, $element['#upload_validators'], $element['#upload_location'])) { - $item = array_merge($item, (array) $file); - } - - // Remove the temporary file generated from paste. - @unlink($filepath); - } -} - -/** - * Handles the uploading of a file through a POST request. - */ -function filefield_source_clipboard_page($entity_type, $bundle_name, $field_name) { - global $conf; - - // Check access. - if (!$instance = field_info_instance($entity_type, $field_name, $bundle_name)) { - return drupal_access_denied(); - } - $field = field_info_field($field_name); - - module_load_include('inc', 'imce', 'inc/imce.page'); - return imce($field['settings']['uri_scheme']); -} - -/** - * Theme the output of the clipboard field. - */ -function theme_filefield_source_clipboard_element($variables) { - $element = $variables['element']; - - $capture = '
example_filename.png
'; - $element['#field_suffix'] = drupal_render($element['upload']) . ' ' . t('ctrl + v') . ''; - $element['#description'] = t('Enter a file name and paste an image from the clipboard. This feature only works in limited browsers.'); - $element['#children'] = $capture . drupal_render_children($element); - return '
' . theme('form_element', array('element' => $element)) . '
'; -} \ No newline at end of file diff --git a/sources/imce.inc b/sources/imce.inc deleted file mode 100644 index 8634d65..0000000 --- a/sources/imce.inc +++ /dev/null @@ -1,319 +0,0 @@ - t('IMCE file browser'), - 'label' => t('File browser'), - 'description' => t('Select a file to use from a file browser.'), - 'process' => 'filefield_source_imce_process', - 'value' => 'filefield_source_imce_value', - 'weight' => -1, - 'file' => 'includes/imce.inc', - ); - return $source; -} - -/** - * Implements hook_menu(). - */ -function filefield_source_imce_menu() { - $items = array(); - $items['file/imce/%/%/%'] = array( - 'page callback' => 'filefield_source_imce_page', - 'page arguments' => array(2, 3, 4), - 'access callback' => '_filefield_sources_field_access', - 'access arguments' => array(2, 3, 4), - 'file' => 'sources/imce.inc', - 'type' => MENU_CALLBACK, - ); - return $items; -} - -/** - * Implements hook_theme(). - */ -function filefield_source_imce_theme() { - return array( - 'filefield_source_imce_element' => array( - 'render element' => 'element', - 'file' => 'sources/imce.inc', - ), - ); -} - -/** - * Implements hook_filefield_source_settings(). - */ -function filefield_source_imce_settings($op, $instance) { - $return = array(); - - if ($op == 'form') { - $settings = $instance['widget']['settings']['filefield_sources']; - - $return['source_imce'] = array( - '#title' => t('IMCE file browser settings'), - '#type' => 'fieldset', - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#access' => module_exists('imce'), - ); - - $return['source_imce']['imce_mode'] = array( - '#type' => 'radios', - '#title' => t('File browser mode'), - '#options' => array( - 0 => t('Restricted: Users can only browse the field directory. No file operations are allowed.'), - 1 => t('Full: Browsable directories are defined by IMCE configuration profiles. File operations are allowed.', array('!imce-admin-url' => url('admin/config/media/imce'))), - ), - '#default_value' => isset($settings['source_imce']['imce_mode']) ? $settings['source_imce']['imce_mode'] : 0, - ); - } - elseif ($op == 'save') { - $return['source_imce']['imce_mode'] = 0; - } - - return $return; - -} - -/** - * A #process callback to extend the filefield_widget element type. - */ -function filefield_source_imce_process($element, &$form_state, $form) { - $instance = field_widget_instance($element, $form_state); - - $element['filefield_imce'] = array( - '#weight' => 100.5, - '#theme' => 'filefield_source_imce_element', - '#filefield_source' => TRUE, // Required for proper theming. - '#description' => filefield_sources_element_validation_help($element['#upload_validators']), - ); - - $filepath_id = $element['#id'] . '-imce-path'; - $display_id = $element['#id'] . '-imce-display'; - $select_id = $element['#id'] . '-imce-select'; - $element['filefield_imce']['file_path'] = array( - // IE doesn't support onchange events for hidden fields, so we use a - // textfield and hide it from display. - '#type' => 'textfield', - '#value' => '', - '#attributes' => array( - 'id' => $filepath_id, - 'onblur' => "if (this.value.length > 0) { jQuery('#$select_id').triggerHandler('mousedown'); }", - 'style' => 'position:absolute; left: -9999em', - ), - ); - - $imce_function = 'window.open(\'' . url('file/imce/' . $element['#entity_type'] . '/' . $element['#bundle'] . '/' . $element['#field_name'], array('query' => array('app' => $instance['label'] . '|url@' . $filepath_id))) . '\', \'\', \'width=760,height=560,resizable=1\'); return false;'; - $element['filefield_imce']['display_path'] = array( - '#type' => 'markup', - '#markup' => '' . t('No file selected') . ' (' . t('browse') . ')', - ); - - $element['filefield_imce']['select'] = array( - '#name' => implode('_', $element['#array_parents']) . '_imce_select', - '#type' => 'submit', - '#value' => t('Select'), - '#validate' => array(), - '#submit' => array('filefield_sources_field_submit'), - '#limit_validation_errors' => array($element['#parents']), - '#name' => $element['#name'] . '[filefield_imce][button]', - '#id' => $select_id, - '#attributes' => array('style' => 'display: none;'), - '#ajax' => array( - 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], - 'wrapper' => $element['#id'] . '-ajax-wrapper', - 'method' => 'replace', - 'effect' => 'fade', - ), - ); - - return $element; -} - -/** - * A #filefield_value_callback function. - */ -function filefield_source_imce_value($element, &$item) { - if (isset($item['filefield_imce']['file_path']) && $item['filefield_imce']['file_path'] != '') { - $field = field_info_field($element['#field_name']); - - $scheme = $field['settings']['uri_scheme']; - $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme); - $file_directory_prefix = $scheme == 'private' ? 'system/files' : $wrapper->getDirectoryPath(); - $uri = preg_replace('/^' . preg_quote(base_path() . $file_directory_prefix . '/', '/') . '/', $scheme . '://', $item['filefield_imce']['file_path']); - - // Resolve the file path to an FID. - $fid = db_select('file_managed', 'f') - ->condition('uri', rawurldecode($uri)) - ->fields('f', array('fid')) - ->execute() - ->fetchField(); - if ($fid) { - $file = file_load($fid); - if (filefield_sources_element_validate($element, $file)) { - $item = array_merge($item, (array) $file); - } - } - else { - form_error($element, t('The selected file could not be used because the file does not exist in the database.')); - } - // No matter what happens, clear the value from the file path field. - $item['filefield_imce']['file_path'] = ''; - } -} - -/** - * Theme the output of the autocomplete field. - */ -function theme_filefield_source_imce_element($variables) { - $element = $variables['element']; - - $output = drupal_render_children($element);; - return '
' . $output . '
'; -} - -/** - * Outputs the IMCE browser for FileField. - */ -function filefield_source_imce_page($entity_type, $bundle_name, $field_name) { - global $conf; - - // Check access. - if (!module_exists('imce') || !imce_access() || !$instance = field_info_instance($entity_type, $field_name, $bundle_name)) { - return drupal_access_denied(); - } - $field = field_info_field($field_name); - - // Full mode - if (!empty($instance['widget']['settings']['filefield_sources']['source_imce']['imce_mode'])) { - $conf['imce_custom_scan'] = 'filefield_source_imce_custom_scan_full'; - } - // Restricted mode - else { - $conf['imce_custom_scan'] = 'filefield_source_imce_custom_scan_restricted'; - $conf['imce_custom_field'] = $field + array('_uri' => file_field_widget_uri($field, $instance)); - } - - // Disable absolute URLs. - $conf['imce_settings_absurls'] = 0; - - module_load_include('inc', 'imce', 'inc/imce.page'); - return imce($field['settings']['uri_scheme']); -} - -/** - * Scan and return files, subdirectories, and total size for "full" mode. - */ -function filefield_source_imce_custom_scan_full($dirname, &$imce) { - // Get a list of files in the database for this directory. - $scheme = $imce['scheme']; - $sql_uri_name = $dirname == '.' ? $scheme . '://' : $scheme . '://' . $dirname . '/'; - - $result = db_select('file_managed', 'f') - ->fields('f', array('uri')) - ->condition('f.uri', $sql_uri_name . '%', 'LIKE') - ->condition('f.uri', $sql_uri_name . '_%/%', 'NOT LIKE') - ->execute(); - - $db_files = array(); - foreach ($result as $row) { - $db_files[basename($row->uri)] = 1; - } - - // Get the default IMCE directory scan, then filter down to database files. - $directory = imce_scan_directory($dirname, $imce); - foreach ($directory['files'] as $filename => $file) { - if (!isset($db_files[$filename])) { - unset($directory['files'][$filename]); - $directory['dirsize'] -= $file['size']; - } - } - - return $directory; -} - -/** - * Scan directory and return file list, subdirectories, and total size for Restricted Mode. - */ -function filefield_source_imce_custom_scan_restricted($dirname, &$imce) { - $field = $GLOBALS['conf']['imce_custom_field']; - $root = $imce['scheme'] . '://'; - $field_uri = $field['_uri']; - $is_root = $field_uri == $root; - - // Process IMCE. Make field directory the only accessible one. - $imce['dir'] = $is_root ? '.' : substr($field_uri, strlen($root)); - $imce['directories'] = array(); - if (!empty($imce['perm'])) { - filefield_source_imce_disable_perms($imce, array('browse')); - } - - // Create directory info - $directory = array('dirsize' => 0, 'files' => array(), 'subdirectories' => array(), 'error' => FALSE); - - if (isset($field['storage']['details']['sql']['FIELD_LOAD_CURRENT'])) { - $storage = $field['storage']['details']['sql']['FIELD_LOAD_CURRENT']; - $table_info = reset($storage); - $table = key($storage); - $sql_uri = $field_uri . ($is_root ? '' : '/'); - $query = db_select($table, 'cf'); - $query->innerJoin('file_managed', 'f', 'f.fid = cf.' . $table_info['fid']); - $result = $query->fields('f') - ->condition('f.status', 1) - ->condition('f.uri', $sql_uri . '%', 'LIKE') - ->condition('f.uri', $sql_uri . '%/%', 'NOT LIKE') - ->execute(); - foreach ($result as $file) { - // Get real name - $name = basename($file->uri); - // Get dimensions - $width = $height = 0; - if ($img = imce_image_info($file->uri)) { - $width = $img['width']; - $height = $img['height']; - } - $directory['files'][$name] = array( - 'name' => $name, - 'size' => $file->filesize, - 'width' => $width, - 'height' => $height, - 'date' => $file->timestamp, - ); - $directory['dirsize'] += $file->filesize; - } - } - - return $directory; - } - -/** - * Disable IMCE profile permissions. - */ -function filefield_source_imce_disable_perms(&$imce, $exceptions = array()) { - $disable_all = empty($exceptions); - foreach ($imce['perm'] as $name => $val) { - if ($disable_all || !in_array($name, $exceptions)) { - $imce['perm'][$name] = 0; - } - } - $imce['directories'][$imce['dir']] = array('name' => $imce['dir']) + $imce['perm']; -} diff --git a/sources/reference.inc b/sources/reference.inc deleted file mode 100644 index 8456da0..0000000 --- a/sources/reference.inc +++ /dev/null @@ -1,258 +0,0 @@ - t('Autocomplete reference textfield'), - 'label' => t('Reference existing'), - 'description' => t('Reuse an existing file by entering its file name.'), - 'process' => 'filefield_source_reference_process', - 'value' => 'filefield_source_reference_value', - 'weight' => 1, - 'file' => 'includes/reference.inc', - ); - return $source; -} - -/** - * Implements hook_menu(). - */ -function filefield_source_reference_menu() { - $items = array(); - - $items['file/reference/%/%/%'] = array( - 'page callback' => 'filefield_source_reference_autocomplete', - 'page arguments' => array(2, 3, 4), - 'access callback' => '_filefield_sources_field_access', - 'access arguments' => array(2, 3, 4), - 'file' => 'sources/reference.inc', - 'type' => MENU_CALLBACK, - ); - return $items; -} - -/** - * Implements hook_theme(). - */ -function filefield_source_reference_theme() { - return array( - 'filefield_source_reference_element' => array( - 'render element' => 'element', - 'file' => 'sources/reference.inc', - ), - 'filefield_source_reference_autocomplete_item' => array( - 'variables' => array('file' => NULL), - 'file' => 'sources/reference.inc', - ), - ); -} - -/** - * Implements hook_filefield_source_settings(). - */ -function filefield_source_reference_settings($op, $instance) { - $return = array(); - - if ($op == 'form') { - $settings = $instance['widget']['settings']['filefield_sources']; - - $return['source_reference'] = array( - '#title' => t('Autocomplete reference options'), - '#type' => 'fieldset', - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - - $return['source_reference']['autocomplete'] = array( - '#title' => t('Match file name'), - '#options' => array( - '0' => t('Starts with string'), - '1' => t('Contains string'), - ), - '#type' => 'radios', - '#default_value' => isset($settings['source_reference']['autocomplete']) ? $settings['source_reference']['autocomplete'] : '0', - ); - } - elseif ($op == 'save') { - $return['source_reference']['autocomplete'] = '0'; - } - - return $return; -} - -/** - * A #process callback to extend the filefield_widget element type. - */ -function filefield_source_reference_process($element, &$form_state, $form) { - - $element['filefield_reference'] = array( - '#weight' => 100.5, - '#theme' => 'filefield_source_reference_element', - '#filefield_source' => TRUE, // Required for proper theming. - '#filefield_sources_hint_text' => FILEFIELD_SOURCE_REFERENCE_HINT_TEXT, - ); - - $element['filefield_reference']['autocomplete'] = array( - '#type' => 'textfield', - '#autocomplete_path' => 'file/reference/' . $element['#entity_type'] . '/' . $element['#bundle'] . '/' . $element['#field_name'], - '#description' => filefield_sources_element_validation_help($element['#upload_validators']), - ); - - $element['filefield_reference']['select'] = array( - '#name' => implode('_', $element['#array_parents']) . '_autocomplete_select', - '#type' => 'submit', - '#value' => t('Select'), - '#validate' => array(), - '#submit' => array('filefield_sources_field_submit'), - '#name' => $element['#name'] . '[filefield_reference][button]', - '#limit_validation_errors' => array($element['#parents']), - '#ajax' => array( - 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], - 'wrapper' => $element['#id'] . '-ajax-wrapper', - 'effect' => 'fade', - ), - ); - - return $element; -} - -/** - * A #filefield_value_callback function. - */ -function filefield_source_reference_value($element, &$item) { - if (isset($item['filefield_reference']['autocomplete']) && strlen($item['filefield_reference']['autocomplete']) > 0 && $item['filefield_reference']['autocomplete'] != FILEFIELD_SOURCE_REFERENCE_HINT_TEXT) { - $matches = array(); - if (preg_match('/\[fid:(\d+)\]/', $item['filefield_reference']['autocomplete'], $matches)) { - $fid = $matches[1]; - if ($file = file_load($fid)) { - - // Remove file size restrictions, since the file already exists on disk. - if (isset($element['#upload_validators']['file_validate_size'])) { - unset($element['#upload_validators']['file_validate_size']); - } - - // Check that the user has access to this file through hook_download(). - if (!filefield_sources_file_access($file->uri)) { - form_error($element, t('You do not have permission to use the selected file.')); - } - elseif (filefield_sources_element_validate($element, (object) $file)) { - $item = array_merge($item, (array) $file); - } - } - else { - form_error($element, t('The referenced file could not be used because the file does not exist in the database.')); - } - } - // No matter what happens, clear the value from the autocomplete. - $item['filefield_reference']['autocomplete'] = ''; - } -} - -/** - * Menu callback; autocomplete.js callback to return a list of files. - */ -function filefield_source_reference_autocomplete($entity_type, $bundle_name, $field_name, $filename) { - $field = field_info_instance($entity_type, $field_name, $bundle_name); - - $items = array(); - if (!empty($field)) { - $files = filefield_source_reference_get_files($filename, $field); - foreach ($files as $fid => $file) { - $items[$file->filename ." [fid:$fid]"] = theme('filefield_source_reference_autocomplete_item', array('file' => $file)); - } - } - - drupal_json_output($items); -} - -/** - * Theme the output of a single item in the autocomplete list. - */ -function theme_filefield_source_reference_autocomplete_item($variables) { - $file = $variables['file']; - - $output = ''; - $output .= '
'; - $output .= '' . check_plain($file->filename) . ' (' . format_size($file->filesize) . ')'; - $output .= '
'; - return $output; -} - -/** - * Theme the output of the autocomplete field. - */ -function theme_filefield_source_reference_element($variables) { - $element = $variables['element']; - - $element['autocomplete']['#field_suffix'] = drupal_render($element['select']); - return '
' . drupal_render($element['autocomplete']) . '
'; -} - -/** - * Get all the files used within a particular field (or all fields). - * - * @param $file_name - * The partial name of the file to retrieve. - * @param $instance - * Optional. A CCK field array for which to filter returned files. - */ -function filefield_source_reference_get_files($filename, $instance = NULL) { - $instances = array(); - if (!isset($instance)) { - foreach (field_info_fields() as $instance) { - if ($instance['type'] == 'file' || $instance['type'] == 'image') { - $instances[] = $instance; - } - } - } - else { - $instances = array($instance); - } - - $files = array(); - foreach ($instances as $instance) { - // Load the field data, which contains the schema information. - $field = field_info_field($instance['field_name']); - - // We don't support fields that are not stored with SQL. - if (!isset($field['storage']['details']['sql']['FIELD_LOAD_CURRENT'])) { - continue; - } - - // 1 == contains, 0 == starts with. - $like = empty($instance['widget']['settings']['filefield_sources']['source_reference']['autocomplete']) ? (db_like($filename) . '%') : ('%' . db_like($filename) . '%'); - - $table_info = reset($field['storage']['details']['sql']['FIELD_LOAD_CURRENT']); - $table = key($field['storage']['details']['sql']['FIELD_LOAD_CURRENT']); - $query = db_select($table, 'cf'); - $query->innerJoin('file_managed', 'f', 'f.fid = cf.' . $table_info['fid']); - $query->fields('f'); - $query->condition('f.status', 1); - $query->condition('f.filename', $like, 'LIKE'); - $query->orderBy('f.timestamp', 'DESC'); - $query->groupBy('f.fid'); - $query->range(0, 30); - $query->addTag('filefield_source_reference_list'); - $result = $query->execute(); - - foreach ($result as $file) { - $files[$file->fid] = $file; - } - } - - return $files; -} diff --git a/sources/remote.inc b/sources/remote.inc deleted file mode 100644 index aa890a8..0000000 --- a/sources/remote.inc +++ /dev/null @@ -1,396 +0,0 @@ - t('Remote URL textfield'), - 'label' => t('Remote URL'), - 'description' => t('Download a file from a remote server.'), - 'process' => 'filefield_source_remote_process', - 'value' => 'filefield_source_remote_value', - 'file' => 'includes/remote.inc', - ); - return $source; -} - -/** - * Implements hook_menu(). - */ -function filefield_source_remote_menu() { - $items = array(); - - $items['file/remote/progress/%/%/%/%'] = array( - 'page callback' => 'filefield_source_remote_progress', - 'page arguments' => array(3, 4, 5, 6), - 'access callback' => TRUE, - 'file' => 'sources/remote.inc', - 'type' => MENU_CALLBACK, - ); - return $items; -} - -/** - * Implements hook_theme(). - */ -function filefield_source_remote_theme() { - return array( - 'filefield_source_remote_element' => array( - 'render element' => 'element', - 'file' => 'sources/remote.inc', - ), - ); -} - -/** - * Implements hook_filefield_source_settings(). - */ -function filefield_source_remote_settings($op, $instance) { - $return = array(); - - // Add settings to the FileField widget form. - - return $return; - -} - -/** - * A #process callback to extend the filefield_widget element type. - */ -function filefield_source_remote_process($element, &$form_state, $form) { - - $element['filefield_remote'] = array( - '#weight' => 100.5, - '#theme' => 'filefield_source_remote_element', - '#filefield_source' => TRUE, // Required for proper theming. - '#filefield_sources_hint_text' => FILEFIELD_SOURCE_REMOTE_HINT_TEXT, - ); - - $element['filefield_remote']['url'] = array( - '#type' => 'textfield', - '#description' => filefield_sources_element_validation_help($element['#upload_validators']), - '#maxlength' => NULL, - ); - - $element['filefield_remote']['transfer'] = array( - '#name' => implode('_', $element['#array_parents']) . '_transfer', - '#type' => 'submit', - '#value' => t('Transfer'), - '#validate' => array(), - '#submit' => array('filefield_sources_field_submit'), - '#limit_validation_errors' => array($element['#parents']), - '#ajax' => array( - 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], - 'wrapper' => $element['#id'] . '-ajax-wrapper', - 'effect' => 'fade', - 'progress' => array( - 'type' => 'bar', - 'path' => 'file/remote/progress/' . $element['#entity_type'] . '/' . $element['#bundle'] . '/' . $element['#field_name'] . '/' . $element['#delta'], - 'message' => t('Starting transfer...'), - ), - ), - ); - - return $element; -} - -/** - * A #filefield_value_callback function. - */ -function filefield_source_remote_value($element, &$item) { - if (isset($item['filefield_remote']['url']) && strlen($item['filefield_remote']['url']) > 0 && valid_url($item['filefield_remote']['url']) && $item['filefield_remote']['url'] != FILEFIELD_SOURCE_REMOTE_HINT_TEXT) { - $field = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']); - $url = $item['filefield_remote']['url']; - - // Check that the destination is writable. - $temporary_directory = 'temporary://'; - if (!file_prepare_directory($temporary_directory, FILE_MODIFY_PERMISSIONS)) { - watchdog('file', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => drupal_realpath($temporary_directory))); - drupal_set_message(t('The file could not be transferred because the temporary directory is not writable.'), 'error'); - return; - } - - // Check that the destination is writable. - $directory = $element['#upload_location']; - $mode = variable_get('file_chmod_directory', 0775); - - // This first chmod check is for other systems such as S3, which don't work - // with file_prepare_directory(). - if (!drupal_chmod($directory, $mode) && !file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) { - watchdog('file', 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $url, '%destination' => drupal_realpath($directory))); - drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $url)), 'error'); - return; - } - - // Check the headers to make sure it exists and is within the allowed size. - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, TRUE); - curl_setopt($ch, CURLOPT_NOBODY, TRUE); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, '_filefield_source_remote_parse_header'); - // Causes a warning if PHP safe mode is on. - @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE); - curl_exec($ch); - $info = curl_getinfo($ch); - if ($info['http_code'] != 200) { - curl_setopt($ch, CURLOPT_HTTPGET, TRUE); - $file_contents = curl_exec($ch); - $info = curl_getinfo($ch); - } - curl_close($ch); - - if ($info['http_code'] != 200) { - switch ($info['http_code']) { - case 403: - form_error($element, t('The remote file could not be transferred because access to the file was denied.')); - break; - case 404: - form_error($element, t('The remote file could not be transferred because it was not found.')); - break; - default: - form_error($element, t('The remote file could not be transferred due to an HTTP error (@code).', array('@code' => $info['http_code']))); - } - return; - } - - // Update the $url variable to reflect any redirects. - $url = $info['url']; - $url_info = parse_url($url); - - // Determine the proper filename by reading the filename given in the - // Content-Disposition header. If the server fails to send this header, - // fall back on the basename of the URL. - // - // We prefer to use the Content-Disposition header, because we can then - // use URLs like http://example.com/get_file/23 which would otherwise be - // rejected because the URL basename lacks an extension. - $filename = _filefield_source_remote_filename(); - if (empty($filename)) { - $filename = rawurldecode(basename($url_info['path'])); - } - - $pathinfo = pathinfo($filename); - - // Create the file extension from the MIME header if all else has failed. - if (empty($pathinfo['extension']) && $extension = _filefield_source_remote_mime_extension()) { - $filename = $filename . '.' . $extension; - $pathinfo = pathinfo($filename); - } - - $filename = filefield_sources_clean_filename($filename, $field['settings']['file_extensions']); - $filepath = file_create_filename($filename, $temporary_directory); - - if (empty($pathinfo['extension'])) { - form_error($element, t('The remote URL must be a file and have an extension.')); - return; - } - - // Perform basic extension check on the file before trying to transfer. - $extensions = $field['settings']['file_extensions']; - $regex = '/\.('. preg_replace('/[ +]/', '|', preg_quote($extensions)) .')$/i'; - if (!empty($extensions) && !preg_match($regex, $filename)) { - form_error($element, t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions))); - return; - } - - // Check file size based off of header information. - if (!empty($element['#upload_validators']['file_validate_size'][0])) { - $max_size = $element['#upload_validators']['file_validate_size'][0]; - $file_size = $info['download_content_length']; - if ($file_size > $max_size) { - form_error($element, t('The remote file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file_size), '%maxsize' => format_size($max_size)))); - return; - } - } - - // Set progress bar information. - $options = array( - 'key' => $element['#entity_type'] . '_' . $element['#bundle'] . '_' . $element['#field_name'] . '_' . $element['#delta'], - 'filepath' => $filepath, - ); - filefield_source_remote_set_transfer_options($options); - - $transfer_success = FALSE; - // If we've already downloaded the entire file because the header-retrieval - // failed, just ave the contents we have. - if (isset($file_contents)) { - if ($fp = @fopen($filepath, 'w')) { - fwrite($fp, $file_contents); - fclose($fp); - $transfer_success = TRUE; - } - } - // If we don't have the file contents, download the actual file. - else { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, FALSE); - curl_setopt($ch, CURLOPT_WRITEFUNCTION, 'filefield_source_remote_curl_write'); - // Causes a warning if PHP safe mode is on. - @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE); - $transfer_success = curl_exec($ch); - curl_close($ch); - } - if ($transfer_success && $file = filefield_sources_save_file($filepath, $element['#upload_validators'], $element['#upload_location'])) { - $item = array_merge($item, (array) $file); - } - - // Delete the temporary file. - @unlink($filepath); - } -} - -/** - * Parse cURL header and record the filename specified in Content-Disposition. - */ -function _filefield_source_remote_parse_header(&$ch, $header) { - if (preg_match('/Content-Disposition:.*?filename="(.+?)"/', $header, $matches)) { - // Content-Disposition: attachment; filename="FILE NAME HERE" - _filefield_source_remote_filename($matches[1]); - } - elseif (preg_match('/Content-Disposition:.*?filename=([^; ]+)/', $header, $matches)) { - // Content-Disposition: attachment; filename=file.ext - $uri = trim($matches[1]); - _filefield_source_remote_filename($uri); - } - elseif (preg_match('/Content-Type:[ ]*([a-z0-9_\-]+\/[a-z0-9_\-]+)/i', $header, $matches)) { - $mime_type = $matches[1]; - _filefield_source_remote_mime_extension($mime_type); - } - - // This is required by cURL. - return strlen($header); -} - -/** - * Get/set the remote file name in a static variable. - */ -function _filefield_source_remote_filename($curl_filename = NULL) { - static $filename = NULL; - if (isset($curl_filename)) { - $filename = $curl_filename; - } - return $filename; -} - -/** - * Get/set the remote file extension in a static variable. - */ -function _filefield_source_remote_mime_extension($curl_mime_type = NULL) { - static $extension = NULL; - if (isset($curl_mime_type)) { - include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc'; - $curl_mime_type = drupal_strtolower($curl_mime_type); - $mapping = file_mimetype_mapping(); - // See if this matches a known MIME type. - $map_id = array_search($curl_mime_type, $mapping['mimetypes']); - if ($map_id !== FALSE) { - // If we have a match, get this list of likely extensions. For some reason - // Drupal lists the "most common" extension last for most file types - // including php, jpg, and doc. - if ($extensions = array_keys($mapping['extensions'], $map_id)) { - $extension = end($extensions); - } - } - } - return $extension; -} - -/** - * Menu callback; progress.js callback to return upload progress. - */ -function filefield_source_remote_progress($entity_type, $bundle_name, $field_name, $delta) { - $key = $entity_type . '_' . $bundle_name . '_' . $field_name . '_' . $delta; - $progress = array( - 'message' => t('Starting transfer...'), - 'percentage' => -1, - ); - - if ($cache = cache_get('filefield_transfer:'. session_id() . ':' . $key)) { - $current = $cache->data['current']; - $total = $cache->data['total']; - $progress['message'] = t('Transferring... (@current of @total)', array('@current' => format_size($current), '@total' => format_size($total))); - $progress['percentage'] = round(100 * $current / $total); - } - - drupal_json_output($progress); -} - -/** - * cURL write function to save the file to disk. Also updates progress bar. - */ -function filefield_source_remote_curl_write(&$ch, $data) { - $progress_update = 0; - $options = filefield_source_remote_get_transfer_options(); - - // Get the current progress and update the progress value. - // Only update every 64KB to reduce cache_set calls. cURL usually writes - // in 16KB chunks. - if (curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) / 65536 > $progress_update) { - $progress_update++; - $progress = array( - 'current' => curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD), - 'total' => curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD), - ); - // Set a cache so that we can retrieve this value from the progress bar. - $cid = 'filefield_transfer:'. session_id() . ':' . $options['key']; - if ($progress['current'] != $progress['total']) { - cache_set($cid, $progress, 'cache', time() + 300); - } - else { - cache_clear_all($cid, 'cache'); - } - } - - $data_length = 0; - if ($fp = @fopen($options['filepath'], 'a')) { - fwrite($fp, $data); - fclose($fp); - $data_length = strlen($data); - } - - return $data_length; -} - -/** - * Set a transfer key that can be retreived by the progress function. - */ -function filefield_source_remote_set_transfer_options($options = NULL) { - static $current = FALSE; - if (isset($options)) { - $current = $options; - } - return $current; -} - -/** - * Get a transfer key that can be retrieved by the progress function. - */ -function filefield_source_remote_get_transfer_options() { - return filefield_source_remote_set_transfer_options(); -} - -/** - * Theme the output of the autocomplete field. - */ -function theme_filefield_source_remote_element($variables) { - $element = $variables['element']; - - $element['url']['#field_suffix'] = drupal_render($element['transfer']); - return '
' . drupal_render($element['url']) . '
'; -} - diff --git a/src/Access/FieldAccessCheck.php b/src/Access/FieldAccessCheck.php new file mode 100644 index 0000000..b70f535 --- /dev/null +++ b/src/Access/FieldAccessCheck.php @@ -0,0 +1,38 @@ +access('edit', $account, TRUE); + } + +} diff --git a/src/Annotation/FilefieldSource.php b/src/Annotation/FilefieldSource.php new file mode 100644 index 0000000..3a873fe --- /dev/null +++ b/src/Annotation/FilefieldSource.php @@ -0,0 +1,62 @@ +checkDefaultMapping(); + + $mime_key = array_search($mimetype, $this->mapping['mimetypes']); + $extension = array_search($mime_key, $this->mapping['extensions']); + + return $extension; + } + + /** + * Convert mime type to most common extension. + * + * @param string $mimetype + * Mime type. + * + * @return string|bool + * Return extension if found, FALSE otherwise. + */ + public function convertMimeTypeToMostCommonExtension($mimetype) { + $this->checkDefaultMapping(); + + $extension = FALSE; + if (isset($mimetype)) { + // See if this matches a known MIME type. + $mime_key = array_search($mimetype, $this->mapping['mimetypes']); + if ($mime_key !== FALSE) { + // If we have a match, get this list of likely extensions. For some + // reason Drupal lists the "most common" extension last for most file + // types including php, jpg, and doc. + if ($extensions = array_keys($this->mapping['extensions'], $mime_key)) { + $extension = end($extensions); + } + } + } + return $extension; + } + + /** + * Check for default mapping. + */ + private function checkDefaultMapping() { + if ($this->mapping === NULL) { + $mapping = $this->defaultMapping; + // Allow modules to alter the default mapping. + $this->moduleHandler->alter('file_mimetype_mapping', $mapping); + $this->mapping = $mapping; + } + } + +} diff --git a/src/FilefieldSourceInterface.php b/src/FilefieldSourceInterface.php new file mode 100644 index 0000000..8ea45ca --- /dev/null +++ b/src/FilefieldSourceInterface.php @@ -0,0 +1,55 @@ +setCacheBackend($cache_backend, 'filefield_sources'); + + parent::__construct('Plugin/FilefieldSource', $namespaces, $module_handler, 'Drupal\filefield_sources\FilefieldSourceInterface', 'Drupal\filefield_sources\Annotation\FilefieldSource'); + } + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + $definitions = parent::getDefinitions(); + if (!\Drupal::moduleHandler()->moduleExists('imce') || !imce_access()) { + unset($definitions['imce']); + } + return $definitions; + } + +} diff --git a/src/FilefieldSourcesServiceProvider.php b/src/FilefieldSourcesServiceProvider.php new file mode 100644 index 0000000..8f2a65f --- /dev/null +++ b/src/FilefieldSourcesServiceProvider.php @@ -0,0 +1,26 @@ +getDefinition('file.mime_type.guesser.extension'); + $definition->setClass('Drupal\filefield_sources\File\MimeType\ExtensionMimeTypeGuesser'); + } + +} diff --git a/src/Plugin/FilefieldSource/Attach.php b/src/Plugin/FilefieldSource/Attach.php new file mode 100644 index 0000000..be269bf --- /dev/null +++ b/src/Plugin/FilefieldSource/Attach.php @@ -0,0 +1,349 @@ +log(E_NOTICE, 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array( + '%file' => $filepath, + '%destination' => drupal_realpath($directory), + )); + drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $filepath)), 'error'); + return; + } + + // Clean up the file name extensions and transliterate. + $original_filepath = $filepath; + $new_filepath = filefield_sources_clean_filename($filepath, $instance->settings['file_extensions']); + rename($filepath, $new_filepath); + $filepath = $new_filepath; + + // Run all the normal validations, minus file size restrictions. + $validators = $element['#upload_validators']; + if (isset($validators['file_validate_size'])) { + unset($validators['file_validate_size']); + } + + // Save the file to the new location. + if ($file = filefield_sources_save_file($filepath, $validators, $directory)) { + if (!in_array($file->id(), $input['fids'])) { + $input['fids'][] = $file->id(); + } + + // Delete the original file if "moving" the file instead of copying. + if ($element['#filefield_sources_settings']['source_attach']['attach_mode'] !== FILEFIELD_SOURCE_ATTACH_MODE_COPY) { + @unlink($filepath); + } + } + + // Restore the original file name if the file still exists. + if (file_exists($filepath) && $filepath != $original_filepath) { + rename($filepath, $original_filepath); + } + + $input['filefield_attach']['filename'] = ''; + } + } + + /** + * {@inheritdoc} + */ + public static function process(array &$element, FormStateInterface $form_state, array &$complete_form) { + $settings = $element['#filefield_sources_settings']['source_attach']; + $field_name = $element['#field_name']; + $instance = entity_load('field_config', $element['#entity_type'] . '.' . $element['#bundle'] . '.' . $field_name); + + $element['filefield_attach'] = array( + '#weight' => 100.5, + '#theme' => 'filefield_sources_element', + '#source_id' => 'attach', + // Required for proper theming. + '#filefield_source' => TRUE, + ); + + $path = static::getDirectory($settings); + $options = static::getAttachOptions($path); + + // If we have built this element before, append the list of options that we + // had previously. This allows files to be deleted after copying them and + // still be considered a valid option during the validation and submit. + $triggering_element = $form_state->getTriggeringElement(); + $property = array( + 'filefield_sources', + $field_name, + 'attach_options', + ); + if (!isset($triggering_element) && $form_state->has($property)) { + $attach_options = $form_state->get($property); + $options = $options + $attach_options; + } + // On initial form build and rebuilds after processing input, save the + // original list of options so they can be restored in the line above. + else { + $form_state->set(array('filefield_sources', $field_name, 'attach_options'), $options); + } + + $description = t('This method may be used to attach files that exceed the file size limit. Files may be attached from the %directory directory on the server, usually uploaded through FTP.', array('%directory' => realpath($path))); + + // Error messages. + if ($options === FALSE || empty($settings['path'])) { + $attach_message = t('A file attach directory could not be located.'); + $attach_description = t('Please check your settings for the %field field.', array('%field' => $instance->getLabel())); + } + elseif (!count($options)) { + $attach_message = t('There currently are no files to attach.'); + $attach_description = $description; + } + + if (isset($attach_message)) { + $element['filefield_attach']['attach_message'] = array( + '#markup' => $attach_message, + ); + $element['filefield_attach']['#description'] = $attach_description; + } + else { + $validators = $element['#upload_validators']; + if (isset($validators['file_validate_size'])) { + unset($validators['file_validate_size']); + } + $description .= '
' . filefield_sources_element_validation_help($validators); + $element['filefield_attach']['filename'] = array( + '#type' => 'select', + '#options' => $options, + ); + $element['filefield_attach']['#description'] = $description; + } + + $ajax_settings = [ + 'url' => Url::fromRoute('file.ajax_upload'), + 'options' => [ + 'query' => [ + 'element_parents' => implode('/', $element['#array_parents']), + 'form_build_id' => $complete_form['form_build_id']['#value'], + ], + ], + 'wrapper' => $element['#id'] . '-ajax-wrapper', + 'effect' => 'fade', + ]; + + $element['filefield_attach']['attach'] = [ + '#name' => implode('_', $element['#parents']) . '_attach', + '#type' => 'submit', + '#value' => t('Attach'), + '#validate' => [], + '#submit' => ['filefield_sources_field_submit'], + '#limit_validation_errors' => [$element['#parents']], + '#ajax' => $ajax_settings, + ]; + + return $element; + } + + /** + * Theme the output of the attach element. + */ + public static function element($variables) { + $element = $variables['element']; + + if (isset($element['attach_message'])) { + $output = $element['attach_message']['#markup']; + } + else { + $size = !empty($element['filename']['#size']) ? ' size="' . $element['filename']['#size'] . '"' : ''; + $element['filename']['#attributes']['class'][] = 'form-select'; + $multiple = !empty($element['#multiple']); + $output = ''; + } + $output .= drupal_render($element['attach']); + $element['#children'] = $output; + $element['#theme_wrappers'] = array('form_element'); + return '
' . drupal_render($element) . '
'; + } + + /** + * Get directory from settings. + * + * @param array $settings + * Attach source's settings. + * @param object $account + * User to replace token. + * + * @return string + * Path that contains files to attach. + */ + protected static function getDirectory(array $settings, $account = NULL) { + $account = isset($account) ? $account : \Drupal::currentUser(); + $path = $settings['path']; + $absolute = !empty($settings['absolute']); + + // Replace user level tokens. + // Node level tokens require a lot of complexity like temporary storage + // locations when values don't exist. See the filefield_paths module. + if (\Drupal::moduleHandler()->moduleExists('token')) { + $path = token_replace($path, array('user' => $account)); + } + + return $absolute ? $path : file_default_scheme() . '://' . $path; + } + + /** + * Get attach options. + * + * @param string $path + * Path to scan files. + * + * @return array + * List of options. + */ + protected static function getAttachOptions($path) { + if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY)) { + drupal_set_message(t('Specified file attach path must exist or be writable.'), 'error'); + return FALSE; + } + + $options = array(); + $file_attach = file_scan_directory($path, '/.*/', array('key' => 'filename'), 0); + + if (count($file_attach)) { + $options = array('' => t('-- Select file --')); + foreach ($file_attach as $filename => $fileinfo) { + $filename = basename($filename); + $options[$fileinfo->uri] = str_replace($path . '/', '', $fileinfo->uri); + } + } + + natcasesort($options); + return $options; + } + + /** + * Implements hook_filefield_source_settings(). + */ + public static function settings(WidgetInterface $plugin) { + $settings = $plugin->getThirdPartySetting('filefield_sources', 'filefield_sources', array( + 'source_attach' => array( + 'path' => FILEFIELD_SOURCE_ATTACH_DEFAULT_PATH, + 'absolute' => FILEFIELD_SOURCE_ATTACH_RELATIVE, + 'attach_mode' => FILEFIELD_SOURCE_ATTACH_MODE_MOVE, + ), + )); + + $return['source_attach'] = array( + '#title' => t('File attach settings'), + '#type' => 'details', + '#description' => t('File attach allows for selecting a file from a directory on the server, commonly used in combination with FTP.') . ' ' . t('This file source will ignore file size checking when used.') . '', + '#element_validate' => array(array(get_called_class(), 'filePathValidate')), + '#weight' => 3, + ); + $return['source_attach']['path'] = array( + '#type' => 'textfield', + '#title' => t('File attach path'), + '#default_value' => $settings['source_attach']['path'], + '#size' => 60, + '#maxlength' => 128, + '#description' => t('The directory within the File attach location that will contain attachable files.'), + ); + if (\Drupal::moduleHandler()->moduleExists('token')) { + $return['source_attach']['tokens'] = array( + '#theme' => 'token_tree', + '#token_types' => array('user'), + ); + } + $return['source_attach']['absolute'] = array( + '#type' => 'radios', + '#title' => t('File attach location'), + '#options' => array( + FILEFIELD_SOURCE_ATTACH_RELATIVE => t('Within the files directory'), + FILEFIELD_SOURCE_ATTACH_ABSOLUTE => t('Absolute server path'), + ), + '#default_value' => $settings['source_attach']['absolute'], + '#description' => t('The File attach path may be with the files directory (%file_directory) or from the root of your server. If an absolute path is used and it does not start with a "/" your path will be relative to your site directory: %realpath.', array('%file_directory' => drupal_realpath(file_default_scheme() . '://'), '%realpath' => realpath('./'))), + ); + $return['source_attach']['attach_mode'] = array( + '#type' => 'radios', + '#title' => t('Attach method'), + '#options' => array( + FILEFIELD_SOURCE_ATTACH_MODE_MOVE => t('Move the file directly to the final location'), + FILEFIELD_SOURCE_ATTACH_MODE_COPY => t('Leave a copy of the file in the attach directory'), + ), + '#default_value' => isset($settings['source_attach']['attach_mode']) ? $settings['source_attach']['attach_mode'] : 'move', + ); + + return $return; + } + + /** + * Validate file path. + * + * @param array $element + * Form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param array $complete_form + * Complete form. + */ + public static function filePathValidate(array &$element, FormStateInterface $form_state, array &$complete_form) { + $parents = $element['#parents']; + array_pop($parents); + $input_exists = FALSE; + + // Get input of the whole parent element. + $input = NestedArray::getValue($form_state->getValues(), $parents, $input_exists); + if ($input_exists) { + // Only validate if this source is enabled. + if (!$input['sources']['attach']) { + return; + } + + // Strip slashes from the end of the file path. + $filepath = rtrim($element['path']['#value'], '\\/'); + $form_state->setValueForElement($element['path'], $filepath); + $filepath = static::getDirectory($input['source_attach']); + + // Check that the directory exists and is writable. + if (!file_prepare_directory($filepath, FILE_CREATE_DIRECTORY)) { + $form_state->setError($element['path'], t('Specified file attach path must exist or be writable.')); + } + } + } + +} diff --git a/src/Plugin/FilefieldSource/Clipboard.php b/src/Plugin/FilefieldSource/Clipboard.php new file mode 100644 index 0000000..17e5813 --- /dev/null +++ b/src/Plugin/FilefieldSource/Clipboard.php @@ -0,0 +1,153 @@ +limited browser support)"), + * label = @Translation("Clipboard"), + * description = @Translation("Allow users to paste a file directly from the clipboard."), + * weight = 1 + * ) + */ +class Clipboard implements FilefieldSourceInterface { + + /** + * {@inheritdoc} + */ + public static function value(array &$element, &$input, FormStateInterface $form_state) { + if (isset($input['filefield_clipboard']['contents']) && strlen($input['filefield_clipboard']['contents']) > 0) { + // Check that the destination is writable. + $temporary_directory = 'temporary://'; + if (!file_prepare_directory($temporary_directory, FILE_MODIFY_PERMISSIONS)) { + \Drupal::logger('filefield_sources')->log(E_NOTICE, 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => drupal_realpath($temporary_directory))); + drupal_set_message(t('The file could not be transferred because the temporary directory is not writable.'), 'error'); + return; + } + // Check that the destination is writable. + $directory = $element['#upload_location']; + $mode = Settings::get('file_chmod_directory', FILE_CHMOD_DIRECTORY); + + // This first chmod check is for other systems such as S3, which don't + // work with file_prepare_directory(). + if (!drupal_chmod($directory, $mode) && !file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) { + $url = $input['filefield_clipboard']['filename']; + \Drupal::logger('filefield_sources')->log(E_NOTICE, 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $url, '%destination' => drupal_realpath($directory))); + drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $url)), 'error'); + return; + } + + // Split the file information in mimetype and base64 encoded binary. + $base64_data = $input['filefield_clipboard']['contents']; + $comma_position = strpos($base64_data, ','); + $semicolon_position = strpos($base64_data, ';'); + $file_contents = base64_decode(substr($base64_data, $comma_position + 1)); + $mimetype = substr($base64_data, 5, $semicolon_position - 5); + + $extension = \Drupal::service('file.mime_type.guesser.extension')->convertMimeTypeToExtension($mimetype); + + $filename = trim($input['filefield_clipboard']['filename']); + $filename = preg_replace('/\.[a-z0-9]{3,4}$/', '', $filename); + $filename = (empty($filename) ? 'paste_' . REQUEST_TIME : $filename) . '.' . $extension; + $filepath = file_create_filename($filename, $temporary_directory); + + $copy_success = FALSE; + if ($fp = @fopen($filepath, 'w')) { + fwrite($fp, $file_contents); + fclose($fp); + $copy_success = TRUE; + } + + if ($copy_success && $file = filefield_sources_save_file($filepath, $element['#upload_validators'], $element['#upload_location'])) { + if (!in_array($file->id(), $input['fids'])) { + $input['fids'][] = $file->id(); + } + } + + // Remove the temporary file generated from paste. + @unlink($filepath); + } + } + + /** + * {@inheritdoc} + */ + public static function process(array &$element, FormStateInterface $form_state, array &$complete_form) { + $element['filefield_clipboard'] = array( + '#weight' => 100.5, + '#theme' => 'filefield_sources_element', + '#source_id' => 'clipboard', + // Required for proper theming. + '#filefield_source' => TRUE, + '#filefield_sources_hint_text' => t('Enter filename then paste.'), + '#description' => filefield_sources_element_validation_help($element['#upload_validators']), + ); + + $element['filefield_clipboard']['capture'] = array( + '#type' => 'item', + '#markup' => '
example_filename.png
' . t('ctrl + v') . '', + '#description' => t('Enter a file name and paste an image from the clipboard. This feature only works in limited browsers.'), + ); + + $element['filefield_clipboard']['filename'] = array( + '#type' => 'hidden', + '#attributes' => array('class' => array('filefield-source-clipboard-filename')), + ); + $element['filefield_clipboard']['contents'] = array( + '#type' => 'hidden', + '#attributes' => array('class' => array('filefield-source-clipboard-contents')), + ); + + $ajax_settings = [ + 'url' => Url::fromRoute('file.ajax_upload'), + 'options' => [ + 'query' => [ + 'element_parents' => implode('/', $element['#array_parents']), + 'form_build_id' => $complete_form['form_build_id']['#value'], + ], + ], + 'wrapper' => $element['#id'] . '-ajax-wrapper', + 'effect' => 'fade', + 'progress' => [ + 'type' => 'throbber', + 'message' => t('Transfering file...'), + ], + ]; + + $element['filefield_clipboard']['upload'] = [ + '#name' => implode('_', $element['#parents']) . '_clipboard_upload_button', + '#type' => 'submit', + '#value' => t('Upload'), + '#attributes' => ['class' => ['js-hide']], + '#validate' => [], + '#submit' => ['filefield_sources_field_submit'], + '#limit_validation_errors' => [$element['#parents']], + '#ajax' => $ajax_settings, + ]; + + return $element; + } + + /** + * Theme the output of the clipboard element. + */ + public static function element($variables) { + $element = $variables['element']; + + return '
' . drupal_render_children($element) . '
'; + } + +} diff --git a/src/Plugin/FilefieldSource/Imce.php b/src/Plugin/FilefieldSource/Imce.php new file mode 100644 index 0000000..31e71a3 --- /dev/null +++ b/src/Plugin/FilefieldSource/Imce.php @@ -0,0 +1,361 @@ +getSettings(); + $scheme = $field_settings['uri_scheme']; + + $wrapper = \Drupal::service('stream_wrapper_manager')->getViaScheme($scheme); + $file_directory_prefix = $scheme == 'private' ? 'system/files' : $wrapper->getDirectoryPath(); + $uri = preg_replace('/^' . preg_quote(base_path() . $file_directory_prefix . '/', '/') . '/', $scheme . '://', $input['filefield_imce']['file_path']); + + // Resolve the file path to an FID. + $fid = db_select('file_managed', 'f') + ->condition('uri', rawurldecode($uri)) + ->fields('f', array('fid')) + ->execute() + ->fetchField(); + if ($fid) { + $file = file_load($fid); + if (filefield_sources_element_validate($element, $file)) { + if (!in_array($file->id(), $input['fids'])) { + $input['fids'][] = $file->id(); + } + } + } + else { + $form_state->setError($element, t('The selected file could not be used because the file does not exist in the database.')); + } + // No matter what happens, clear the value from the file path field. + $input['filefield_imce']['file_path'] = ''; + } + } + + /** + * {@inheritdoc} + */ + public static function process(array &$element, FormStateInterface $form_state, array &$complete_form) { + $instance = entity_load('field_config', $element['#entity_type'] . '.' . $element['#bundle'] . '.' . $element['#field_name']); + + $element['filefield_imce'] = array( + '#weight' => 100.5, + '#theme' => 'filefield_sources_element', + '#source_id' => 'imce', + // Required for proper theming. + '#filefield_source' => TRUE, + '#description' => filefield_sources_element_validation_help($element['#upload_validators']), + ); + + $filepath_id = $element['#id'] . '-imce-path'; + $display_id = $element['#id'] . '-imce-display'; + $select_id = $element['#id'] . '-imce-select'; + $element['filefield_imce']['file_path'] = array( + // IE doesn't support onchange events for hidden fields, so we use a + // textfield and hide it from display. + '#type' => 'textfield', + '#value' => '', + '#attributes' => array( + 'id' => $filepath_id, + 'onblur' => "if (this.value.length > 0) { jQuery('#$select_id').triggerHandler('mousedown'); }", + 'style' => 'position:absolute; left: -9999em', + ), + ); + + $imce_function = 'window.open(\'' . \Drupal::url('filefield_sources.imce', array( + 'entity_type' => $element['#entity_type'], + 'bundle' => $element['#bundle'], + 'field_name' => $element['#field_name'], + ), + array( + 'query' => array( + 'app' => $instance->getLabel() . '|url@' . $filepath_id, + ), + )) . '\', \'\', \'width=760,height=560,resizable=1\'); return false;'; + $element['filefield_imce']['display_path'] = array( + '#type' => 'markup', + '#markup' => '' . t('No file selected') . ' (' . t('browse') . ')', + ); + + $ajax_settings = [ + 'url' => Url::fromRoute('file.ajax_upload'), + 'options' => [ + 'query' => [ + 'element_parents' => implode('/', $element['#array_parents']), + 'form_build_id' => $complete_form['form_build_id']['#value'], + ], + ], + 'wrapper' => $element['#id'] . '-ajax-wrapper', + 'effect' => 'fade', + ]; + + $element['filefield_imce']['select'] = array( + '#name' => implode('_', $element['#parents']) . '_imce_select', + '#type' => 'submit', + '#value' => t('Select'), + '#validate' => [], + '#submit' => array('filefield_sources_field_submit'), + '#limit_validation_errors' => [$element['#parents']], + '#name' => $element['#name'] . '[filefield_imce][button]', + '#id' => $select_id, + '#attributes' => ['class' => ['js-hide']], + '#ajax' => $ajax_settings, + ); + + return $element; + } + + /** + * Theme the output of the imce element. + */ + public static function element($variables) { + $element = $variables['element']; + + $output = drupal_render_children($element); + return '
' . $output . '
'; + } + + /** + * Outputs the IMCE browser for FileField. + */ + public static function page($entity_type, $bundle_name, $field_name) { + global $conf; + + // Check access. + if (!\Drupal::moduleHandler()->moduleExists('imce') || !imce_access() || !$instance = entity_load('field_config', $entity_type . '.' . $bundle_name . '.' . $field_name)) { + throw new AccessDeniedHttpException(); + } + $settings = $instance->getSettings(); + + $widget = entity_get_form_display($entity_type, $bundle_name, 'default')->getComponent($field_name); + // Full mode. + if (!empty($widget['third_party_settings']['filefield_sources']['filefield_sources']['source_imce']['imce_mode'])) { + $conf['imce_custom_scan'] = array(get_called_class(), 'customScanFull'); + } + // Restricted mode. + else { + $conf['imce_custom_scan'] = array(get_called_class(), 'customScanRestricted'); + $conf['imce_custom_context'] = array( + 'field_storage' => entity_load('field_storage_config', $entity_type . '.' . $field_name), + 'uri' => static::getUploadLocation($settings), + ); + } + + // Disable absolute URLs. + $conf['imce_settings_absurls'] = 0; + + module_load_include('inc', 'imce', 'inc/imce.page'); + return imce($settings['uri_scheme']); + } + + /** + * Determines the URI for a file field. + * + * @param array $data + * An array of token objects to pass to token_replace(). + * + * @return string + * A file directory URI with tokens replaced. + * + * @see token_replace() + */ + public static function getUploadLocation($settings, $data = array()) { + $destination = trim($settings['file_directory'], '/'); + + // Replace tokens. + $destination = \Drupal::token()->replace($destination, $data); + + return $settings['uri_scheme'] . '://' . $destination; + } + + /** + * Scan and return files, subdirectories, and total size for "full" mode. + */ + protected static function customScanFull($dirname, &$imce) { + // Get a list of files in the database for this directory. + $scheme = $imce['scheme']; + $sql_uri_name = $dirname == '.' ? $scheme . '://' : $scheme . '://' . $dirname . '/'; + + $result = db_select('file_managed', 'f') + ->fields('f', array('uri')) + ->condition('f.uri', $sql_uri_name . '%', 'LIKE') + ->condition('f.uri', $sql_uri_name . '_%/%', 'NOT LIKE') + ->execute(); + + $db_files = array(); + foreach ($result as $row) { + $db_files[basename($row->uri)] = 1; + } + + // Get the default IMCE directory scan, then filter down to database files. + $directory = imce_scan_directory($dirname, $imce); + foreach ($directory['files'] as $filename => $file) { + if (!isset($db_files[$filename])) { + unset($directory['files'][$filename]); + $directory['dirsize'] -= $file['size']; + } + } + + return $directory; + } + + /** + * Scan directory and return file list, subdirectories, and total size. + * + * This only work on Restricted Mode. + */ + protected static function customScanRestricted($dirname, &$imce) { + $context = $GLOBALS['conf']['imce_custom_context']; + $field_storage = $context['field_storage']; + $root = $imce['scheme'] . '://'; + $field_uri = $context['uri']; + $is_root = $field_uri == $root; + + // Process IMCE. Make field directory the only accessible one. + $imce['dir'] = $is_root ? '.' : substr($field_uri, strlen($root)); + $imce['directories'] = array(); + if (!empty($imce['perm'])) { + static::disablePerms($imce, array('browse')); + } + + // Create directory info. + $directory = array( + 'dirsize' => 0, + 'files' => array(), + 'subdirectories' => array(), + 'error' => FALSE, + ); + + if (isset($field_storage['storage']['details']['sql']['FIELD_LOAD_CURRENT'])) { + $storage = $field_storage['storage']['details']['sql']['FIELD_LOAD_CURRENT']; + $table_info = reset($storage); + $table = key($storage); + $sql_uri = $field_uri . ($is_root ? '' : '/'); + $query = db_select($table, 'cf'); + $query->innerJoin('file_managed', 'f', 'f.fid = cf.' . $table_info['fid']); + $result = $query->fields('f') + ->condition('f.status', 1) + ->condition('f.uri', $sql_uri . '%', 'LIKE') + ->condition('f.uri', $sql_uri . '%/%', 'NOT LIKE') + ->execute(); + foreach ($result as $file) { + // Get real name. + $name = basename($file->uri); + // Get dimensions. + $width = $height = 0; + if ($img = imce_image_info($file->uri)) { + $width = $img['width']; + $height = $img['height']; + } + $directory['files'][$name] = array( + 'name' => $name, + 'size' => $file->filesize, + 'width' => $width, + 'height' => $height, + 'date' => $file->timestamp, + ); + $directory['dirsize'] += $file->filesize; + } + } + + return $directory; + } + + /** + * Disable IMCE profile permissions. + */ + protected static function disablePerms(&$imce, $exceptions = array()) { + $disable_all = empty($exceptions); + foreach ($imce['perm'] as $name => $val) { + if ($disable_all || !in_array($name, $exceptions)) { + $imce['perm'][$name] = 0; + } + } + $imce['directories'][$imce['dir']] = array('name' => $imce['dir']) + $imce['perm']; + } + + /** + * Define routes for Imce source. + * + * @return array + * Array of routes. + */ + public static function routes() { + $routes = array(); + + $routes['filefield_sources.imce'] = new Route( + '/file/imce/{entity_type}/{bundle_name}/{field_name}', + array( + '_controller' => get_called_class() . '::page', + ), + array( + '_access_filefield_sources_field' => 'TRUE', + ) + ); + + return $routes; + } + + /** + * Implements hook_filefield_source_settings(). + */ + public static function settings(WidgetInterface $plugin) { + $settings = $plugin->getThirdPartySetting('filefield_sources', 'filefield_sources', array( + 'source_imce' => array( + 'imce_mode' => 0, + ), + )); + + $return['source_imce'] = array( + '#title' => t('IMCE file browser settings'), + '#type' => 'details', + '#access' => \Drupal::moduleHandler()->moduleExists('imce'), + ); + + // $imce_admin_url = \Drupal::url('imce.admin'); + $imce_admin_url = 'admin/config/media/imce'; + $return['source_imce']['imce_mode'] = array( + '#type' => 'radios', + '#title' => t('File browser mode'), + '#options' => array( + 0 => t('Restricted: Users can only browse the field directory. No file operations are allowed.'), + 1 => t('Full: Browsable directories are defined by IMCE configuration profiles. File operations are allowed.', array('!imce-admin-url' => $imce_admin_url)), + ), + '#default_value' => isset($settings['source_imce']['imce_mode']) ? $settings['source_imce']['imce_mode'] : 0, + ); + + return $return; + + } + +} diff --git a/src/Plugin/FilefieldSource/Reference.php b/src/Plugin/FilefieldSource/Reference.php new file mode 100644 index 0000000..166b6a3 --- /dev/null +++ b/src/Plugin/FilefieldSource/Reference.php @@ -0,0 +1,214 @@ + 0 && $input['filefield_reference']['autocomplete'] != FILEFIELD_SOURCE_REFERENCE_HINT_TEXT) { + $matches = array(); + if (preg_match('/\[fid:(\d+)\]/', $input['filefield_reference']['autocomplete'], $matches)) { + $fid = $matches[1]; + if ($file = file_load($fid)) { + + // Remove file size restrictions, since the file already exists on + // disk. + if (isset($element['#upload_validators']['file_validate_size'])) { + unset($element['#upload_validators']['file_validate_size']); + } + + // Check that the user has access to this file through + // hook_download(). + if (!$file->access('download')) { + $form_state->setError($element, t('You do not have permission to use the selected file.')); + } + elseif (filefield_sources_element_validate($element, (object) $file, $form_state)) { + if (!in_array($file->id(), $input['fids'])) { + $input['fids'][] = $file->id(); + } + } + } + else { + $form_state->setError($element, t('The referenced file could not be used because the file does not exist in the database.')); + } + } + // No matter what happens, clear the value from the autocomplete. + $input['filefield_reference']['autocomplete'] = ''; + } + } + + /** + * {@inheritdoc} + */ + public static function process(array &$element, FormStateInterface $form_state, array &$complete_form) { + + $element['filefield_reference'] = array( + '#weight' => 100.5, + '#theme' => 'filefield_sources_element', + '#source_id' => 'reference', + // Required for proper theming. + '#filefield_source' => TRUE, + '#filefield_sources_hint_text' => FILEFIELD_SOURCE_REFERENCE_HINT_TEXT, + ); + + $autocomplete_route_parameters = array( + 'entity_type' => $element['#entity_type'], + 'bundle_name' => $element['#bundle'], + 'field_name' => $element['#field_name'], + ); + + $element['filefield_reference']['autocomplete'] = array( + '#type' => 'textfield', + '#autocomplete_route_name' => 'filefield_sources.autocomplete', + '#autocomplete_route_parameters' => $autocomplete_route_parameters, + '#description' => filefield_sources_element_validation_help($element['#upload_validators']), + ); + + $ajax_settings = [ + 'url' => Url::fromRoute('file.ajax_upload'), + 'options' => [ + 'query' => [ + 'element_parents' => implode('/', $element['#array_parents']), + 'form_build_id' => $complete_form['form_build_id']['#value'], + ], + ], + 'wrapper' => $element['#id'] . '-ajax-wrapper', + 'effect' => 'fade', + ]; + + $element['filefield_reference']['select'] = [ + '#name' => implode('_', $element['#parents']) . '_autocomplete_select', + '#type' => 'submit', + '#value' => t('Select'), + '#validate' => [], + '#submit' => ['filefield_sources_field_submit'], + '#limit_validation_errors' => [$element['#parents']], + '#ajax' => $ajax_settings, + ]; + + return $element; + } + + /** + * Theme the output of the reference element. + */ + public static function element($variables) { + $element = $variables['element']; + + $element['autocomplete']['#field_suffix'] = drupal_render($element['select']); + return '
' . drupal_render($element['autocomplete']) . '
'; + } + + /** + * Menu callback; autocomplete.js callback to return a list of files. + */ + public static function autocomplete(Request $request, $entity_type, $bundle_name, $field_name) { + $matches = array(); + $string = Unicode::strtolower($request->query->get('q')); + + $field_definition = entity_load('field_config', $entity_type . '.' . $bundle_name . '.' . $field_name); + $handler = \Drupal::getContainer()->get('plugin.manager.entity_reference_selection')->getSelectionHandler($field_definition); + + if (isset($string)) { + // Get an array of matching entities. + $widget = entity_get_form_display($entity_type, $bundle_name, 'default')->getComponent($field_name); + $autocomplete_type = $widget['third_party_settings']['filefield_sources']['filefield_sources']['source_reference']['autocomplete']; + $match_operator = !empty($autocomplete_type) ? $autocomplete_type : FILEFIELD_SOURCE_REFERENCE_CONTAINS_AUTOCOMPLETE_TYPE; + $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10); + + // Loop through the entities and convert them into autocomplete output. + foreach ($entity_labels as $values) { + foreach ($values as $entity_id => $label) { + $key = "$label [fid:$entity_id]"; + // Strip things like starting/trailing white spaces, line breaks and + // tags. + $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key))))); + // Names containing commas or quotes must be wrapped in quotes. + $matches[] = array('value' => $key, 'label' => $label); + } + } + } + + return new JsonResponse($matches); + } + + /** + * Define routes for Reference source. + * + * @return array + * Array of routes. + */ + public static function routes() { + $routes = array(); + + $routes['filefield_sources.autocomplete'] = new Route( + '/file/reference/{entity_type}/{bundle_name}/{field_name}', + array( + '_controller' => get_called_class() . '::autocomplete', + ), + array( + '_access_filefield_sources_field' => 'TRUE', + ) + ); + + return $routes; + } + + /** + * Implements hook_filefield_source_settings(). + */ + public static function settings(WidgetInterface $plugin) { + $settings = $plugin->getThirdPartySetting('filefield_sources', 'filefield_sources', array( + 'source_reference' => array( + 'autocomplete' => FILEFIELD_SOURCE_REFERENCE_STARTS_WITH_AUTOCOMPLETE_TYPE, + ), + )); + + $return['source_reference'] = array( + '#title' => t('Autocomplete reference options'), + '#type' => 'details', + ); + + $return['source_reference']['autocomplete'] = array( + '#title' => t('Match file name'), + '#options' => array( + FILEFIELD_SOURCE_REFERENCE_STARTS_WITH_AUTOCOMPLETE_TYPE => t('Starts with'), + FILEFIELD_SOURCE_REFERENCE_CONTAINS_AUTOCOMPLETE_TYPE => t('Contains'), + ), + '#type' => 'radios', + '#default_value' => isset($settings['source_reference']['autocomplete']) ? $settings['source_reference']['autocomplete'] : FILEFIELD_SOURCE_REFERENCE_STARTS_WITH_AUTOCOMPLETE_TYPE, + ); + + return $return; + } + +} diff --git a/src/Plugin/FilefieldSource/Remote.php b/src/Plugin/FilefieldSource/Remote.php new file mode 100644 index 0000000..260e701 --- /dev/null +++ b/src/Plugin/FilefieldSource/Remote.php @@ -0,0 +1,396 @@ + 0 && UrlHelper::isValid($input['filefield_remote']['url']) && $input['filefield_remote']['url'] != FILEFIELD_SOURCE_REMOTE_HINT_TEXT) { + $field = entity_load('field_config', $element['#entity_type'] . '.' . $element['#bundle'] . '.' . $element['#field_name']); + $url = $input['filefield_remote']['url']; + + // Check that the destination is writable. + $temporary_directory = 'temporary://'; + if (!file_prepare_directory($temporary_directory, FILE_MODIFY_PERMISSIONS)) { + \Drupal::logger('filefield_sources')->log(E_NOTICE, 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => drupal_realpath($temporary_directory))); + drupal_set_message(t('The file could not be transferred because the temporary directory is not writable.'), 'error'); + return; + } + + // Check that the destination is writable. + $directory = $element['#upload_location']; + $mode = Settings::get('file_chmod_directory', FILE_CHMOD_DIRECTORY); + + // This first chmod check is for other systems such as S3, which don't + // work with file_prepare_directory(). + if (!drupal_chmod($directory, $mode) && !file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) { + \Drupal::logger('filefield_sources')->log(E_NOTICE, 'File %file could not be copied, because the destination directory %destination is not configured correctly.', array('%file' => $url, '%destination' => drupal_realpath($directory))); + drupal_set_message(t('The specified file %file could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions. More information is available in the system log.', array('%file' => $url)), 'error'); + return; + } + + // Check the headers to make sure it exists and is within the allowed + // size. + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, TRUE); + curl_setopt($ch, CURLOPT_NOBODY, TRUE); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, array(get_called_class(), 'parseHeader')); + // Causes a warning if PHP safe mode is on. + @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE); + curl_exec($ch); + $info = curl_getinfo($ch); + if ($info['http_code'] != 200) { + curl_setopt($ch, CURLOPT_HTTPGET, TRUE); + $file_contents = curl_exec($ch); + $info = curl_getinfo($ch); + } + curl_close($ch); + + if ($info['http_code'] != 200) { + switch ($info['http_code']) { + case 403: + $form_state->setError($element, t('The remote file could not be transferred because access to the file was denied.')); + break; + + case 404: + $form_state->setError($element, t('The remote file could not be transferred because it was not found.')); + break; + + default: + $form_state->setError($element, t('The remote file could not be transferred due to an HTTP error (@code).', array('@code' => $info['http_code']))); + } + return; + } + + // Update the $url variable to reflect any redirects. + $url = $info['url']; + $url_info = parse_url($url); + + // Determine the proper filename by reading the filename given in the + // Content-Disposition header. If the server fails to send this header, + // fall back on the basename of the URL. + // + // We prefer to use the Content-Disposition header, because we can then + // use URLs like http://example.com/get_file/23 which would otherwise be + // rejected because the URL basename lacks an extension. + $filename = static::filename(); + if (empty($filename)) { + $filename = rawurldecode(basename($url_info['path'])); + } + + $pathinfo = pathinfo($filename); + + // Create the file extension from the MIME header if all else has failed. + if (empty($pathinfo['extension']) && $extension = static::mimeExtension()) { + $filename = $filename . '.' . $extension; + $pathinfo = pathinfo($filename); + } + + $filename = filefield_sources_clean_filename($filename, $field->settings['file_extensions']); + $filepath = file_create_filename($filename, $temporary_directory); + + if (empty($pathinfo['extension'])) { + $form_state->setError($element, t('The remote URL must be a file and have an extension.')); + return; + } + + // Perform basic extension check on the file before trying to transfer. + $extensions = $field->settings['file_extensions']; + $regex = '/\.(' . preg_replace('/[ +]/', '|', preg_quote($extensions)) . ')$/i'; + if (!empty($extensions) && !preg_match($regex, $filename)) { + $form_state->setError($element, t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions))); + return; + } + + // Check file size based off of header information. + if (!empty($element['#upload_validators']['file_validate_size'][0])) { + $max_size = $element['#upload_validators']['file_validate_size'][0]; + $file_size = $info['download_content_length']; + if ($file_size > $max_size) { + $form_state->setError($element, t('The remote file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file_size), '%maxsize' => format_size($max_size)))); + return; + } + } + + // Set progress bar information. + $options = array( + 'key' => $element['#entity_type'] . '_' . $element['#bundle'] . '_' . $element['#field_name'] . '_' . $element['#delta'], + 'filepath' => $filepath, + ); + static::setTransferOptions($options); + + $transfer_success = FALSE; + // If we've already downloaded the entire file because the + // header-retrieval failed, just ave the contents we have. + if (isset($file_contents)) { + if ($fp = @fopen($filepath, 'w')) { + fwrite($fp, $file_contents); + fclose($fp); + $transfer_success = TRUE; + } + } + // If we don't have the file contents, download the actual file. + else { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, FALSE); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, array(get_called_class(), 'curlWrite')); + // Causes a warning if PHP safe mode is on. + @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE); + $transfer_success = curl_exec($ch); + curl_close($ch); + } + if ($transfer_success && $file = filefield_sources_save_file($filepath, $element['#upload_validators'], $element['#upload_location'])) { + if (!in_array($file->id(), $input['fids'])) { + $input['fids'][] = $file->id(); + } + } + + // Delete the temporary file. + @unlink($filepath); + } + } + + /** + * Set a transfer key that can be retreived by the progress function. + */ + protected static function setTransferOptions($options = NULL) { + static $current = FALSE; + if (isset($options)) { + $current = $options; + } + return $current; + } + + /** + * Get a transfer key that can be retrieved by the progress function. + */ + protected static function getTransferOptions() { + return static::setTransferOptions(); + } + + /** + * Save the file to disk. Also updates progress bar. + */ + protected static function curlWrite(&$ch, $data) { + $progress_update = 0; + $options = static::getTransferOptions(); + + // Get the current progress and update the progress value. + // Only update every 64KB to reduce Drupal::cache()->set() calls. + // cURL usually writes in 16KB chunks. + if (curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) / 65536 > $progress_update) { + $progress_update++; + $progress = array( + 'current' => curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD), + 'total' => curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD), + ); + // Set a cache so that we can retrieve this value from the progress bar. + $cid = 'filefield_transfer:' . session_id() . ':' . $options['key']; + if ($progress['current'] != $progress['total']) { + \Drupal::cache()->set($cid, $progress, time() + 300); + } + else { + \Drupal::cache()->delete($cid); + } + } + + $data_length = 0; + if ($fp = @fopen($options['filepath'], 'a')) { + fwrite($fp, $data); + fclose($fp); + $data_length = strlen($data); + } + + return $data_length; + } + + /** + * Parse cURL header and record the filename specified in Content-Disposition. + */ + protected static function parseHeader(&$ch, $header) { + if (preg_match('/Content-Disposition:.*?filename="(.+?)"/', $header, $matches)) { + // Content-Disposition: attachment; filename="FILE NAME HERE" + static::filename($matches[1]); + } + elseif (preg_match('/Content-Disposition:.*?filename=([^; ]+)/', $header, $matches)) { + // Content-Disposition: attachment; filename=file.ext + $uri = trim($matches[1]); + static::filename($uri); + } + elseif (preg_match('/Content-Type:[ ]*([a-z0-9_\-]+\/[a-z0-9_\-]+)/i', $header, $matches)) { + $mime_type = $matches[1]; + static::mimeExtension($mime_type); + } + + // This is required by cURL. + return strlen($header); + } + + /** + * Get/set the remote file extension in a static variable. + */ + protected static function mimeExtension($curl_mime_type = NULL) { + static $extension = NULL; + $mimetype = Unicode::strtolower($curl_mime_type); + $result = \Drupal::service('file.mime_type.guesser.extension')->convertMimeTypeToMostCommonExtension($mimetype); + if ($result) { + $extension = $result; + } + return $extension; + } + + /** + * Get/set the remote file name in a static variable. + */ + protected static function filename($curl_filename = NULL) { + static $filename = NULL; + if (isset($curl_filename)) { + $filename = $curl_filename; + } + return $filename; + } + + /** + * {@inheritdoc} + */ + public static function process(array &$element, FormStateInterface $form_state, array &$complete_form) { + + $element['filefield_remote'] = array( + '#weight' => 100.5, + '#theme' => 'filefield_sources_element', + '#source_id' => 'remote', + // Required for proper theming. + '#filefield_source' => TRUE, + '#filefield_sources_hint_text' => FILEFIELD_SOURCE_REMOTE_HINT_TEXT, + ); + + $element['filefield_remote']['url'] = array( + '#type' => 'textfield', + '#description' => filefield_sources_element_validation_help($element['#upload_validators']), + '#maxlength' => NULL, + ); + + $ajax_settings = [ + 'url' => Url::fromRoute('file.ajax_upload'), + 'options' => [ + 'query' => [ + 'element_parents' => implode('/', $element['#array_parents']), + 'form_build_id' => $complete_form['form_build_id']['#value'], + ], + ], + 'wrapper' => $element['#id'] . '-ajax-wrapper', + 'effect' => 'fade', + 'progress' => [ + 'type' => 'bar', + 'path' => 'file/remote/progress/' . $element['#entity_type'] . '/' . $element['#bundle'] . '/' . $element['#field_name'] . '/' . $element['#delta'], + 'message' => t('Starting transfer...'), + ], + ]; + + $element['filefield_remote']['transfer'] = [ + '#name' => implode('_', $element['#parents']) . '_transfer', + '#type' => 'submit', + '#value' => t('Transfer'), + '#validate' => array(), + '#submit' => ['filefield_sources_field_submit'], + '#limit_validation_errors' => [$element['#parents']], + '#ajax' => $ajax_settings, + ]; + + return $element; + } + + /** + * Theme the output of the remote element. + */ + public static function element($variables) { + $element = $variables['element']; + + $element['url']['#field_suffix'] = drupal_render($element['transfer']); + return '
' . drupal_render($element['url']) . '
'; + } + + /** + * Menu callback; progress.js callback to return upload progress. + */ + public static function progress($entity_type, $bundle_name, $field_name, $delta) { + $key = $entity_type . '_' . $bundle_name . '_' . $field_name . '_' . $delta; + $progress = array( + 'message' => t('Starting transfer...'), + 'percentage' => -1, + ); + + if ($cache = \Drupal::cache()->get('filefield_transfer:' . session_id() . ':' . $key)) { + $current = $cache->data['current']; + $total = $cache->data['total']; + $progress['message'] = t('Transferring... (@current of @total)', array('@current' => format_size($current), '@total' => format_size($total))); + $progress['percentage'] = round(100 * $current / $total); + } + + return new JsonResponse($progress); + } + + /** + * Define routes for Remote source. + * + * @return array + * Array of routes. + */ + public static function routes() { + $routes = array(); + + $routes['filefield_sources.remote'] = new Route( + '/file/remote/progress/{entity_type}/{bundle_name}/{field_name}/{delta}', + array( + '_controller' => get_called_class() . '::progress', + ), + array( + '_access' => 'TRUE', + ) + ); + + return $routes; + } + + /** + * Implements hook_filefield_source_settings(). + */ + public static function settings(WidgetInterface $plugin) { + $return = array(); + + return $return; + + } + +} diff --git a/src/Routing/FilefieldSourcesRoutes.php b/src/Routing/FilefieldSourcesRoutes.php new file mode 100644 index 0000000..0530566 --- /dev/null +++ b/src/Routing/FilefieldSourcesRoutes.php @@ -0,0 +1,35 @@ +getDefinitions() as $definition) { + // Get routes defined by each plugin. + $callback = array($definition['class'], 'routes'); + if (is_callable($callback)) { + $routes = array_merge($routes, call_user_func($callback)); + } + } + + return $routes; + } + +} diff --git a/src/Tests/AttachSourceTest.php b/src/Tests/AttachSourceTest.php new file mode 100644 index 0000000..6f3afda --- /dev/null +++ b/src/Tests/AttachSourceTest.php @@ -0,0 +1,149 @@ +xpath('//select[@name=:name]/option[@value=:option]', array( + ':name' => $this->fieldName . '[0][filefield_attach][filename]', + ':option' => $uri, + )); + return isset($options[0]); + } + + /** + * Check to see if can attach file. + * + * @param object $file + * File to attach. + */ + public function assertCanAttachFile($file) { + // Ensure option is present. + $this->assertTrue($this->isOptionPresent($file->uri), 'File option is present.'); + + // Ensure empty message is not present. + $this->assertNoText('There currently are no files to attach.', "Empty message is not present."); + + // Attach button is always present. + $this->assertFieldByXpath('//input[@type="submit"]', t('Attach'), 'Attach button is present.'); + } + + /** + * Check to see if can attach file. + * + * @param object $file + * File to attach. + */ + public function assertCanNotAttachFile($file) { + // Ensure option is not present. + $this->assertFalse($this->isOptionPresent($file->uri), 'File option is not present.'); + + // Ensure empty message is present. + $this->assertText('There currently are no files to attach.', "Empty message is present."); + + // Attach button is always present. + $this->assertFieldByXpath('//input[@type="submit"]', t('Attach'), 'Attach button is present.'); + } + + /** + * Tests move file from relative path. + * + * Default settings: Move file from 'public://file_attach' to 'public://'. + */ + public function testMoveFileFromRelativePath() { + // Create test file. + $path = file_default_scheme() . '://' . FILEFIELD_SOURCE_ATTACH_DEFAULT_PATH; + $file = $this->createTemporaryFile($path); + $dest_uri = file_default_scheme() . '://' . $file->filename; + + $this->enableSources(array( + 'attach' => TRUE, + )); + + $this->assertCanAttachFile($file); + + // Upload a file. + $this->uploadFileByAttachSource($file->uri, $file->filename, 0); + + // We can only attach one file on single value field. + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Attach'), 'After uploading a file, "Attach" button is no longer displayed.'); + + // Ensure file is moved. + $this->assertFalse(is_file($file->uri), 'Source file has been removed.'); + $this->assertTrue(is_file($dest_uri), 'Destination file has been created.'); + + $this->removeFile($file->filename, 0); + + $this->assertCanNotAttachFile($file); + } + + /** + * Calculate custom absolute path. + */ + public function getCustomAttachPath() { + $path = drupal_realpath(file_default_scheme() . '://'); + $path = str_replace(realpath('./'), '', $path); + $path = ltrim($path, '/'); + $path = $path . '/custom_file_attach'; + return $path; + } + + /** + * Tests copy file from absolute path. + * + * Copy file from 'sites/default/files/custom_file_attach' to 'public://'. + */ + public function testCopyFileFromAbsolutePath() { + $path = $this->getCustomAttachPath(); + + // Create test file. + $file = $this->createTemporaryFile($path); + $dest_uri = file_default_scheme() . '://' . $file->filename; + + // Change settings. + $this->updateFilefieldSourcesSettings('source_attach', 'path', $path); + $this->updateFilefieldSourcesSettings('source_attach', 'absolute', FILEFIELD_SOURCE_ATTACH_ABSOLUTE); + $this->updateFilefieldSourcesSettings('source_attach', 'attach_mode', FILEFIELD_SOURCE_ATTACH_MODE_COPY); + + $this->enableSources(array( + 'attach' => TRUE, + )); + + $this->assertCanAttachFile($file); + + // Upload a file. + $this->uploadFileByAttachSource($file->uri, $file->filename, 0); + + // We can only attach one file on single value field. + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Attach'), 'After uploading a file, "Attach" button is no longer displayed.'); + + // Ensure file is copied. + $this->assertTrue(is_file($file->uri), 'Source file still exists.'); + $this->assertTrue(is_file($dest_uri), 'Destination file has been created.'); + + $this->removeFile($file->filename, 0); + + $this->assertCanAttachFile($file); + } + +} diff --git a/src/Tests/ClipboardSourceTest.php b/src/Tests/ClipboardSourceTest.php new file mode 100644 index 0000000..215883a --- /dev/null +++ b/src/Tests/ClipboardSourceTest.php @@ -0,0 +1,37 @@ +enableSources(array( + 'clipboard' => TRUE, + )); + $file = $this->createTemporaryFileEntity(); + + $this->uploadFileByClipboardSource($file->getFileUri(), $file->getFilename(), 0); + + // We can only upload one file on single value field. + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Upload'), t('After uploading a file, "Upload" button is no longer displayed.')); + + $this->removeFile($file->getFilename(), 0); + + // Can upload file again. + $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After clicking the "Remove" button, the "Upload" button is displayed.'); + } + +} diff --git a/src/Tests/EmptyValuesTest.php b/src/Tests/EmptyValuesTest.php new file mode 100644 index 0000000..317cb96 --- /dev/null +++ b/src/Tests/EmptyValuesTest.php @@ -0,0 +1,70 @@ +drupalPostForm('admin/structure/types/manage/' . $this->typeName . '/fields/node.' . $this->typeName . '.' . $this->fieldName . '/storage', array('cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED), t('Save field settings')); + + $this->enableSources(array( + 'upload' => TRUE, + 'remote' => TRUE, + 'clipboard' => TRUE, + 'reference' => TRUE, + 'attach' => TRUE, + )); + + // Upload a file by 'Remote' source. + $this->uploadFileByRemoteSource(); + + // Upload a file by 'Reference' source. + $this->uploadFileByReferenceSource(); + + // Upload a file by 'Clipboard' source. + $this->uploadFileByClipboardSource(); + + // Upload a file by 'Attach' source. + $this->uploadFileByAttachSource(); + + // Upload a file by 'Upload' source. + $this->uploadFileByUploadSource('', '', 0, TRUE); + + $this->assertUniqueSubmitButtons(); + } + + /** + * Check that there is only one submit button of a source. + */ + protected function assertUniqueSubmitButtons() { + $buttons = array( + $this->fieldName . '_0_attach' => t('Attach'), + $this->fieldName . '_0_clipboard_upload_button' => t('Upload'), + $this->fieldName . '_0_autocomplete_select' => t('Select'), + $this->fieldName . '_0_transfer' => t('Transfer'), + $this->fieldName . '_0_upload_button' => t('Upload'), + ); + foreach ($buttons as $button_name => $button_label) { + // Ensure that there is only one button with name. + $buttons = $this->xpath('//input[@name="' . $button_name . '" and @value="' . $button_label . '"]'); + $this->assertEqual(count($buttons), 1, format_string('There is only one button with name %name and label %label', array('%name' => $button_name, '%label' => $button_label))); + } + } + +} diff --git a/src/Tests/FileFieldSourcesTestBase.php b/src/Tests/FileFieldSourcesTestBase.php new file mode 100644 index 0000000..abe0eb4 --- /dev/null +++ b/src/Tests/FileFieldSourcesTestBase.php @@ -0,0 +1,395 @@ +adminUser = $this->drupalCreateUser(array( + 'access content', + 'access administration pages', + 'administer site configuration', + 'administer users', + 'administer permissions', + 'administer content types', + 'administer node fields', + 'administer node display', + 'administer node form display', + 'administer nodes', + 'bypass node access', + )); + $this->drupalLogin($this->adminUser); + + // Create content type. + $this->typeName = 'article'; + $this->drupalCreateContentType(array('type' => $this->typeName, 'name' => 'Article')); + + // Add node. + $this->node = $this->drupalCreateNode(); + + // Add file field. + $this->fieldName = strtolower($this->randomMachineName()); + $this->createFileField($this->fieldName, 'node', $this->typeName); + } + + /** + * Enable file field sources. + * + * @param array $sources + * List of sources to enable or disable. e.g + * array( + * 'upload' => FALSE, + * 'remote' => TRUE, + * ). + */ + public function enableSources($sources = array()) { + $sources += array('upload' => TRUE); + $map = array( + 'upload' => 'Upload', + 'remote' => 'Remote URL', + 'clipboard' => 'Clipboard', + 'reference' => 'Reference existing', + 'attach' => 'File attach', + ); + $sources = array_intersect_key($sources, $map); + ksort($sources); + + // Upload source enabled by default. + $manage_display = 'admin/structure/types/manage/' . $this->typeName . '/form-display'; + $this->drupalGet($manage_display); + $this->assertText("File field sources: upload", 'The expected summary is displayed.'); + + // Click on the widget settings button to open the widget settings form. + $this->drupalPostAjaxForm(NULL, array(), $this->fieldName . "_settings_edit"); + + // Enable sources. + $prefix = 'fields[' . $this->fieldName . '][settings_edit_form][third_party_settings][filefield_sources][filefield_sources][sources]'; + $edit = array(); + foreach ($sources as $source => $enabled) { + $edit[$prefix . '[' . $source . ']'] = $enabled ? TRUE : FALSE; + } + $this->drupalPostAjaxForm(NULL, $edit, array($this->fieldName . '_plugin_settings_update' => t('Update'))); + $this->assertText("File field sources: " . implode(', ', array_keys($sources)), 'The expected summary is displayed.'); + + // Save the form to save the third party settings. + $this->drupalPostForm(NULL, array(), t('Save')); + + $add_node = 'node/add/' . $this->typeName; + $this->drupalGet($add_node); + if (count($sources) > 1) { + // We can swith between sources. + foreach ($sources as $source => $enabled) { + $label = $map[$source]; + $this->assertLink($label); + } + } + else { + foreach ($map as $source => $label) { + $this->assertNoLink($label); + } + } + } + + /** + * Create permanent file entity. + * + * @return object + * Permanent file entity. + */ + public function createPermanentFileEntity() { + $file = $this->createTemporaryFileEntity(); + // Only permanent file can be referred. + $file->status = FILE_STATUS_PERMANENT; + // Author has permission to access file. + $file->uid = $this->adminUser->id(); + $file->save(); + + // Permanent file must be used by an entity. + \Drupal::service('file.usage')->add($file, 'file', 'node', $this->node->id()); + + return $file; + } + + /** + * Create temporary file entity. + * + * @return object + * Temporary file entity. + */ + public function createTemporaryFileEntity() { + $file = $this->createTemporaryFile(); + + // Add a filesize property to files as would be read by file_load(). + $file->filesize = filesize($file->uri); + + return entity_create('file', (array) $file); + } + + /** + * Create temporary file. + * + * @return object + * Permanent file object. + */ + public function createTemporaryFile($path = '') { + $filename = $this->randomMachineName() . '.txt'; + if (empty($path)) { + $path = file_default_scheme() . '://'; + } + $uri = $path . '/' . $filename; + $contents = $this->randomString(); + + // Change mode so that we can create files. + file_prepare_directory($path, FILE_CREATE_DIRECTORY); + drupal_chmod($path, FILE_CHMOD_DIRECTORY); + + file_put_contents($uri, $contents); + $this->assertTrue(is_file($uri), 'The temporary file has been created.'); + + // Change mode so that we can delete created file. + drupal_chmod($uri, FILE_CHMOD_FILE); + + // Return object similar to file_scan_directory(). + $file = new \stdClass(); + $file->uri = $uri; + $file->filename = $filename; + $file->name = pathinfo($filename, PATHINFO_FILENAME); + return $file; + } + + /** + * Update file field sources settings. + * + * @param string $source_key + * Wrapper, defined by each source. + * @param string $key + * Key, defined by each source. + * @param mixed $value + * Value to set. + */ + public function updateFilefieldSourcesSettings($source_key, $key, $value) { + $manage_display = 'admin/structure/types/manage/' . $this->typeName . '/form-display'; + $this->drupalGet($manage_display); + + // Click on the widget settings button to open the widget settings form. + $this->drupalPostAjaxForm(NULL, array(), $this->fieldName . "_settings_edit"); + + // Update settings. + $name = 'fields[' . $this->fieldName . '][settings_edit_form][third_party_settings][filefield_sources][filefield_sources]' . "[$source_key][$key]"; + $edit = array($name => $value); + $this->drupalPostAjaxForm(NULL, $edit, array($this->fieldName . '_plugin_settings_update' => t('Update'))); + + // Save the form to save the third party settings. + $this->drupalPostForm(NULL, array(), t('Save')); + } + + /** + * Upload file by 'Attach' source. + * + * @param string $uri + * File uri. + * @param string $filename + * File name. + * @param int $delta + * Delta in multiple values field. + */ + public function uploadFileByAttachSource($uri = '', $filename = '', $delta = 0) { + if ($uri) { + $edit = array( + $this->fieldName . '[' . $delta . '][filefield_attach][filename]' => $uri, + ); + } + else { + $edit = array(); + } + $this->drupalPostAjaxForm(NULL, $edit, array($this->fieldName . '_' . $delta . '_attach' => t('Attach'))); + + if ($filename) { + $this->assertFileUploaded($filename, $delta); + } + else { + $this->assertFileNotUploaded($delta); + } + } + + /** + * Upload file by 'Reference' source. + * + * @param int $fid + * File id. + * @param string $filename + * File name. + * @param int $delta + * Delta in multiple values field. + */ + public function uploadFileByReferenceSource($fid = 0, $filename = '', $delta = 0) { + $name = $this->fieldName . '[' . $delta . '][filefield_reference][autocomplete]'; + $value = $fid ? $filename . ' [fid:' . $fid . ']' : ''; + $edit = array($name => $value); + $this->drupalPostAjaxForm(NULL, $edit, array($this->fieldName . '_' . $delta . '_autocomplete_select' => t('Select'))); + + if ($filename) { + $this->assertFileUploaded($filename, $delta); + } + else { + $this->assertFileNotUploaded($delta); + } + } + + /** + * Upload file by 'Clipboard' source. + * + * @param string $uri + * File uri. + * @param string $filename + * File name. + * @param int $delta + * Delta in multiple values field. + */ + public function uploadFileByClipboardSource($uri = '', $filename = '', $delta = 0) { + $prefix = $this->fieldName . '[' . $delta . '][filefield_clipboard]'; + $file_content = $uri ? 'data:text/plain;base64,' . base64_encode(file_get_contents($uri)) : ''; + $edit = array( + $prefix . '[filename]' => $filename, + $prefix . '[contents]' => $file_content, + ); + $this->drupalPostAjaxForm(NULL, $edit, array($this->fieldName . '_' . $delta . '_clipboard_upload_button' => t('Upload'))); + + if ($filename) { + $this->assertFileUploaded($filename, $delta); + } + else { + $this->assertFileNotUploaded($delta); + } + } + + /** + * Upload file by 'Remote' source. + * + * @param string $url + * File url. + * @param string $filename + * File name. + * @param int $delta + * Delta in multiple values field. + */ + public function uploadFileByRemoteSource($url = '', $filename = '', $delta = 0) { + $name = $this->fieldName . '[' . $delta . '][filefield_remote][url]'; + $edit = array($name => $url); + $this->drupalPostAjaxForm(NULL, $edit, array($this->fieldName . '_' . $delta . '_transfer' => t('Transfer'))); + + if ($filename) { + $this->assertFileUploaded($filename, $delta); + } + else { + $this->assertFileNotUploaded($delta); + } + } + + /** + * Upload file by 'Upload' source. + * + * @param string $uri + * File uri. + * @param string $filename + * File name. + * @param int $delta + * Delta in multiple values field. + */ + public function uploadFileByUploadSource($uri = '', $filename = '', $delta = 0, $multiple = FALSE) { + $name = 'files[' . $this->fieldName . '_' . $delta . ']'; + if ($multiple) { + $name .= '[]'; + } + $edit = array( + $name => $uri ? drupal_realpath($uri) : '', + ); + $this->drupalPostAjaxForm(NULL, $edit, array($this->fieldName . '_' . $delta . '_upload_button' => t('Upload'))); + + if ($filename) { + $this->assertFileUploaded($filename, $delta); + } + else { + $this->assertFileNotUploaded($delta); + } + } + + /** + * Check to see if file is uploaded. + * + * @param string $filename + * File name. + * @param int $delta + * Delta in multiple values field. + */ + public function assertFileUploaded($filename, $delta = 0) { + $this->assertLink($filename); + $this->assertFieldByXPath('//input[@name="' . $this->fieldName . '_' . $delta . '_remove_button"]', t('Remove'), 'After uploading a file, "Remove" button is displayed.'); + } + + /** + * Check to see if file is not uploaded. + * + * @param int $delta + * Delta in multiple values field. + */ + public function assertFileNotUploaded($delta = 0) { + $this->assertNoFieldByXPath('//input[@name="' . $this->fieldName . '_' . $delta . '_remove_button"]', t('Remove'), '"Remove" button is not displayed.'); + } + + /** + * Remove uploaded file. + * + * @param string $filename + * File name. + * @param int $delta + * Delta in multiple values field. + */ + public function removeFile($filename, $delta = 0) { + $this->drupalPostAjaxForm(NULL, array(), array($this->fieldName . '_' . $delta . '_remove_button' => t('Remove'))); + + // Ensure file is removed. + $this->assertFileRemoved($filename); + } + + /** + * Check to see if file is removed. + * + * @param string $filename + * File name. + */ + public function assertFileRemoved($filename) { + $this->assertNoLink($filename); + } + +} diff --git a/src/Tests/MultipleValuesTest.php b/src/Tests/MultipleValuesTest.php new file mode 100644 index 0000000..f388bdf --- /dev/null +++ b/src/Tests/MultipleValuesTest.php @@ -0,0 +1,117 @@ +permanent_file_entity = $this->createPermanentFileEntity(); + $this->temporary_file_entity_1 = $this->createTemporaryFileEntity(); + $this->temporary_file_entity_2 = $this->createTemporaryFileEntity(); + + $path = file_default_scheme() . '://' . FILEFIELD_SOURCE_ATTACH_DEFAULT_PATH; + $this->temporary_file = $this->createTemporaryFile($path); + + // Change allowed number of values. + $this->drupalPostForm('admin/structure/types/manage/' . $this->typeName . '/fields/node.' . $this->typeName . '.' . $this->fieldName . '/storage', array('cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED), t('Save field settings')); + + $this->enableSources(array( + 'upload' => TRUE, + 'remote' => TRUE, + 'clipboard' => TRUE, + 'reference' => TRUE, + 'attach' => TRUE, + )); + } + + /** + * Tests uploading then removing files. + */ + public function testUploadThenRemoveFiles() { + $this->uploadFiles(); + + // Remove all uploaded files. + $this->removeFile($this->temporary_file_entity_2->getFilename(), 4); + $this->removeFile('INSTALL.txt', 0); + $this->removeFile($this->temporary_file_entity_1->getFilename(), 1); + $this->removeFile($this->temporary_file->filename, 1); + $this->removeFile($this->permanent_file_entity->getFilename(), 0); + + // Ensure all files have been removed. + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), 'All files have been removed.'); + } + + /** + * Tests uploading files and saving node. + */ + public function testUploadFilesThenSaveNode() { + $this->uploadFiles(); + + $this->drupalPostForm(NULL, array('title[0][value]' => $this->randomMachineName()), t('Save and publish')); + + // Ensure all files are saved to node. + $this->assertLink('INSTALL.txt'); + $this->assertLink($this->permanent_file_entity->getFilename()); + $this->assertLink($this->temporary_file_entity_1->getFilename()); + $this->assertLink($this->temporary_file_entity_2->getFilename()); + $this->assertLink($this->temporary_file->filename); + } + + /** + * Upload files. + * + * @return int + * Number of files uploaded. + */ + protected function uploadFiles() { + $uploaded_files = 0; + + // Ensure no files has been uploaded. + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), 'There are no file have been uploaded.'); + + // Upload a file by 'Remote' source. + $this->uploadFileByRemoteSource($GLOBALS['base_url'] . '/core/INSTALL.txt', 'INSTALL.txt', $uploaded_files); + $uploaded_files++; + + // Upload a file by 'Reference' source. + $this->uploadFileByReferenceSource($this->permanent_file_entity->id(), $this->permanent_file_entity->getFilename(), $uploaded_files); + $uploaded_files++; + + // Upload a file by 'Clipboard' source. + $this->uploadFileByClipboardSource($this->temporary_file_entity_1->getFileUri(), $this->temporary_file_entity_1->getFileName(), $uploaded_files); + $uploaded_files++; + + // Upload a file by 'Attach' source. + $this->uploadFileByAttachSource($this->temporary_file->uri, $this->temporary_file->filename, $uploaded_files); + $uploaded_files++; + + // Upload a file by 'Upload' source. + $this->uploadFileByUploadSource($this->temporary_file_entity_2->getFileUri(), $this->temporary_file_entity_2->getFilename(), $uploaded_files, TRUE); + $uploaded_files++; + + // Ensure files have been uploaded. + $remove_buttons = $this->xpath('//input[@type="submit" and @value="' . t('Remove') . '"]'); + $this->assertEqual(count($remove_buttons), $uploaded_files, "There are $uploaded_files files have been uploaded."); + + return $uploaded_files; + } + +} diff --git a/src/Tests/ReferenceSourceTest.php b/src/Tests/ReferenceSourceTest.php new file mode 100644 index 0000000..af5eb48 --- /dev/null +++ b/src/Tests/ReferenceSourceTest.php @@ -0,0 +1,109 @@ +createPermanentFileEntity(); + + $this->enableSources(array( + 'reference' => TRUE, + )); + + // Upload a file by 'Reference' source. + $this->uploadFileByReferenceSource($file->id(), $file->getFilename(), 0); + + // We can only refer one file on single value field. + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Select'), t('After uploading a file, "Select" button is no longer displayed.')); + + // Remove uploaded file. + $this->removeFile($file->getFileName(), 0); + + // Can select file again. + $this->assertFieldByXpath('//input[@type="submit"]', t('Select'), 'After clicking the "Remove" button, the "Select" button is displayed.'); + } + + /** + * Test autocompletion. + */ + public function testAutocompletion() { + // Create test file. + $file = $this->createPermanentFileEntity(); + $filename = $file->getFileName(); + $first_character = substr($filename, 0, 1); + $second_character = substr($filename, 1, 1); + + // Switch to 'Starts with' match type. + $this->updateFilefieldSourcesSettings('source_reference', 'autocomplete', 'STARTS_WITH'); + + // STARTS_WITH: empty results. + $query = $this->findCharacterNotInString($first_character); + $autocomplete_result = $this->drupalGetJSON('file/reference/node/' . $this->typeName . '/' . $this->fieldName, array('query' => array('q' => $query))); + $this->assertEqual($autocomplete_result, array(), "No files that have name starts with '$query'"); + + // STARTS_WITH: not empty results. + $query = $first_character; + $autocomplete_result = $this->drupalGetJSON('file/reference/node/' . $this->typeName . '/' . $this->fieldName, array('query' => array('q' => $query))); + $this->assertEqual($autocomplete_result[0]['label'], $filename, 'Autocompletion return correct label.'); + $this->assertEqual($autocomplete_result[0]['value'], $filename . ' [fid:' . $file->id() . ']', 'Autocompletion return correct value.'); + + // Switch to 'Contains' match type. + $this->updateFilefieldSourcesSettings('source_reference', 'autocomplete', 'CONTAINS'); + + // CONTAINS: empty results. + $query = $this->findCharacterNotInString($filename); + $autocomplete_result = $this->drupalGetJSON('file/reference/node/' . $this->typeName . '/' . $this->fieldName, array('query' => array('q' => $query))); + $this->assertEqual($autocomplete_result, array(), "No files that have name contains '$query'"); + + // CONTAINS: not empty results. + $query = $second_character; + $autocomplete_result = $this->drupalGetJSON('file/reference/node/' . $this->typeName . '/' . $this->fieldName, array('query' => array('q' => $query))); + $this->assertEqual($autocomplete_result[0]['label'], $filename, 'Autocompletion return correct label.'); + $this->assertEqual($autocomplete_result[0]['value'], $filename . ' [fid:' . $file->id() . ']', 'Autocompletion return correct value.'); + } + + /** + * Find the first character that is not in string. + * + * Only find for lower case character. + * + * @param string $string + * String to check. + * + * @return string + * First character that is not in the string. + */ + protected function findCharacterNotInString($string) { + // Only check for lower case string. + $string = Unicode::strtolower($string); + + // Lower case characters and numbers generated by + // \Drupal\simpletest\TestBase::randomMachineName(). + $values = array_merge(range(97, 122), range(48, 57)); + foreach ($values as $value) { + $character = chr($value); + if (strpos($string, $character) === FALSE) { + return $character; + } + } + } + +} diff --git a/src/Tests/RemoteSourceTest.php b/src/Tests/RemoteSourceTest.php new file mode 100644 index 0000000..2cc5500 --- /dev/null +++ b/src/Tests/RemoteSourceTest.php @@ -0,0 +1,38 @@ +enableSources(array( + 'remote' => TRUE, + )); + + // Upload a file by 'Remote' source. + $this->uploadFileByRemoteSource($GLOBALS['base_url'] . '/README.txt', 'README.txt', 0); + + // We can only transfer one file on single value field. + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Transfer'), t('After uploading a file, "Transfer" button is no longer displayed.')); + + // Remove uploaded file. + $this->removeFile('README.txt', 0); + + // Can transfer file again. + $this->assertFieldByXpath('//input[@type="submit"]', t('Transfer'), 'After clicking the "Remove" button, the "Transfer" button is displayed.'); + } + +} diff --git a/src/Tests/UploadSourceTest.php b/src/Tests/UploadSourceTest.php new file mode 100644 index 0000000..01afc53 --- /dev/null +++ b/src/Tests/UploadSourceTest.php @@ -0,0 +1,62 @@ +enableSources(array( + 'upload' => TRUE, + )); + + $this->assertUploadSourceWorkProperly(); + } + + /** + * Tests all sources enabled. + */ + public function testAllSourcesEnabled() { + $this->enableSources(array( + 'upload' => TRUE, + 'remote' => TRUE, + 'clipboard' => TRUE, + 'reference' => TRUE, + 'attach' => TRUE, + )); + + $this->assertUploadSourceWorkProperly(); + } + + /** + * Tests upload source still working properly. + */ + protected function assertUploadSourceWorkProperly() { + $file = $this->createTemporaryFileEntity(); + + // Upload a file by 'Upload' source. + $this->uploadFileByUploadSource($file->getFileUri(), $file->getFilename(), 0, FALSE); + + // We can only upload one file on single value field. + $this->assertNoFieldByXPath('//input[@type="submit"]', t('Upload'), t('After uploading a file, "Upload" button is no longer displayed.')); + + // Remove uploaded file. + $this->removeFile($file->getFilename(), 0); + + // Can upload file again. + $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After clicking the "Remove" button, the "Upload" button is displayed.'); + } + +}