Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 244 |
| ManagedFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 7 |
3306 | |
0.00% |
0 / 244 |
| getInfo | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 26 |
|||
| valueCallback | |
0.00% |
0 / 1 |
552 | |
0.00% |
0 / 59 |
|||
| uploadAjaxCallback | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 17 |
|||
| processManagedFile | |
0.00% |
0 / 1 |
240 | |
0.00% |
0 / 105 |
|||
| preRenderManagedFile | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 11 |
|||
| validateManagedFile | |
0.00% |
0 / 1 |
132 | |
0.00% |
0 / 24 |
|||
| fileUsage | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
| <?php | |
| /** | |
| * @file | |
| * Contains \Drupal\file\Element\ManagedFile. | |
| */ | |
| namespace Drupal\file\Element; | |
| use Drupal\Component\Utility\NestedArray; | |
| use Drupal\Component\Utility\Html; | |
| use Drupal\Core\Ajax\AjaxResponse; | |
| use Drupal\Core\Ajax\ReplaceCommand; | |
| use Drupal\Core\Form\FormStateInterface; | |
| use Drupal\Core\Render\Element\FormElement; | |
| use Drupal\Core\Url; | |
| use Drupal\file\Entity\File; | |
| use Symfony\Component\HttpFoundation\Request; | |
| /** | |
| * Provides an AJAX/progress aware widget for uploading and saving a file. | |
| * | |
| * @FormElement("managed_file") | |
| */ | |
| class ManagedFile extends FormElement { | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getInfo() { | |
| $class = get_class($this); | |
| return [ | |
| '#input' => TRUE, | |
| '#process' => [ | |
| [$class, 'processManagedFile'], | |
| ], | |
| '#element_validate' => [ | |
| [$class, 'validateManagedFile'], | |
| ], | |
| '#pre_render' => [ | |
| [$class, 'preRenderManagedFile'], | |
| ], | |
| '#theme' => 'file_managed_file', | |
| '#theme_wrappers' => ['form_element'], | |
| '#progress_indicator' => 'throbber', | |
| '#progress_message' => NULL, | |
| '#upload_validators' => [], | |
| '#upload_location' => NULL, | |
| '#size' => 22, | |
| '#multiple' => FALSE, | |
| '#extended' => FALSE, | |
| '#attached' => [ | |
| 'library' => ['file/drupal.file'], | |
| ], | |
| ]; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public static function valueCallback(&$element, $input, FormStateInterface $form_state) { | |
| // Find the current value of this field. | |
| $fids = !empty($input['fids']) ? explode(' ', $input['fids']) : []; | |
| foreach ($fids as $key => $fid) { | |
| $fids[$key] = (int) $fid; | |
| } | |
| $force_default = FALSE; | |
| // Process any input and save new uploads. | |
| if ($input !== FALSE) { | |
| $input['fids'] = $fids; | |
| $return = $input; | |
| // Uploads take priority over all other values. | |
| if ($files = file_managed_file_save_upload($element, $form_state)) { | |
| if ($element['#multiple']) { | |
| $fids = array_merge($fids, array_keys($files)); | |
| } | |
| else { | |
| $fids = array_keys($files); | |
| } | |
| } | |
| else { | |
| // Check for #filefield_value_callback values. | |
| // Because FAPI does not allow multiple #value_callback values like it | |
| // does for #element_validate and #process, this fills the missing | |
| // functionality to allow File fields to be extended through FAPI. | |
| if (isset($element['#file_value_callbacks'])) { | |
| foreach ($element['#file_value_callbacks'] as $callback) { | |
| $callback($element, $input, $form_state); | |
| } | |
| } | |
| // Load files if the FIDs have changed to confirm they exist. | |
| if (!empty($input['fids'])) { | |
| $fids = []; | |
| foreach ($input['fids'] as $fid) { | |
| if ($file = File::load($fid)) { | |
| // Temporary files that belong to other users should never be | |
| // allowed. Since file ownership can't be determined for anonymous | |
| // users, they are not allowed to reuse temporary files at all. | |
| if ($file->isTemporary() && (\Drupal::currentUser()->isAnonymous() || $file->getOwnerId() != \Drupal::currentUser()->id())) { | |
| $force_default = TRUE; | |
| break; | |
| } | |
| // If all checks pass, allow the files to be changed. | |
| else { | |
| $fids[] = $file->id(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // If there is no input or if the default value was requested above, use the | |
| // default value. | |
| if ($input === FALSE || $force_default) { | |
| if ($element['#extended']) { | |
| $default_fids = isset($element['#default_value']['fids']) ? $element['#default_value']['fids'] : []; | |
| $return = isset($element['#default_value']) ? $element['#default_value'] : ['fids' => []]; | |
| } | |
| else { | |
| $default_fids = isset($element['#default_value']) ? $element['#default_value'] : []; | |
| $return = ['fids' => []]; | |
| } | |
| // Confirm that the file exists when used as a default value. | |
| if (!empty($default_fids)) { | |
| $fids = []; | |
| foreach ($default_fids as $fid) { | |
| if ($file = File::load($fid)) { | |
| $fids[] = $file->id(); | |
| } | |
| } | |
| } | |
| } | |
| $return['fids'] = $fids; | |
| return $return; | |
| } | |
| /** | |
| * #ajax callback for managed_file upload forms. | |
| * | |
| * This ajax callback takes care of the following things: | |
| * - Ensures that broken requests due to too big files are caught. | |
| * - Adds a class to the response to be able to highlight in the UI, that a | |
| * new file got uploaded. | |
| * | |
| * @param array $form | |
| * The build form. | |
| * @param \Drupal\Core\Form\FormStateInterface $form_state | |
| * The form state. | |
| * @param \Symfony\Component\HttpFoundation\Request $request | |
| * The current request. | |
| * | |
| * @return \Drupal\Core\Ajax\AjaxResponse | |
| * The ajax response of the ajax upload. | |
| */ | |
| public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) { | |
| /** @var \Drupal\Core\Render\RendererInterface $renderer */ | |
| $renderer = \Drupal::service('renderer'); | |
| $form_parents = explode('/', $request->query->get('element_parents')); | |
| // Retrieve the element to be rendered. | |
| $form = NestedArray::getValue($form, $form_parents); | |
| // Add the special AJAX class if a new file was added. | |
| $current_file_count = $form_state->get('file_upload_delta_initial'); | |
| if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) { | |
| $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content'; | |
| } | |
| // Otherwise just add the new content class on a placeholder. | |
| else { | |
| $form['#suffix'] .= '<span class="ajax-new-content"></span>'; | |
| } | |
| $status_messages = ['#type' => 'status_messages']; | |
| $form['#prefix'] .= $renderer->renderRoot($status_messages); | |
| $output = $renderer->renderRoot($form); | |
| $response = new AjaxResponse(); | |
| $response->setAttachments($form['#attached']); | |
| return $response->addCommand(new ReplaceCommand(NULL, $output)); | |
| } | |
| /** | |
| * Render API callback: Expands the managed_file element type. | |
| * | |
| * Expands the file type to include Upload and Remove buttons, as well as | |
| * support for a default value. | |
| */ | |
| public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) { | |
| // This is used sometimes so let's implode it just once. | |
| $parents_prefix = implode('_', $element['#parents']); | |
| $fids = isset($element['#value']['fids']) ? $element['#value']['fids'] : []; | |
| // Set some default element properties. | |
| $element['#progress_indicator'] = empty($element['#progress_indicator']) ? 'none' : $element['#progress_indicator']; | |
| $element['#files'] = !empty($fids) ? File::loadMultiple($fids) : FALSE; | |
| $element['#tree'] = TRUE; | |
| // Generate a unique wrapper HTML ID. | |
| $ajax_wrapper_id = Html::getUniqueId('ajax-wrapper'); | |
| $ajax_settings = [ | |
| 'callback' => [get_called_class(), 'uploadAjaxCallback'], | |
| 'options' => [ | |
| 'query' => [ | |
| 'element_parents' => implode('/', $element['#array_parents']), | |
| ], | |
| ], | |
| 'wrapper' => $ajax_wrapper_id, | |
| 'effect' => 'fade', | |
| 'progress' => [ | |
| 'type' => $element['#progress_indicator'], | |
| 'message' => $element['#progress_message'], | |
| ], | |
| ]; | |
| // Set up the buttons first since we need to check if they were clicked. | |
| $element['upload_button'] = [ | |
| '#name' => $parents_prefix . '_upload_button', | |
| '#type' => 'submit', | |
| '#value' => t('Upload'), | |
| '#attributes' => ['class' => ['js-hide']], | |
| '#validate' => [], | |
| '#submit' => ['file_managed_file_submit'], | |
| '#limit_validation_errors' => [$element['#parents']], | |
| '#ajax' => $ajax_settings, | |
| '#weight' => -5, | |
| ]; | |
| // Force the progress indicator for the remove button to be either 'none' or | |
| // 'throbber', even if the upload button is using something else. | |
| $ajax_settings['progress']['type'] = ($element['#progress_indicator'] == 'none') ? 'none' : 'throbber'; | |
| $ajax_settings['progress']['message'] = NULL; | |
| $ajax_settings['effect'] = 'none'; | |
| $element['remove_button'] = [ | |
| '#name' => $parents_prefix . '_remove_button', | |
| '#type' => 'submit', | |
| '#value' => $element['#multiple'] ? t('Remove selected') : t('Remove'), | |
| '#validate' => [], | |
| '#submit' => ['file_managed_file_submit'], | |
| '#limit_validation_errors' => [$element['#parents']], | |
| '#ajax' => $ajax_settings, | |
| '#weight' => 1, | |
| ]; | |
| $element['fids'] = [ | |
| '#type' => 'hidden', | |
| '#value' => $fids, | |
| ]; | |
| // Add progress bar support to the upload if possible. | |
| if ($element['#progress_indicator'] == 'bar' && $implementation = file_progress_implementation()) { | |
| $upload_progress_key = mt_rand(); | |
| if ($implementation == 'uploadprogress') { | |
| $element['UPLOAD_IDENTIFIER'] = [ | |
| '#type' => 'hidden', | |
| '#value' => $upload_progress_key, | |
| '#attributes' => ['class' => ['file-progress']], | |
| // Uploadprogress extension requires this field to be at the top of | |
| // the form. | |
| '#weight' => -20, | |
| ]; | |
| } | |
| elseif ($implementation == 'apc') { | |
| $element['APC_UPLOAD_PROGRESS'] = [ | |
| '#type' => 'hidden', | |
| '#value' => $upload_progress_key, | |
| '#attributes' => ['class' => ['file-progress']], | |
| // Uploadprogress extension requires this field to be at the top of | |
| // the form. | |
| '#weight' => -20, | |
| ]; | |
| } | |
| // Add the upload progress callback. | |
| $element['upload_button']['#ajax']['progress']['url'] = Url::fromRoute('file.ajax_progress', ['key' => $upload_progress_key]); | |
| } | |
| // The file upload field itself. | |
| $element['upload'] = [ | |
| '#name' => 'files[' . $parents_prefix . ']', | |
| '#type' => 'file', | |
| '#title' => t('Choose a file'), | |
| '#title_display' => 'invisible', | |
| '#size' => $element['#size'], | |
| '#multiple' => $element['#multiple'], | |
| '#theme_wrappers' => [], | |
| '#weight' => -10, | |
| '#error_no_message' => TRUE, | |
| ]; | |
| if (!empty($fids) && $element['#files']) { | |
| foreach ($element['#files'] as $delta => $file) { | |
| $file_link = [ | |
| '#theme' => 'file_link', | |
| '#file' => $file, | |
| ]; | |
| if ($element['#multiple']) { | |
| $element['file_' . $delta]['selected'] = [ | |
| '#type' => 'checkbox', | |
| '#title' => \Drupal::service('renderer')->renderPlain($file_link), | |
| ]; | |
| } | |
| else { | |
| $element['file_' . $delta]['filename'] = $file_link + ['#weight' => -10]; | |
| } | |
| } | |
| } | |
| // Add the extension list to the page as JavaScript settings. | |
| if (isset($element['#upload_validators']['file_validate_extensions'][0])) { | |
| $extension_list = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0]))); | |
| $element['upload']['#attached']['drupalSettings']['file']['elements']['#' . $element['#id']] = $extension_list; | |
| } | |
| // Let #id point to the file element, so the field label's 'for' corresponds | |
| // with it. | |
| $element['#id'] = &$element['upload']['#id']; | |
| // Prefix and suffix used for Ajax replacement. | |
| $element['#prefix'] = '<div id="' . $ajax_wrapper_id . '">'; | |
| $element['#suffix'] = '</div>'; | |
| return $element; | |
| } | |
| /** | |
| * Render API callback: Hides display of the upload or remove controls. | |
| * | |
| * Upload controls are hidden when a file is already uploaded. Remove controls | |
| * are hidden when there is no file attached. Controls are hidden here instead | |
| * of in \Drupal\file\Element\ManagedFile::processManagedFile(), because | |
| * #access for these buttons depends on the managed_file element's #value. See | |
| * the documentation of \Drupal\Core\Form\FormBuilderInterface::doBuildForm() | |
| * for more detailed information about the relationship between #process, | |
| * #value, and #access. | |
| * | |
| * Because #access is set here, it affects display only and does not prevent | |
| * JavaScript or other untrusted code from submitting the form as though | |
| * access were enabled. The form processing functions for these elements | |
| * should not assume that the buttons can't be "clicked" just because they are | |
| * not displayed. | |
| * | |
| * @see \Drupal\file\Element\ManagedFile::processManagedFile() | |
| * @see \Drupal\Core\Form\FormBuilderInterface::doBuildForm() | |
| */ | |
| public static function preRenderManagedFile($element) { | |
| // If we already have a file, we don't want to show the upload controls. | |
| if (!empty($element['#value']['fids'])) { | |
| if (!$element['#multiple']) { | |
| $element['upload']['#access'] = FALSE; | |
| $element['upload_button']['#access'] = FALSE; | |
| } | |
| } | |
| // If we don't already have a file, there is nothing to remove. | |
| else { | |
| $element['remove_button']['#access'] = FALSE; | |
| } | |
| return $element; | |
| } | |
| /** | |
| * Render API callback: Validates the managed_file element. | |
| */ | |
| public static function validateManagedFile(&$element, FormStateInterface $form_state, &$complete_form) { | |
| // If referencing an existing file, only allow if there are existing | |
| // references. This prevents unmanaged files from being deleted if this | |
| // item were to be deleted. | |
| $clicked_button = end($form_state->getTriggeringElement()['#parents']); | |
| if ($clicked_button != 'remove_button' && !empty($element['fids']['#value'])) { | |
| $fids = $element['fids']['#value']; | |
| foreach ($fids as $fid) { | |
| if ($file = File::load($fid)) { | |
| if ($file->isPermanent()) { | |
| $references = static::fileUsage()->listUsage($file); | |
| if (empty($references)) { | |
| // We expect the field name placeholder value to be wrapped in t() | |
| // here, so it won't be escaped again as it's already marked safe. | |
| $form_state->setError($element, t('The file used in the @name field may not be referenced.', ['@name' => $element['#title']])); | |
| } | |
| } | |
| } | |
| else { | |
| // We expect the field name placeholder value to be wrapped in t() | |
| // here, so it won't be escaped again as it's already marked safe. | |
| $form_state->setError($element, t('The file referenced by the @name field does not exist.', ['@name' => $element['#title']])); | |
| } | |
| } | |
| } | |
| // Check required property based on the FID. | |
| if ($element['#required'] && empty($element['fids']['#value']) && !in_array($clicked_button, ['upload_button', 'remove_button'])) { | |
| // We expect the field name placeholder value to be wrapped in t() | |
| // here, so it won't be escaped again as it's already marked safe. | |
| $form_state->setError($element, t('@name field is required.', ['@name' => $element['#title']])); | |
| } | |
| // Consolidate the array value of this field to array of FIDs. | |
| if (!$element['#extended']) { | |
| $form_state->setValueForElement($element, $element['fids']['#value']); | |
| } | |
| } | |
| /** | |
| * Wraps the file usage service. | |
| * | |
| * @return \Drupal\file\FileUsage\FileUsageInterface | |
| */ | |
| protected static function fileUsage() { | |
| return \Drupal::service('file.usage'); | |
| } | |
| } |