To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / core / modules / file / src / Element / ManagedFile.php @ 15:e200cb7efeb3
History | View | Annotate | Download (17.3 KB)
| 1 |
<?php
|
|---|---|
| 2 |
|
| 3 |
namespace Drupal\file\Element; |
| 4 |
|
| 5 |
use Drupal\Component\Utility\Crypt; |
| 6 |
use Drupal\Component\Utility\Html; |
| 7 |
use Drupal\Component\Utility\NestedArray; |
| 8 |
use Drupal\Core\Ajax\AjaxResponse; |
| 9 |
use Drupal\Core\Ajax\ReplaceCommand; |
| 10 |
use Drupal\Core\Form\FormStateInterface; |
| 11 |
use Drupal\Core\Render\Element; |
| 12 |
use Drupal\Core\Render\Element\FormElement; |
| 13 |
use Drupal\Core\Site\Settings; |
| 14 |
use Drupal\Core\Url; |
| 15 |
use Drupal\file\Entity\File; |
| 16 |
use Symfony\Component\HttpFoundation\Request; |
| 17 |
|
| 18 |
/**
|
| 19 |
* Provides an AJAX/progress aware widget for uploading and saving a file.
|
| 20 |
*
|
| 21 |
* @FormElement("managed_file")
|
| 22 |
*/
|
| 23 |
class ManagedFile extends FormElement { |
| 24 |
|
| 25 |
/**
|
| 26 |
* {@inheritdoc}
|
| 27 |
*/
|
| 28 |
public function getInfo() { |
| 29 |
$class = get_class($this); |
| 30 |
return [
|
| 31 |
'#input' => TRUE, |
| 32 |
'#process' => [
|
| 33 |
[$class, 'processManagedFile'], |
| 34 |
], |
| 35 |
'#element_validate' => [
|
| 36 |
[$class, 'validateManagedFile'], |
| 37 |
], |
| 38 |
'#pre_render' => [
|
| 39 |
[$class, 'preRenderManagedFile'], |
| 40 |
], |
| 41 |
'#theme' => 'file_managed_file', |
| 42 |
'#theme_wrappers' => ['form_element'], |
| 43 |
'#progress_indicator' => 'throbber', |
| 44 |
'#progress_message' => NULL, |
| 45 |
'#upload_validators' => [],
|
| 46 |
'#upload_location' => NULL, |
| 47 |
'#size' => 22, |
| 48 |
'#multiple' => FALSE, |
| 49 |
'#extended' => FALSE, |
| 50 |
'#attached' => [
|
| 51 |
'library' => ['file/drupal.file'], |
| 52 |
], |
| 53 |
'#accept' => NULL, |
| 54 |
]; |
| 55 |
} |
| 56 |
|
| 57 |
/**
|
| 58 |
* {@inheritdoc}
|
| 59 |
*/
|
| 60 |
public static function valueCallback(&$element, $input, FormStateInterface $form_state) { |
| 61 |
// Find the current value of this field.
|
| 62 |
$fids = !empty($input['fids']) ? explode(' ', $input['fids']) : []; |
| 63 |
foreach ($fids as $key => $fid) { |
| 64 |
$fids[$key] = (int) $fid; |
| 65 |
} |
| 66 |
$force_default = FALSE; |
| 67 |
|
| 68 |
// Process any input and save new uploads.
|
| 69 |
if ($input !== FALSE) { |
| 70 |
$input['fids'] = $fids; |
| 71 |
$return = $input; |
| 72 |
|
| 73 |
// Uploads take priority over all other values.
|
| 74 |
if ($files = file_managed_file_save_upload($element, $form_state)) { |
| 75 |
if ($element['#multiple']) { |
| 76 |
$fids = array_merge($fids, array_keys($files)); |
| 77 |
} |
| 78 |
else {
|
| 79 |
$fids = array_keys($files); |
| 80 |
} |
| 81 |
} |
| 82 |
else {
|
| 83 |
// Check for #filefield_value_callback values.
|
| 84 |
// Because FAPI does not allow multiple #value_callback values like it
|
| 85 |
// does for #element_validate and #process, this fills the missing
|
| 86 |
// functionality to allow File fields to be extended through FAPI.
|
| 87 |
if (isset($element['#file_value_callbacks'])) { |
| 88 |
foreach ($element['#file_value_callbacks'] as $callback) { |
| 89 |
$callback($element, $input, $form_state); |
| 90 |
} |
| 91 |
} |
| 92 |
|
| 93 |
// Load files if the FIDs have changed to confirm they exist.
|
| 94 |
if (!empty($input['fids'])) { |
| 95 |
$fids = [];
|
| 96 |
foreach ($input['fids'] as $fid) { |
| 97 |
if ($file = File::load($fid)) { |
| 98 |
$fids[] = $file->id(); |
| 99 |
// Temporary files that belong to other users should never be
|
| 100 |
// allowed.
|
| 101 |
if ($file->isTemporary()) { |
| 102 |
if ($file->getOwnerId() != \Drupal::currentUser()->id()) { |
| 103 |
$force_default = TRUE; |
| 104 |
break;
|
| 105 |
} |
| 106 |
// Since file ownership can't be determined for anonymous users,
|
| 107 |
// they are not allowed to reuse temporary files at all. But
|
| 108 |
// they do need to be able to reuse their own files from earlier
|
| 109 |
// submissions of the same form, so to allow that, check for the
|
| 110 |
// token added by $this->processManagedFile().
|
| 111 |
elseif (\Drupal::currentUser()->isAnonymous()) { |
| 112 |
$token = NestedArray::getValue($form_state->getUserInput(), array_merge($element['#parents'], ['file_' . $file->id(), 'fid_token'])); |
| 113 |
if ($token !== Crypt::hmacBase64('file-' . $file->id(), \Drupal::service('private_key')->get() . Settings::getHashSalt())) { |
| 114 |
$force_default = TRUE; |
| 115 |
break;
|
| 116 |
} |
| 117 |
} |
| 118 |
} |
| 119 |
} |
| 120 |
} |
| 121 |
if ($force_default) { |
| 122 |
$fids = [];
|
| 123 |
} |
| 124 |
} |
| 125 |
} |
| 126 |
} |
| 127 |
|
| 128 |
// If there is no input or if the default value was requested above, use the
|
| 129 |
// default value.
|
| 130 |
if ($input === FALSE || $force_default) { |
| 131 |
if ($element['#extended']) { |
| 132 |
$default_fids = isset($element['#default_value']['fids']) ? $element['#default_value']['fids'] : []; |
| 133 |
$return = isset($element['#default_value']) ? $element['#default_value'] : ['fids' => []]; |
| 134 |
} |
| 135 |
else {
|
| 136 |
$default_fids = isset($element['#default_value']) ? $element['#default_value'] : []; |
| 137 |
$return = ['fids' => []]; |
| 138 |
} |
| 139 |
|
| 140 |
// Confirm that the file exists when used as a default value.
|
| 141 |
if (!empty($default_fids)) { |
| 142 |
$fids = [];
|
| 143 |
foreach ($default_fids as $fid) { |
| 144 |
if ($file = File::load($fid)) { |
| 145 |
$fids[] = $file->id(); |
| 146 |
} |
| 147 |
} |
| 148 |
} |
| 149 |
} |
| 150 |
|
| 151 |
$return['fids'] = $fids; |
| 152 |
return $return; |
| 153 |
} |
| 154 |
|
| 155 |
/**
|
| 156 |
* #ajax callback for managed_file upload forms.
|
| 157 |
*
|
| 158 |
* This ajax callback takes care of the following things:
|
| 159 |
* - Ensures that broken requests due to too big files are caught.
|
| 160 |
* - Adds a class to the response to be able to highlight in the UI, that a
|
| 161 |
* new file got uploaded.
|
| 162 |
*
|
| 163 |
* @param array $form
|
| 164 |
* The build form.
|
| 165 |
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
| 166 |
* The form state.
|
| 167 |
* @param \Symfony\Component\HttpFoundation\Request $request
|
| 168 |
* The current request.
|
| 169 |
*
|
| 170 |
* @return \Drupal\Core\Ajax\AjaxResponse
|
| 171 |
* The ajax response of the ajax upload.
|
| 172 |
*/
|
| 173 |
public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) { |
| 174 |
/** @var \Drupal\Core\Render\RendererInterface $renderer */
|
| 175 |
$renderer = \Drupal::service('renderer'); |
| 176 |
|
| 177 |
$form_parents = explode('/', $request->query->get('element_parents')); |
| 178 |
|
| 179 |
// Sanitize form parents before using them.
|
| 180 |
$form_parents = array_filter($form_parents, [Element::class, 'child']); |
| 181 |
|
| 182 |
// Retrieve the element to be rendered.
|
| 183 |
$form = NestedArray::getValue($form, $form_parents); |
| 184 |
|
| 185 |
// Add the special AJAX class if a new file was added.
|
| 186 |
$current_file_count = $form_state->get('file_upload_delta_initial'); |
| 187 |
if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) { |
| 188 |
$form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content'; |
| 189 |
} |
| 190 |
// Otherwise just add the new content class on a placeholder.
|
| 191 |
else {
|
| 192 |
$form['#suffix'] .= '<span class="ajax-new-content"></span>'; |
| 193 |
} |
| 194 |
|
| 195 |
$status_messages = ['#type' => 'status_messages']; |
| 196 |
$form['#prefix'] .= $renderer->renderRoot($status_messages); |
| 197 |
$output = $renderer->renderRoot($form); |
| 198 |
|
| 199 |
$response = new AjaxResponse(); |
| 200 |
$response->setAttachments($form['#attached']); |
| 201 |
|
| 202 |
return $response->addCommand(new ReplaceCommand(NULL, $output)); |
| 203 |
} |
| 204 |
|
| 205 |
/**
|
| 206 |
* Render API callback: Expands the managed_file element type.
|
| 207 |
*
|
| 208 |
* Expands the file type to include Upload and Remove buttons, as well as
|
| 209 |
* support for a default value.
|
| 210 |
*/
|
| 211 |
public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) { |
| 212 |
|
| 213 |
// This is used sometimes so let's implode it just once.
|
| 214 |
$parents_prefix = implode('_', $element['#parents']); |
| 215 |
|
| 216 |
$fids = isset($element['#value']['fids']) ? $element['#value']['fids'] : []; |
| 217 |
|
| 218 |
// Set some default element properties.
|
| 219 |
$element['#progress_indicator'] = empty($element['#progress_indicator']) ? 'none' : $element['#progress_indicator']; |
| 220 |
$element['#files'] = !empty($fids) ? File::loadMultiple($fids) : FALSE; |
| 221 |
$element['#tree'] = TRUE; |
| 222 |
|
| 223 |
// Generate a unique wrapper HTML ID.
|
| 224 |
$ajax_wrapper_id = Html::getUniqueId('ajax-wrapper'); |
| 225 |
|
| 226 |
$ajax_settings = [
|
| 227 |
'callback' => [get_called_class(), 'uploadAjaxCallback'], |
| 228 |
'options' => [
|
| 229 |
'query' => [
|
| 230 |
'element_parents' => implode('/', $element['#array_parents']), |
| 231 |
], |
| 232 |
], |
| 233 |
'wrapper' => $ajax_wrapper_id, |
| 234 |
'effect' => 'fade', |
| 235 |
'progress' => [
|
| 236 |
'type' => $element['#progress_indicator'], |
| 237 |
'message' => $element['#progress_message'], |
| 238 |
], |
| 239 |
]; |
| 240 |
|
| 241 |
// Set up the buttons first since we need to check if they were clicked.
|
| 242 |
$element['upload_button'] = [ |
| 243 |
'#name' => $parents_prefix . '_upload_button', |
| 244 |
'#type' => 'submit', |
| 245 |
'#value' => t('Upload'), |
| 246 |
'#attributes' => ['class' => ['js-hide']], |
| 247 |
'#validate' => [],
|
| 248 |
'#submit' => ['file_managed_file_submit'], |
| 249 |
'#limit_validation_errors' => [$element['#parents']], |
| 250 |
'#ajax' => $ajax_settings, |
| 251 |
'#weight' => -5, |
| 252 |
]; |
| 253 |
|
| 254 |
// Force the progress indicator for the remove button to be either 'none' or
|
| 255 |
// 'throbber', even if the upload button is using something else.
|
| 256 |
$ajax_settings['progress']['type'] = ($element['#progress_indicator'] == 'none') ? 'none' : 'throbber'; |
| 257 |
$ajax_settings['progress']['message'] = NULL; |
| 258 |
$ajax_settings['effect'] = 'none'; |
| 259 |
$element['remove_button'] = [ |
| 260 |
'#name' => $parents_prefix . '_remove_button', |
| 261 |
'#type' => 'submit', |
| 262 |
'#value' => $element['#multiple'] ? t('Remove selected') : t('Remove'), |
| 263 |
'#validate' => [],
|
| 264 |
'#submit' => ['file_managed_file_submit'], |
| 265 |
'#limit_validation_errors' => [$element['#parents']], |
| 266 |
'#ajax' => $ajax_settings, |
| 267 |
'#weight' => 1, |
| 268 |
]; |
| 269 |
|
| 270 |
$element['fids'] = [ |
| 271 |
'#type' => 'hidden', |
| 272 |
'#value' => $fids, |
| 273 |
]; |
| 274 |
|
| 275 |
// Add progress bar support to the upload if possible.
|
| 276 |
if ($element['#progress_indicator'] == 'bar' && $implementation = file_progress_implementation()) { |
| 277 |
$upload_progress_key = mt_rand();
|
| 278 |
|
| 279 |
if ($implementation == 'uploadprogress') { |
| 280 |
$element['UPLOAD_IDENTIFIER'] = [ |
| 281 |
'#type' => 'hidden', |
| 282 |
'#value' => $upload_progress_key, |
| 283 |
'#attributes' => ['class' => ['file-progress']], |
| 284 |
// Uploadprogress extension requires this field to be at the top of
|
| 285 |
// the form.
|
| 286 |
'#weight' => -20, |
| 287 |
]; |
| 288 |
} |
| 289 |
elseif ($implementation == 'apc') { |
| 290 |
$element['APC_UPLOAD_PROGRESS'] = [ |
| 291 |
'#type' => 'hidden', |
| 292 |
'#value' => $upload_progress_key, |
| 293 |
'#attributes' => ['class' => ['file-progress']], |
| 294 |
// Uploadprogress extension requires this field to be at the top of
|
| 295 |
// the form.
|
| 296 |
'#weight' => -20, |
| 297 |
]; |
| 298 |
} |
| 299 |
|
| 300 |
// Add the upload progress callback.
|
| 301 |
$element['upload_button']['#ajax']['progress']['url'] = Url::fromRoute('file.ajax_progress', ['key' => $upload_progress_key]); |
| 302 |
} |
| 303 |
|
| 304 |
// The file upload field itself.
|
| 305 |
$element['upload'] = [ |
| 306 |
'#name' => 'files[' . $parents_prefix . ']', |
| 307 |
'#type' => 'file', |
| 308 |
'#title' => t('Choose a file'), |
| 309 |
'#title_display' => 'invisible', |
| 310 |
'#size' => $element['#size'], |
| 311 |
'#multiple' => $element['#multiple'], |
| 312 |
'#theme_wrappers' => [],
|
| 313 |
'#weight' => -10, |
| 314 |
'#error_no_message' => TRUE, |
| 315 |
]; |
| 316 |
if (!empty($element['#accept'])) { |
| 317 |
$element['upload']['#attributes'] = ['accept' => $element['#accept']]; |
| 318 |
} |
| 319 |
|
| 320 |
if (!empty($fids) && $element['#files']) { |
| 321 |
foreach ($element['#files'] as $delta => $file) { |
| 322 |
$file_link = [
|
| 323 |
'#theme' => 'file_link', |
| 324 |
'#file' => $file, |
| 325 |
]; |
| 326 |
if ($element['#multiple']) { |
| 327 |
$element['file_' . $delta]['selected'] = [ |
| 328 |
'#type' => 'checkbox', |
| 329 |
'#title' => \Drupal::service('renderer')->renderPlain($file_link), |
| 330 |
]; |
| 331 |
} |
| 332 |
else {
|
| 333 |
$element['file_' . $delta]['filename'] = $file_link + ['#weight' => -10]; |
| 334 |
} |
| 335 |
// Anonymous users who have uploaded a temporary file need a
|
| 336 |
// non-session-based token added so $this->valueCallback() can check
|
| 337 |
// that they have permission to use this file on subsequent submissions
|
| 338 |
// of the same form (for example, after an Ajax upload or form
|
| 339 |
// validation error).
|
| 340 |
if ($file->isTemporary() && \Drupal::currentUser()->isAnonymous()) { |
| 341 |
$element['file_' . $delta]['fid_token'] = [ |
| 342 |
'#type' => 'hidden', |
| 343 |
'#value' => Crypt::hmacBase64('file-' . $delta, \Drupal::service('private_key')->get() . Settings::getHashSalt()), |
| 344 |
]; |
| 345 |
} |
| 346 |
} |
| 347 |
} |
| 348 |
|
| 349 |
// Add the extension list to the page as JavaScript settings.
|
| 350 |
if (isset($element['#upload_validators']['file_validate_extensions'][0])) { |
| 351 |
$extension_list = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0]))); |
| 352 |
$element['upload']['#attached']['drupalSettings']['file']['elements']['#' . $element['#id']] = $extension_list; |
| 353 |
} |
| 354 |
|
| 355 |
// Let #id point to the file element, so the field label's 'for' corresponds
|
| 356 |
// with it.
|
| 357 |
$element['#id'] = &$element['upload']['#id']; |
| 358 |
|
| 359 |
// Prefix and suffix used for Ajax replacement.
|
| 360 |
$element['#prefix'] = '<div id="' . $ajax_wrapper_id . '">'; |
| 361 |
$element['#suffix'] = '</div>'; |
| 362 |
|
| 363 |
return $element; |
| 364 |
} |
| 365 |
|
| 366 |
/**
|
| 367 |
* Render API callback: Hides display of the upload or remove controls.
|
| 368 |
*
|
| 369 |
* Upload controls are hidden when a file is already uploaded. Remove controls
|
| 370 |
* are hidden when there is no file attached. Controls are hidden here instead
|
| 371 |
* of in \Drupal\file\Element\ManagedFile::processManagedFile(), because
|
| 372 |
* #access for these buttons depends on the managed_file element's #value. See
|
| 373 |
* the documentation of \Drupal\Core\Form\FormBuilderInterface::doBuildForm()
|
| 374 |
* for more detailed information about the relationship between #process,
|
| 375 |
* #value, and #access.
|
| 376 |
*
|
| 377 |
* Because #access is set here, it affects display only and does not prevent
|
| 378 |
* JavaScript or other untrusted code from submitting the form as though
|
| 379 |
* access were enabled. The form processing functions for these elements
|
| 380 |
* should not assume that the buttons can't be "clicked" just because they are
|
| 381 |
* not displayed.
|
| 382 |
*
|
| 383 |
* @see \Drupal\file\Element\ManagedFile::processManagedFile()
|
| 384 |
* @see \Drupal\Core\Form\FormBuilderInterface::doBuildForm()
|
| 385 |
*/
|
| 386 |
public static function preRenderManagedFile($element) { |
| 387 |
// If we already have a file, we don't want to show the upload controls.
|
| 388 |
if (!empty($element['#value']['fids'])) { |
| 389 |
if (!$element['#multiple']) { |
| 390 |
$element['upload']['#access'] = FALSE; |
| 391 |
$element['upload_button']['#access'] = FALSE; |
| 392 |
} |
| 393 |
} |
| 394 |
// If we don't already have a file, there is nothing to remove.
|
| 395 |
else {
|
| 396 |
$element['remove_button']['#access'] = FALSE; |
| 397 |
} |
| 398 |
return $element; |
| 399 |
} |
| 400 |
|
| 401 |
/**
|
| 402 |
* Render API callback: Validates the managed_file element.
|
| 403 |
*/
|
| 404 |
public static function validateManagedFile(&$element, FormStateInterface $form_state, &$complete_form) { |
| 405 |
$clicked_button = end($form_state->getTriggeringElement()['#parents']); |
| 406 |
if ($clicked_button != 'remove_button' && !empty($element['fids']['#value'])) { |
| 407 |
$fids = $element['fids']['#value']; |
| 408 |
foreach ($fids as $fid) { |
| 409 |
if ($file = File::load($fid)) { |
| 410 |
// If referencing an existing file, only allow if there are existing
|
| 411 |
// references. This prevents unmanaged files from being deleted if
|
| 412 |
// this item were to be deleted. When files that are no longer in use
|
| 413 |
// are automatically marked as temporary (now disabled by default),
|
| 414 |
// it is not safe to reference a permanent file without usage. Adding
|
| 415 |
// a usage and then later on removing it again would delete the file,
|
| 416 |
// but it is unknown if and where it is currently referenced. However,
|
| 417 |
// when files are not marked temporary (and then removed)
|
| 418 |
// automatically, it is safe to add and remove usages, as it would
|
| 419 |
// simply return to the current state.
|
| 420 |
// @see https://www.drupal.org/node/2891902
|
| 421 |
if ($file->isPermanent() && \Drupal::config('file.settings')->get('make_unused_managed_files_temporary')) { |
| 422 |
$references = static::fileUsage()->listUsage($file); |
| 423 |
if (empty($references)) { |
| 424 |
// We expect the field name placeholder value to be wrapped in t()
|
| 425 |
// here, so it won't be escaped again as it's already marked safe.
|
| 426 |
$form_state->setError($element, t('The file used in the @name field may not be referenced.', ['@name' => $element['#title']])); |
| 427 |
} |
| 428 |
} |
| 429 |
} |
| 430 |
else {
|
| 431 |
// We expect the field name placeholder value to be wrapped in t()
|
| 432 |
// here, so it won't be escaped again as it's already marked safe.
|
| 433 |
$form_state->setError($element, t('The file referenced by the @name field does not exist.', ['@name' => $element['#title']])); |
| 434 |
} |
| 435 |
} |
| 436 |
} |
| 437 |
|
| 438 |
// Check required property based on the FID.
|
| 439 |
if ($element['#required'] && empty($element['fids']['#value']) && !in_array($clicked_button, ['upload_button', 'remove_button'])) { |
| 440 |
// We expect the field name placeholder value to be wrapped in t()
|
| 441 |
// here, so it won't be escaped again as it's already marked safe.
|
| 442 |
$form_state->setError($element, t('@name field is required.', ['@name' => $element['#title']])); |
| 443 |
} |
| 444 |
|
| 445 |
// Consolidate the array value of this field to array of FIDs.
|
| 446 |
if (!$element['#extended']) { |
| 447 |
$form_state->setValueForElement($element, $element['fids']['#value']); |
| 448 |
} |
| 449 |
} |
| 450 |
|
| 451 |
/**
|
| 452 |
* Wraps the file usage service.
|
| 453 |
*
|
| 454 |
* @return \Drupal\file\FileUsage\FileUsageInterface
|
| 455 |
*/
|
| 456 |
protected static function fileUsage() { |
| 457 |
return \Drupal::service('file.usage'); |
| 458 |
} |
| 459 |
|
| 460 |
} |