Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\file;
use Drupal\Core\TypedData\TypedData;
/**
* Computed file URL property class.
*/
class ComputedFileUrl extends TypedData {
/**
* Computed root-relative file URL.
*
* @var string
*/
protected $url = NULL;
/**
* {@inheritdoc}
*/
public function getValue() {
if ($this->url !== NULL) {
return $this->url;
}
assert($this->getParent()->getEntity() instanceof FileInterface);
$uri = $this->getParent()->getEntity()->getFileUri();
$this->url = file_url_transform_relative(file_create_url($uri));
return $this->url;
}
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
$this->url = $value;
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
}

View file

@ -8,6 +8,7 @@ use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
@ -175,6 +176,9 @@ class ManagedFile extends FormElement {
$form_parents = explode('/', $request->query->get('element_parents'));
// Sanitize form parents before using them.
$form_parents = array_filter($form_parents, [Element::class, 'child']);
// Retrieve the element to be rendered.
$form = NestedArray::getValue($form, $form_parents);
@ -295,6 +299,10 @@ class ManagedFile extends FormElement {
// Add the upload progress callback.
$element['upload_button']['#ajax']['progress']['url'] = Url::fromRoute('file.ajax_progress', ['key' => $upload_progress_key]);
// Set a custom submit event so we can modify the upload progress
// identifier element before the form gets submitted.
$element['upload_button']['#ajax']['event'] = 'fileUpload';
}
// The file upload field itself.
@ -398,15 +406,23 @@ class ManagedFile extends FormElement {
* 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()) {
// 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. When files that are no longer in use
// are automatically marked as temporary (now disabled by default),
// it is not safe to reference a permanent file without usage. Adding
// a usage and then later on removing it again would delete the file,
// but it is unknown if and where it is currently referenced. However,
// when files are not marked temporary (and then removed)
// automatically, it is safe to add and remove usages, as it would
// simply return to the current state.
// @see https://www.drupal.org/node/2891902
if ($file->isPermanent() && \Drupal::config('file.settings')->get('make_unused_managed_files_temporary')) {
$references = static::fileUsage()->listUsage($file);
if (empty($references)) {
// We expect the field name placeholder value to be wrapped in t()

View file

@ -18,6 +18,13 @@ use Drupal\user\UserInterface;
* @ContentEntityType(
* id = "file",
* label = @Translation("File"),
* label_collection = @Translation("Files"),
* label_singular = @Translation("file"),
* label_plural = @Translation("files"),
* label_count = @PluralTranslation(
* singular = "@count file",
* plural = "@count files",
* ),
* handlers = {
* "storage" = "Drupal\file\FileStorage",
* "storage_schema" = "Drupal\file\FileStorageSchema",
@ -190,7 +197,10 @@ class File extends ContentEntityBase implements FileInterface {
// The file itself might not exist or be available right now.
$uri = $this->getFileUri();
if ($size = @filesize($uri)) {
$size = @filesize($uri);
// Set size unless there was an error.
if ($size !== FALSE) {
$this->setSize($size);
}
}
@ -240,7 +250,7 @@ class File extends ContentEntityBase implements FileInterface {
->setLabel(t('Filename'))
->setDescription(t('Name of the file with no path components.'));
$fields['uri'] = BaseFieldDefinition::create('uri')
$fields['uri'] = BaseFieldDefinition::create('file_uri')
->setLabel(t('URI'))
->setDescription(t('The URI to access the file (either local or remote).'))
->setSetting('max_length', 255)

View file

@ -22,8 +22,12 @@ class FileAccessControlHandler extends EntityAccessControlHandler {
/** @var \Drupal\file\FileInterface $entity */
if ($operation == 'download' || $operation == 'view') {
if (\Drupal::service('file_system')->uriScheme($entity->getFileUri()) === 'public') {
// Always allow access to file in public file system.
return AccessResult::allowed();
if ($operation === 'download') {
return AccessResult::allowed();
}
else {
return AccessResult::allowedIfHasPermission($account, 'access content');
}
}
elseif ($references = $this->getFileReferences($entity)) {
foreach ($references as $field_name => $entity_map) {
@ -48,11 +52,11 @@ class FileAccessControlHandler extends EntityAccessControlHandler {
// services can be more properly injected.
$allowed_fids = \Drupal::service('session')->get('anonymous_allowed_file_ids', []);
if (!empty($allowed_fids[$entity->id()])) {
return AccessResult::allowed();
return AccessResult::allowed()->addCacheContexts(['session', 'user']);
}
}
else {
return AccessResult::allowed();
return AccessResult::allowed()->addCacheContexts(['user']);
}
}
}
@ -60,11 +64,11 @@ class FileAccessControlHandler extends EntityAccessControlHandler {
if ($operation == 'delete' || $operation == 'update') {
$account = $this->prepareUser($account);
$file_uid = $entity->get('uid')->getValue();
// Only the file owner can delete and update the file entity.
// Only the file owner can update or delete the file entity.
if ($account->id() == $file_uid[0]['target_id']) {
return AccessResult::allowed();
}
return AccessResult::forbidden();
return AccessResult::forbidden('Only the file owner can update or delete the file entity.');
}
// No opinion.
@ -123,8 +127,6 @@ class FileAccessControlHandler extends EntityAccessControlHandler {
// create file entities that are referenced from another entity
// (e.g. an image for a article). A contributed module is free to alter
// this to allow file entities to be created directly.
// @todo Update comment to mention REST module when
// https://www.drupal.org/node/1927648 is fixed.
return AccessResult::neutral();
}

View file

@ -22,4 +22,4 @@ use Drupal\Core\Entity\EntityAccessControlHandlerInterface;
*
* @see \Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase::needsAccessCheck()
*/
interface FileAccessFormatterControlHandlerInterface extends EntityAccessControlHandlerInterface { }
interface FileAccessFormatterControlHandlerInterface extends EntityAccessControlHandlerInterface {}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\file;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
use Drupal\Core\StackMiddleware\NegotiationMiddleware;
/**
* Adds 'application/octet-stream' as a known (bin) format.
*/
class FileServiceProvider implements ServiceModifierInterface {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), NegotiationMiddleware::class, TRUE)) {
$container->getDefinition('http_middleware.negotiation')->addMethodCall('registerFormat', ['bin', ['application/octet-stream']]);
}
}
}

View file

@ -17,7 +17,7 @@ class FileStorageSchema extends SqlContentEntityStorageSchema {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name == 'file_managed') {
if ($table_name == $this->storage->getBaseTable()) {
switch ($field_name) {
case 'status':
case 'changed':

View file

@ -2,6 +2,7 @@
namespace Drupal\file\FileUsage;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\file\FileInterface;
@ -32,8 +33,11 @@ class DatabaseFileUsageBackend extends FileUsageBase {
* information.
* @param string $table
* (optional) The table to store file usage info. Defaults to 'file_usage'.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* (optional) The config factory.
*/
public function __construct(Connection $connection, $table = 'file_usage') {
public function __construct(Connection $connection, $table = 'file_usage', ConfigFactoryInterface $config_factory = NULL) {
parent::__construct($config_factory);
$this->connection = $connection;
$this->tableName = $table;

View file

@ -2,6 +2,7 @@
namespace Drupal\file\FileUsage;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\file\FileInterface;
/**
@ -9,6 +10,27 @@ use Drupal\file\FileInterface;
*/
abstract class FileUsageBase implements FileUsageInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Creates a FileUsageBase object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* (optional) The config factory. Defaults to NULL and will use
* \Drupal::configFactory() instead.
*
* @deprecated The $config_factory parameter will become required in Drupal
* 9.0.0.
*/
public function __construct(ConfigFactoryInterface $config_factory = NULL) {
$this->configFactory = $config_factory ?: \Drupal::configFactory();
}
/**
* {@inheritdoc}
*/
@ -24,6 +46,10 @@ abstract class FileUsageBase implements FileUsageInterface {
* {@inheritdoc}
*/
public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $count = 1) {
// Do not actually mark files as temporary when the behavior is disabled.
if (!$this->configFactory->get('file.settings')->get('make_unused_managed_files_temporary')) {
return;
}
// If there are no more remaining usages of this file, mark it as temporary,
// which result in a delete through system_cron().
$usage = \Drupal::service('file.usage')->listUsage($file);

View file

@ -65,7 +65,7 @@ class FileViewsData extends EntityViewsData {
$data['file_managed']['uid']['relationship']['title'] = $this->t('User who uploaded');
$data['file_managed']['uid']['relationship']['label'] = $this->t('User who uploaded');
$data['file_usage']['table']['group'] = $this->t('File Usage');
$data['file_usage']['table']['group'] = $this->t('File Usage');
// Provide field-type-things to several base tables; on the core files table
// ("file_managed") so that we can create relationships from files to

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Form\FormStateInterface;
/**
* Base class for file formatters that have to deal with file descriptions.
*/
abstract class DescriptionAwareFileFormatterBase extends FileFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$settings = parent::defaultSettings();
$settings['use_description_as_link_text'] = TRUE;
return $settings;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['use_description_as_link_text'] = [
'#title' => $this->t('Use description as link text'),
'#description' => $this->t('Replace the file name by its description when available'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('use_description_as_link_text'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
if ($this->getSetting('use_description_as_link_text')) {
$summary[] = $this->t('Use description as link text');
}
return $summary;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
/**
* Plugin implementation of the 'file_audio' formatter.
*
* @FieldFormatter(
* id = "file_audio",
* label = @Translation("Audio"),
* description = @Translation("Display the file using an HTML5 audio tag."),
* field_types = {
* "file"
* }
* )
*/
class FileAudioFormatter extends FileMediaFormatterBase {
/**
* {@inheritdoc}
*/
public static function getMediaType() {
return 'audio';
}
}

View file

@ -0,0 +1,220 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Template\Attribute;
/**
* Base class for media file formatter.
*/
abstract class FileMediaFormatterBase extends FileFormatterBase implements FileMediaFormatterInterface {
/**
* Gets the HTML tag for the formatter.
*
* @return string
* The HTML tag of this formatter.
*/
protected function getHtmlTag() {
return static::getMediaType();
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'controls' => TRUE,
'autoplay' => FALSE,
'loop' => FALSE,
'multiple_file_display_type' => 'tags',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
return [
'controls' => [
'#title' => $this->t('Show playback controls'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('controls'),
],
'autoplay' => [
'#title' => $this->t('Autoplay'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('autoplay'),
],
'loop' => [
'#title' => $this->t('Loop'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('loop'),
],
'multiple_file_display_type' => [
'#title' => $this->t('Display of multiple files'),
'#type' => 'radios',
'#options' => [
'tags' => $this->t('Use multiple @tag tags, each with a single source.', ['@tag' => '<' . $this->getHtmlTag() . '>']),
'sources' => $this->t('Use multiple sources within a single @tag tag.', ['@tag' => '<' . $this->getHtmlTag() . '>']),
],
'#default_value' => $this->getSetting('multiple_file_display_type'),
],
];
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
if (!parent::isApplicable($field_definition)) {
return FALSE;
}
/** @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $extension_mime_type_guesser */
$extension_mime_type_guesser = \Drupal::service('file.mime_type.guesser.extension');
$extension_list = array_filter(preg_split('/\s+/', $field_definition->getSetting('file_extensions')));
foreach ($extension_list as $extension) {
$mime_type = $extension_mime_type_guesser->guess('fakedFile.' . $extension);
if (static::mimeTypeApplies($mime_type)) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = $this->t('Playback controls: %controls', ['%controls' => $this->getSetting('controls') ? $this->t('visible') : $this->t('hidden')]);
$summary[] = $this->t('Autoplay: %autoplay', ['%autoplay' => $this->getSetting('autoplay') ? $this->t('yes') : $this->t('no')]);
$summary[] = $this->t('Loop: %loop', ['%loop' => $this->getSetting('loop') ? $this->t('yes') : $this->t('no')]);
switch ($this->getSetting('multiple_file_display_type')) {
case 'tags':
$summary[] = $this->t('Multiple file display: Multiple HTML tags');
break;
case 'sources':
$summary[] = $this->t('Multiple file display: One HTML tag with multiple sources');
break;
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$source_files = $this->getSourceFiles($items, $langcode);
if (empty($source_files)) {
return $elements;
}
$attributes = $this->prepareAttributes();
foreach ($source_files as $delta => $files) {
$elements[$delta] = [
'#theme' => $this->getPluginId(),
'#attributes' => $attributes,
'#files' => $files,
'#cache' => ['tags' => []],
];
$cache_tags = [];
foreach ($files as $file) {
$cache_tags = Cache::mergeTags($cache_tags, $file['file']->getCacheTags());
}
$elements[$delta]['#cache']['tags'] = $cache_tags;
}
return $elements;
}
/**
* Prepare the attributes according to the settings.
*
* @param string[] $additional_attributes
* Additional attributes to be applied to the HTML element. Attribute names
* will be used as key and value in the HTML element.
*
* @return \Drupal\Core\Template\Attribute
* Container with all the attributes for the HTML tag.
*/
protected function prepareAttributes(array $additional_attributes = []) {
$attributes = new Attribute();
foreach (['controls', 'autoplay', 'loop'] + $additional_attributes as $attribute) {
if ($this->getSetting($attribute)) {
$attributes->setAttribute($attribute, $attribute);
}
}
return $attributes;
}
/**
* Check if given MIME type applies to the media type of the formatter.
*
* @param string $mime_type
* The complete MIME type.
*
* @return bool
* TRUE if the MIME type applies, FALSE otherwise.
*/
protected static function mimeTypeApplies($mime_type) {
list($type) = explode('/', $mime_type, 2);
return $type === static::getMediaType();
}
/**
* Gets source files with attributes.
*
* @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items
* The item list.
* @param string $langcode
* The language code of the referenced entities to display.
*
* @return array
* Numerically indexed array, which again contains an associative array with
* the following key/values:
* - file => \Drupal\file\Entity\File
* - source_attributes => \Drupal\Core\Template\Attribute
*/
protected function getSourceFiles(EntityReferenceFieldItemListInterface $items, $langcode) {
$source_files = [];
// Because we can have the files grouped in a single media tag, we do a
// grouping in case the multiple file behavior is not 'tags'.
/** @var \Drupal\file\Entity\File $file */
foreach ($this->getEntitiesToView($items, $langcode) as $file) {
if (static::mimeTypeApplies($file->getMimeType())) {
$source_attributes = new Attribute();
$source_attributes
->setAttribute('src', file_url_transform_relative(file_create_url($file->getFileUri())))
->setAttribute('type', $file->getMimeType());
if ($this->getSetting('multiple_file_display_type') === 'tags') {
$source_files[] = [
[
'file' => $file,
'source_attributes' => $source_attributes,
],
];
}
else {
$source_files[0][] = [
'file' => $file,
'source_attributes' => $source_attributes,
];
}
}
}
return $source_files;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
/**
* Defines getter methods for FileMediaFormatterBase.
*
* This interface is used on the FileMediaFormatterBase class to ensure that
* each file media formatter will be based on a media type.
*
* Abstract classes are not able to implement abstract static methods,
* this interface will work around that.
*
* @see \Drupal\file\Plugin\Field\FieldFormatter\FileMediaFormatterBase
*/
interface FileMediaFormatterInterface {
/**
* Gets the applicable media type for a formatter.
*
* @return string
* The media type of this formatter.
*/
public static function getMediaType();
}

View file

@ -13,7 +13,8 @@ use Drupal\Core\Form\FormStateInterface;
* id = "file_uri",
* label = @Translation("File URI"),
* field_types = {
* "uri"
* "uri",
* "file_uri",
* }
* )
*/

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'file_video' formatter.
*
* @FieldFormatter(
* id = "file_video",
* label = @Translation("Video"),
* description = @Translation("Display the file using an HTML5 video tag."),
* field_types = {
* "file"
* }
* )
*/
class FileVideoFormatter extends FileMediaFormatterBase {
/**
* {@inheritdoc}
*/
public static function getMediaType() {
return 'video';
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'muted' => FALSE,
'width' => 640,
'height' => 480,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
return parent::settingsForm($form, $form_state) + [
'muted' => [
'#title' => $this->t('Muted'),
'#type' => 'checkbox',
'#default_value' => $this->getSetting('muted'),
],
'width' => [
'#type' => 'number',
'#title' => $this->t('Width'),
'#default_value' => $this->getSetting('width'),
'#size' => 5,
'#maxlength' => 5,
'#field_suffix' => $this->t('pixels'),
'#min' => 0,
'#required' => TRUE,
],
'height' => [
'#type' => 'number',
'#title' => $this->t('Height'),
'#default_value' => $this->getSetting('height'),
'#size' => 5,
'#maxlength' => 5,
'#field_suffix' => $this->t('pixels'),
'#min' => 0,
'#required' => TRUE,
],
];
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$summary[] = $this->t('Muted: %muted', ['%muted' => $this->getSetting('muted') ? $this->t('yes') : $this->t('no')]);
$summary[] = $this->t('Size: %width x %height pixels', [
'%width' => $this->getSetting('width'),
'%height' => $this->getSetting('height'),
]);
return $summary;
}
/**
* {@inheritdoc}
*/
protected function prepareAttributes(array $additional_attributes = []) {
return parent::prepareAttributes(['muted'])
->setAttribute('width', $this->getSetting('width'))
->setAttribute('height', $this->getSetting('height'));
}
}

View file

@ -15,7 +15,7 @@ use Drupal\Core\Field\FieldItemListInterface;
* }
* )
*/
class GenericFileFormatter extends FileFormatterBase {
class GenericFileFormatter extends DescriptionAwareFileFormatterBase {
/**
* {@inheritdoc}
@ -28,7 +28,7 @@ class GenericFileFormatter extends FileFormatterBase {
$elements[$delta] = [
'#theme' => 'file_link',
'#file' => $file,
'#description' => $item->description,
'#description' => $this->getSetting('use_description_as_link_text') ? $item->description : NULL,
'#cache' => [
'tags' => $file->getCacheTags(),
],

View file

@ -15,7 +15,7 @@ use Drupal\Core\Field\FieldItemListInterface;
* }
* )
*/
class TableFormatter extends FileFormatterBase {
class TableFormatter extends DescriptionAwareFileFormatterBase {
/**
* {@inheritdoc}
@ -27,11 +27,13 @@ class TableFormatter extends FileFormatterBase {
$header = [t('Attachment'), t('Size')];
$rows = [];
foreach ($files as $delta => $file) {
$item = $file->_referringItem;
$rows[] = [
[
'data' => [
'#theme' => 'file_link',
'#file' => $file,
'#description' => $this->getSetting('use_description_as_link_text') ? $item->description : NULL,
'#cache' => [
'tags' => $file->getCacheTags(),
],

View file

@ -13,7 +13,7 @@ class FileFieldItemList extends EntityReferenceFieldItemList {
/**
* {@inheritdoc}
*/
public function defaultValuesForm(array &$form, FormStateInterface $form_state) { }
public function defaultValuesForm(array &$form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}

View file

@ -333,7 +333,7 @@ class FileItem extends EntityReferenceItem {
$file = file_save_data($data, $destination, FILE_EXISTS_ERROR);
$values = [
'target_id' => $file->id(),
'display' => (int)$settings['display_default'],
'display' => (int) $settings['display_default'],
'description' => $random->sentences(10),
];
return $values;

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\file\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\file\ComputedFileUrl;
/**
* File-specific plugin implementation of a URI item to provide a full URL.
*
* @FieldType(
* id = "file_uri",
* label = @Translation("File URI"),
* description = @Translation("An entity field containing a file URI, and a computed root-relative file URL."),
* no_ui = TRUE,
* default_formatter = "file_uri",
* default_widget = "uri",
* )
*/
class FileUriItem extends UriItem {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
$properties['url'] = DataDefinition::create('string')
->setLabel(t('Root-relative file URL'))
->setComputed(TRUE)
->setInternal(FALSE)
->setClass(ComputedFileUrl::class);
return $properties;
}
}

View file

@ -365,7 +365,7 @@ class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
'%list' => implode(', ', $removed_names),
];
$message = t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args);
drupal_set_message($message, 'warning');
\Drupal::messenger()->addWarning($message);
$values['fids'] = array_slice($values['fids'], 0, $keep);
NestedArray::setValue($form_state->getValues(), $element['#parents'], $values);
}

View file

@ -15,7 +15,12 @@ class FileValidationConstraintValidator extends ConstraintValidator {
*/
public function validate($value, Constraint $constraint) {
// Get the file to execute validators.
$file = $value->get('entity')->getTarget()->getValue();
$target = $value->get('entity')->getTarget();
if (!$target) {
return;
}
$file = $target->getValue();
// Get the validators.
$validators = $value->getUploadValidators();
// Checks that a file meets the criteria specified by the validators.

View file

@ -2,6 +2,8 @@
namespace Drupal\file\Plugin\migrate\cckfield\d6;
@trigger_error('FileField is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\file\Plugin\migrate\field\d6\FileField instead.', E_USER_DEPRECATED);
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
@ -9,8 +11,15 @@ use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
/**
* @MigrateCckField(
* id = "filefield",
* core = {6}
* core = {6},
* source_module = "filefield",
* destination_module = "file"
* )
*
* @deprecated in Drupal 8.3.x, to be removed before Drupal 9.0.x. Use
* \Drupal\file\Plugin\migrate\field\d6\FileField instead.
*
* @see https://www.drupal.org/node/2751897
*/
class FileField extends CckFieldPluginBase {

View file

@ -2,6 +2,8 @@
namespace Drupal\file\Plugin\migrate\cckfield\d7;
@trigger_error('FileField is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\file\Plugin\migrate\field\d7\FileField instead.', E_USER_DEPRECATED);
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
@ -9,8 +11,15 @@ use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
/**
* @MigrateCckField(
* id = "file",
* core = {7}
* core = {7},
* source_module = "file",
* destination_module = "file"
* )
*
* @deprecated in Drupal 8.3.x, to be removed before Drupal 9.0.x. Use
* \Drupal\file\Plugin\migrate\field\d7\FileField instead.
*
* @see https://www.drupal.org/node/2751897
*/
class FileField extends CckFieldPluginBase {
@ -42,7 +51,7 @@ class FileField extends CckFieldPluginBase {
*/
public function processCckFieldValues(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'iterator',
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => 'fid',

View file

@ -2,40 +2,16 @@
namespace Drupal\file\Plugin\migrate\cckfield\d7;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate_drupal\Plugin\migrate\cckfield\CckFieldPluginBase;
@trigger_error('ImageField is deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.x. Use \Drupal\image\Plugin\migrate\field\d7\ImageField instead. See https://www.drupal.org/node/2936061.', E_USER_DEPRECATED);
use Drupal\image\Plugin\migrate\cckfield\d7\ImageField as LegacyImageField;
/**
* @MigrateCckField(
* id = "image",
* core = {7}
* )
* CCK plugin for image fields.
*
* @deprecated in Drupal 8.3.x, to be removed before Drupal 9.0.x. Use
* \Drupal\image\Plugin\migrate\field\d7\ImageField instead.
*
* @see https://www.drupal.org/node/2936061
*/
class ImageField extends CckFieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [];
}
/**
* {@inheritdoc}
*/
public function processCckFieldValues(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'iterator',
'source' => $field_name,
'process' => [
'target_id' => 'fid',
'alt' => 'alt',
'title' => 'title',
'width' => 'width',
'height' => 'height',
],
];
$migration->mergeProcessOfProperty($field_name, $process);
}
}
class ImageField extends LegacyImageField {}

View file

@ -2,7 +2,6 @@
namespace Drupal\file\Plugin\migrate\destination;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateException;
@ -58,7 +57,7 @@ class EntityFile extends EntityContentBase {
// Make it into a proper public file uri, stripping off the existing
// scheme if present.
$value = 'public://' . preg_replace('|^[a-z]+://|i', '', $value);
$value = Unicode::substr($value, 0, $field_definitions['uri']->getSetting('max_length'));
$value = mb_substr($value, 0, $field_definitions['uri']->getSetting('max_length'));
// Create a real file, so File::preSave() can do filesize() on it.
touch($value);
$row->setDestinationProperty('uri', $value);

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\file\Plugin\migrate\field\d6;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
/**
* @MigrateField(
* id = "filefield",
* core = {6},
* source_module = "filefield",
* destination_module = "file"
* )
*/
class FileField extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function getFieldWidgetMap() {
return [
'filefield_widget' => 'file_generic',
];
}
/**
* {@inheritdoc}
*/
public function getFieldFormatterMap() {
return [
'default' => 'file_default',
'url_plain' => 'file_url_plain',
'path_plain' => 'file_url_plain',
'image_plain' => 'image',
'image_nodelink' => 'image',
'image_imagelink' => 'image',
];
}
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'd6_field_file',
'source' => $field_name,
];
$migration->mergeProcessOfProperty($field_name, $process);
}
/**
* {@inheritdoc}
*/
public function getFieldType(Row $row) {
return $row->getSourceProperty('widget_type') == 'imagefield_widget' ? 'image' : 'file';
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\file\Plugin\migrate\field\d6;
@trigger_error('ImageField is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.x. Use \Drupal\image\Plugin\migrate\field\d6\ImageField instead. See https://www.drupal.org/node/2936061.', E_USER_DEPRECATED);
use Drupal\image\Plugin\migrate\field\d6\ImageField as NonLegacyImageField;
/**
* Field plugin for image fields.
*
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
* \Drupal\image\Plugin\migrate\field\d6\ImageField instead.
*
* @see https://www.drupal.org/node/2936061
*/
class ImageField extends NonLegacyImageField {}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\file\Plugin\migrate\field\d7;
use Drupal\file\Plugin\migrate\field\d6\FileField as D6FileField;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* @MigrateField(
* id = "file",
* core = {7},
* source_module = "file",
* destination_module = "file"
* )
*/
class FileField extends D6FileField {
/**
* {@inheritdoc}
*/
public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) {
$process = [
'plugin' => 'sub_process',
'source' => $field_name,
'process' => [
'target_id' => 'fid',
'display' => 'display',
'description' => 'description',
],
];
$migration->mergeProcessOfProperty($field_name, $process);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Drupal\file\Plugin\migrate\field\d7;
@trigger_error('ImageField is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.x. Use \Drupal\image\Plugin\migrate\field\d7\ImageField instead. See https://www.drupal.org/node/2936061.', E_USER_DEPRECATED);
use Drupal\image\Plugin\migrate\field\d7\ImageField as NonLegacyImageField;
/**
* Field plugin for image fields.
*
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.x. Use
* \Drupal\image\Plugin\migrate\field\d7\ImageField instead.
*
* @see https://www.drupal.org/node/2936061
*/
class ImageField extends NonLegacyImageField {}

View file

@ -2,91 +2,16 @@
namespace Drupal\file\Plugin\migrate\process\d6;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
@trigger_error('CckFile is deprecated in Drupal 8.3.x and will be be removed before Drupal 9.0.x. Use \Drupal\file\Plugin\migrate\process\d6\FieldFile instead.', E_USER_DEPRECATED);
/**
* @MigrateProcessPlugin(
* id = "d6_cck_file"
* )
*
* @deprecated in Drupal 8.3.x, to be removed before Drupal 9.0.x. Use
* \Drupal\file\Plugin\migrate\process\d6\FieldFile instead.
*
* @see https://www.drupal.org/node/2751897
*/
class CckFile extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The migration process plugin, configured for lookups in d6_file.
*
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
*/
protected $migrationPlugin;
/**
* Constructs a CckFile plugin instance.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The current migration.
* @param \Drupal\migrate\Plugin\MigrateProcessInterface $migration_plugin
* An instance of the 'migration' process plugin.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, MigrateProcessInterface $migration_plugin) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$this->migrationPlugin = $migration_plugin;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
// Configure the migration process plugin to look up migrated IDs from
// a d6 file migration.
$migration_plugin_configuration = $configuration + [
'migration' => 'd6_file',
'source' => ['fid'],
];
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_plugin_configuration, $migration)
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$options = unserialize($value['data']);
// Try to look up the ID of the migrated file. If one cannot be found, it
// means the file referenced by the current field item did not migrate for
// some reason -- file migration is notoriously brittle -- and we do NOT
// want to send invalid file references into the field system (it causes
// fatals), so return an empty item instead.
if ($fid = $this->migrationPlugin->transform($value['fid'], $migrate_executable, $row, $destination_property)) {
return [
'target_id' => $fid,
'display' => $value['list'],
'description' => isset($options['description']) ? $options['description'] : '',
'alt' => isset($options['alt']) ? $options['alt'] : '',
'title' => isset($options['title']) ? $options['title'] : '',
];
}
else {
return [];
}
}
}
class CckFile extends FieldFile {}

View file

@ -0,0 +1,92 @@
<?php
namespace Drupal\file\Plugin\migrate\process\d6;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @MigrateProcessPlugin(
* id = "d6_field_file"
* )
*/
class FieldFile extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The migration process plugin, configured for lookups in d6_file.
*
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
*/
protected $migrationPlugin;
/**
* Constructs a FieldFile plugin instance.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The current migration.
* @param \Drupal\migrate\Plugin\MigrateProcessInterface $migration_plugin
* An instance of the 'migration' process plugin.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, MigrateProcessInterface $migration_plugin) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$this->migrationPlugin = $migration_plugin;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
// Configure the migration process plugin to look up migrated IDs from
// a d6 file migration.
$migration_plugin_configuration = $configuration + [
'migration' => 'd6_file',
'source' => ['fid'],
];
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_plugin_configuration, $migration)
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$options = unserialize($value['data']);
// Try to look up the ID of the migrated file. If one cannot be found, it
// means the file referenced by the current field item did not migrate for
// some reason -- file migration is notoriously brittle -- and we do NOT
// want to send invalid file references into the field system (it causes
// fatals), so return an empty item instead.
if ($fid = $this->migrationPlugin->transform($value['fid'], $migrate_executable, $row, $destination_property)) {
return [
'target_id' => $fid,
'display' => $value['list'],
'description' => isset($options['description']) ? $options['description'] : '',
'alt' => isset($options['alt']) ? $options['alt'] : '',
'title' => isset($options['title']) ? $options['title'] : '',
];
}
else {
return [];
}
}
}

View file

@ -9,7 +9,8 @@ use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
* Drupal 6 file source from database.
*
* @MigrateSource(
* id = "d6_file"
* id = "d6_file",
* source_module = "system"
* )
*/
class File extends DrupalSqlBase {
@ -41,6 +42,7 @@ class File extends DrupalSqlBase {
public function query() {
return $this->select('files', 'f')
->fields('f')
->condition('filepath', '/tmp%', 'NOT LIKE')
->orderBy('timestamp')
// If two or more files have the same timestamp, they'll end up in a
// non-deterministic order. Ordering by fid (or any other unique field)
@ -87,6 +89,7 @@ class File extends DrupalSqlBase {
'is_public' => $this->t('TRUE if the files directory is public otherwise FALSE.'),
];
}
/**
* {@inheritdoc}
*/

View file

@ -10,7 +10,7 @@ use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
*
* @MigrateSource(
* id = "d6_upload",
* source_provider = "upload"
* source_module = "upload"
* )
*/
class Upload extends DrupalSqlBase {
@ -29,6 +29,7 @@ class Upload extends DrupalSqlBase {
->fields('u', ['nid', 'vid']);
$query->innerJoin('node', 'n', static::JOIN);
$query->addField('n', 'type');
$query->addField('n', 'language');
return $query;
}
@ -54,6 +55,7 @@ class Upload extends DrupalSqlBase {
'nid' => $this->t('The node Id.'),
'vid' => $this->t('The version Id.'),
'type' => $this->t('The node type'),
'language' => $this->t('The node language.'),
'description' => $this->t('The file description.'),
'list' => $this->t('Whether the list should be visible on the node page.'),
'weight' => $this->t('The file weight.'),

View file

@ -10,7 +10,7 @@ use Drupal\migrate\Plugin\migrate\source\DummyQueryTrait;
*
* @MigrateSource(
* id = "d6_upload_instance",
* source_provider = "upload"
* source_module = "upload"
* )
*/
class UploadInstance extends DrupalSqlBase {
@ -25,7 +25,9 @@ class UploadInstance extends DrupalSqlBase {
->fields('nt', ['type'])
->execute()
->fetchCol();
$variables = array_map(function($type) { return 'upload_' . $type; }, $node_types);
$variables = array_map(function ($type) {
return 'upload_' . $type;
}, $node_types);
$max_filesize = $this->variableGet('upload_uploadsize_default', 1);
$max_filesize = $max_filesize ? $max_filesize . 'MB' : '';
@ -76,7 +78,7 @@ class UploadInstance extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function count() {
public function count($refresh = FALSE) {
return count($this->initializeIterator());
}

View file

@ -10,7 +10,8 @@ use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
* Drupal 7 file source from database.
*
* @MigrateSource(
* id = "d7_file"
* id = "d7_file",
* source_module = "file"
* )
*/
class File extends DrupalSqlBase {
@ -42,18 +43,21 @@ class File extends DrupalSqlBase {
public function query() {
$query = $this->select('file_managed', 'f')
->fields('f')
->condition('uri', 'temporary://%', 'NOT LIKE')
->orderBy('f.timestamp');
// Filter by scheme(s), if configured.
if (isset($this->configuration['scheme'])) {
$schemes = [];
// Remove 'temporary' scheme.
$valid_schemes = array_diff((array) $this->configuration['scheme'], ['temporary']);
// Accept either a single scheme, or a list.
foreach ((array) $this->configuration['scheme'] as $scheme) {
foreach ((array) $valid_schemes as $scheme) {
$schemes[] = rtrim($scheme) . '://';
}
$schemes = array_map([$this->getDatabase(), 'escapeLike'], $schemes);
// uri LIKE 'public://%' OR uri LIKE 'private://%'
// Add conditions, uri LIKE 'public://%' OR uri LIKE 'private://%'.
$conditions = new Condition('OR');
foreach ($schemes as $scheme) {
$conditions->condition('uri', $scheme . '%', 'LIKE');

View file

@ -0,0 +1,586 @@
<?php
namespace Drupal\file\Plugin\rest\resource;
use Drupal\Component\Utility\Bytes;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\Config;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\Token;
use Drupal\file\FileInterface;
use Drupal\rest\ModifiedResourceResponse;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\file\Entity\File;
use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait;
use Drupal\rest\RequestHandler;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* File upload resource.
*
* This is implemented as a field-level resource for the following reasons:
* - Validation for uploaded files is tied to fields (allowed extensions, max
* size, etc..).
* - The actual files do not need to be stored in another temporary location,
* to be later moved when they are referenced from a file field.
* - Permission to upload a file can be determined by a users field level
* create access to the file field.
*
* @RestResource(
* id = "file:upload",
* label = @Translation("File Upload"),
* serialization_class = "Drupal\file\Entity\File",
* uri_paths = {
* "https://www.drupal.org/link-relations/create" = "/file/upload/{entity_type_id}/{bundle}/{field_name}"
* }
* )
*/
class FileUploadResource extends ResourceBase {
use EntityResourceValidationTrait {
validate as resourceValidate;
}
/**
* The regex used to extract the filename from the content disposition header.
*
* @var string
*/
const REQUEST_HEADER_FILENAME_REGEX = '@\bfilename(?<star>\*?)=\"(?<filename>.+)\"@';
/**
* The amount of bytes to read in each iteration when streaming file data.
*
* @var int
*/
const BYTES_TO_READ = 8192;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The currently authenticated user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The MIME type guesser.
*
* @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
*/
protected $mimeTypeGuesser;
/**
* The token replacement instance.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* The lock service.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $systemFileConfig;
/**
* Constructs a FileUploadResource instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param array $serializer_formats
* The available serialization formats.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The currently authenticated user.
* @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
* The MIME type guesser.
* @param \Drupal\Core\Utility\Token $token
* The token replacement instance.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock service.
* @param \Drupal\Core\Config\Config $system_file_config
* The system file configuration.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, Token $token, LockBackendInterface $lock, Config $system_file_config) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->fileSystem = $file_system;
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->currentUser = $current_user;
$this->mimeTypeGuesser = $mime_type_guesser;
$this->token = $token;
$this->lock = $lock;
$this->systemFileConfig = $system_file_config;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('file_system'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('current_user'),
$container->get('file.mime_type.guesser'),
$container->get('token'),
$container->get('lock'),
$container->get('config.factory')->get('system.file')
);
}
/**
* {@inheritdoc}
*/
public function permissions() {
// Access to this resource depends on field-level access so no explicit
// permissions are required.
// @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validateAndLoadFieldDefinition()
// @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
return [];
}
/**
* Creates a file from an endpoint.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The entity bundle. This will be the same as $entity_type_id for entity
* types that don't support bundles.
* @param string $field_name
* The field name.
*
* @return \Drupal\rest\ModifiedResourceResponse
* A 201 response, on success.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* Thrown when temporary files cannot be written, a lock cannot be acquired,
* or when temporary files cannot be moved to their new location.
*/
public function post(Request $request, $entity_type_id, $bundle, $field_name) {
$filename = $this->validateAndParseContentDispositionHeader($request);
$field_definition = $this->validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name);
$destination = $this->getUploadLocation($field_definition->getSettings());
// Check the destination file path is writable.
if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
throw new HttpException(500, 'Destination file path is not writable');
}
$validators = $this->getUploadValidators($field_definition);
$prepared_filename = $this->prepareFilename($filename, $validators);
// Create the file.
$file_uri = "{$destination}/{$prepared_filename}";
$temp_file_path = $this->streamUploadData();
// This will take care of altering $file_uri if a file already exists.
file_unmanaged_prepare($temp_file_path, $file_uri);
// Lock based on the prepared file URI.
$lock_id = $this->generateLockIdFromFileUri($file_uri);
if (!$this->lock->acquire($lock_id)) {
throw new HttpException(503, sprintf('File "%s" is already locked for writing'), NULL, ['Retry-After' => 1]);
}
// Begin building file entity.
$file = File::create([]);
$file->setOwnerId($this->currentUser->id());
$file->setFilename($prepared_filename);
$file->setMimeType($this->mimeTypeGuesser->guess($prepared_filename));
$file->setFileUri($file_uri);
// Set the size. This is done in File::preSave() but we validate the file
// before it is saved.
$file->setSize(@filesize($temp_file_path));
// Validate the file entity against entity-level validation and field-level
// validators.
$this->validate($file, $validators);
// Move the file to the correct location after validation. Use
// FILE_EXISTS_ERROR as the file location has already been determined above
// in file_unmanaged_prepare().
if (!file_unmanaged_move($temp_file_path, $file_uri, FILE_EXISTS_ERROR)) {
throw new HttpException(500, 'Temporary file could not be moved to file location');
}
$file->save();
$this->lock->release($lock_id);
// 201 Created responses return the newly created entity in the response
// body. These responses are not cacheable, so we add no cacheability
// metadata here.
return new ModifiedResourceResponse($file, 201);
}
/**
* Streams file upload data to temporary file and moves to file destination.
*
* @return string
* The temp file path.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* Thrown when input data cannot be read, the temporary file cannot be
* opened, or the temporary file cannot be written.
*/
protected function streamUploadData() {
// 'rb' is needed so reading works correctly on Windows environments too.
$file_data = fopen('php://input', 'rb');
$temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
$temp_file = fopen($temp_file_path, 'wb');
if ($temp_file) {
while (!feof($file_data)) {
$read = fread($file_data, static::BYTES_TO_READ);
if ($read === FALSE) {
// Close the file streams.
fclose($temp_file);
fclose($file_data);
$this->logger->error('Input data could not be read');
throw new HttpException(500, 'Input file data could not be read');
}
if (fwrite($temp_file, $read) === FALSE) {
// Close the file streams.
fclose($temp_file);
fclose($file_data);
$this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
throw new HttpException(500, 'Temporary file data could not be written');
}
}
// Close the temp file stream.
fclose($temp_file);
}
else {
// Close the file streams.
fclose($temp_file);
fclose($file_data);
$this->logger->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_path]);
throw new HttpException(500, 'Temporary file could not be opened');
}
// Close the input stream.
fclose($file_data);
return $temp_file_path;
}
/**
* Validates and extracts the filename from the Content-Disposition header.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return string
* The filename extracted from the header.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the 'Content-Disposition' request header is invalid.
*/
protected function validateAndParseContentDispositionHeader(Request $request) {
// Firstly, check the header exists.
if (!$request->headers->has('content-disposition')) {
throw new BadRequestHttpException('"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided');
}
$content_disposition = $request->headers->get('content-disposition');
// Parse the header value. This regex does not allow an empty filename.
// i.e. 'filename=""'. This also matches on a word boundary so other keys
// like 'not_a_filename' don't work.
if (!preg_match(static::REQUEST_HEADER_FILENAME_REGEX, $content_disposition, $matches)) {
throw new BadRequestHttpException('No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided');
}
// Check for the "filename*" format. This is currently unsupported.
if (!empty($matches['star'])) {
throw new BadRequestHttpException('The extended "filename*" format is currently not supported in the "Content-Disposition" header');
}
// Don't validate the actual filename here, that will be done by the upload
// validators in validate().
// @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validate()
$filename = $matches['filename'];
// Make sure only the filename component is returned. Path information is
// stripped as per https://tools.ietf.org/html/rfc6266#section-4.3.
return basename($filename);
}
/**
* Validates and loads a field definition instance.
*
* @param string $entity_type_id
* The entity type ID the field is attached to.
* @param string $bundle
* The bundle the field is attached to.
* @param string $field_name
* The field name.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface
* The field definition.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the field does not exist.
* @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException
* Thrown when the target type of the field is not a file, or the current
* user does not have 'edit' access for the field.
*/
protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
if (!isset($field_definitions[$field_name])) {
throw new NotFoundHttpException(sprintf('Field "%s" does not exist', $field_name));
}
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
$field_definition = $field_definitions[$field_name];
if ($field_definition->getSetting('target_type') !== 'file') {
throw new AccessDeniedHttpException(sprintf('"%s" is not a file field', $field_name));
}
$entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id);
$bundle = $this->entityTypeManager->getDefinition($entity_type_id)->hasKey('bundle') ? $bundle : NULL;
$access_result = $entity_access_control_handler->createAccess($bundle, NULL, [], TRUE)
->andIf($entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE));
if (!$access_result->isAllowed()) {
throw new AccessDeniedHttpException($access_result->getReason());
}
return $field_definition;
}
/**
* Validates the file.
*
* @param \Drupal\file\FileInterface $file
* The file entity to validate.
* @param array $validators
* An array of upload validators to pass to file_validate().
*
* @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
* Thrown when there are file validation errors.
*/
protected function validate(FileInterface $file, array $validators) {
$this->resourceValidate($file);
// Validate the file based on the field definition configuration.
$errors = file_validate($file, $validators);
if (!empty($errors)) {
$message = "Unprocessable Entity: file validation failed.\n";
$message .= implode("\n", array_map(function ($error) {
return PlainTextOutput::renderFromHtml($error);
}, $errors));
throw new UnprocessableEntityHttpException($message);
}
}
/**
* Prepares the filename to strip out any malicious extensions.
*
* @param string $filename
* The file name.
* @param array $validators
* The array of upload validators.
*
* @return string
* The prepared/munged filename.
*/
protected function prepareFilename($filename, array &$validators) {
if (!empty($validators['file_validate_extensions'][0])) {
// If there is a file_validate_extensions validator and a list of
// valid extensions, munge the filename to protect against possible
// malicious extension hiding within an unknown file type. For example,
// "filename.html.foo".
$filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
}
// 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 (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
// The destination filename will also later be used to create the URI.
$filename .= '.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($validators['file_validate_extensions'][0])) {
$validators['file_validate_extensions'][0] .= ' txt';
}
}
return $filename;
}
/**
* Determines the URI for a file field.
*
* @param array $settings
* The array of field settings.
*
* @return string
* An un-sanitized file directory URI with tokens replaced. The result of
* the token replacement is then converted to plain text and returned.
*/
protected function getUploadLocation(array $settings) {
$destination = trim($settings['file_directory'], '/');
// Replace tokens. As the tokens might contain HTML we convert it to plain
// text.
$destination = PlainTextOutput::renderFromHtml($this->token->replace($destination, []));
return $settings['uri_scheme'] . '://' . $destination;
}
/**
* Retrieves the upload validators for a field definition.
*
* This is copied from \Drupal\file\Plugin\Field\FieldType\FileItem as there
* is no entity instance available here that that a FileItem would exist for.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition for which to get validators.
*
* @return array
* An array suitable for passing to file_save_upload() or the file field
* element's '#upload_validators' property.
*/
protected function getUploadValidators(FieldDefinitionInterface $field_definition) {
$validators = [
// Add in our check of the file name length.
'file_validate_name_length' => [],
];
$settings = $field_definition->getSettings();
// Cap the upload size according to the PHP limit.
$max_filesize = Bytes::toInt(file_upload_max_size());
if (!empty($settings['max_filesize'])) {
$max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize']));
}
// There is always a file size limit due to the PHP server limit.
$validators['file_validate_size'] = [$max_filesize];
// Add the extension check if necessary.
if (!empty($settings['file_extensions'])) {
$validators['file_validate_extensions'] = [$settings['file_extensions']];
}
return $validators;
}
/**
* {@inheritdoc}
*/
protected function getBaseRoute($canonical_path, $method) {
return new Route($canonical_path, [
'_controller' => RequestHandler::class . '::handleRaw',
],
$this->getBaseRouteRequirements($method),
[],
'',
[],
// The HTTP method is a requirement for this route.
[$method]
);
}
/**
* {@inheritdoc}
*/
protected function getBaseRouteRequirements($method) {
$requirements = parent::getBaseRouteRequirements($method);
// Add the content type format access check. This will enforce that all
// incoming requests can only use the 'application/octet-stream'
// Content-Type header.
$requirements['_content_type_format'] = 'bin';
return $requirements;
}
/**
* Generates a lock ID based on the file URI.
*
* @param $file_uri
* The file URI.
*
* @return string
* The generated lock ID.
*/
protected static function generateLockIdFromFileUri($file_uri) {
return 'file:rest:' . Crypt::hashBase64($file_uri);
}
}

View file

@ -17,6 +17,8 @@ class File extends WizardPluginBase {
/**
* Set the created column.
*
* @var string
*/
protected $createdColumn = 'created';

View file

@ -1,172 +0,0 @@
<?php
namespace Drupal\file\Tests;
/**
* Tests for download/file transfer functions.
*
* @group file
*/
class DownloadTest extends FileManagedTestBase {
protected function setUp() {
parent::setUp();
// Clear out any hook calls.
file_test_reset();
}
/**
* Test the public file transfer system.
*/
public function testPublicFileTransfer() {
// Test generating a URL to a created file.
$file = $this->createFile();
$url = file_create_url($file->getFileUri());
// URLs can't contain characters outside the ASCII set so $filename has to be
// encoded.
$filename = $GLOBALS['base_url'] . '/' . \Drupal::service('stream_wrapper_manager')->getViaScheme('public')->getDirectoryPath() . '/' . rawurlencode($file->getFilename());
$this->assertEqual($filename, $url, 'Correctly generated a URL for a created file.');
$this->drupalHead($url);
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the created file.');
// Test generating a URL to a shipped file (i.e. a file that is part of
// Drupal core, a module or a theme, for example a JavaScript file).
$filepath = 'core/assets/vendor/jquery/jquery.min.js';
$url = file_create_url($filepath);
$this->assertEqual($GLOBALS['base_url'] . '/' . $filepath, $url, 'Correctly generated a URL for a shipped file.');
$this->drupalHead($url);
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
}
/**
* Test the private file transfer system.
*/
public function testPrivateFileTransferWithoutPageCache() {
$this->doPrivateFileTransferTest();
}
/**
* Test the private file transfer system.
*/
protected function doPrivateFileTransferTest() {
// Set file downloads to private so handler functions get called.
// Create a file.
$contents = $this->randomMachineName(8);
$file = $this->createFile(NULL, $contents, 'private');
// Created private files without usage are by default not accessible
// for a user different from the owner, but createFile always uses uid 1
// as the owner of the files. Therefore make it permanent to allow access
// if a module allows it.
$file->setPermanent();
$file->save();
$url = file_create_url($file->getFileUri());
// Set file_test access header to allow the download.
file_test_set_return('download', ['x-foo' => 'Bar']);
$this->drupalGet($url);
$this->assertEqual($this->drupalGetHeader('x-foo'), 'Bar', 'Found header set by file_test module on private download.');
$this->assertFalse($this->drupalGetHeader('x-drupal-cache'), 'Page cache is disabled on private file download.');
$this->assertResponse(200, 'Correctly allowed access to a file when file_test provides headers.');
// Test that the file transferred correctly.
$this->assertEqual($contents, $this->content, 'Contents of the file are correct.');
// Deny access to all downloads via a -1 header.
file_test_set_return('download', -1);
$this->drupalHead($url);
$this->assertResponse(403, 'Correctly denied access to a file when file_test sets the header to -1.');
// Try non-existent file.
$url = file_create_url('private://' . $this->randomMachineName());
$this->drupalHead($url);
$this->assertResponse(404, 'Correctly returned 404 response for a non-existent file.');
}
/**
* Test file_create_url().
*/
public function testFileCreateUrl() {
// Tilde (~) is excluded from this test because it is encoded by
// rawurlencode() in PHP 5.2 but not in PHP 5.3, as per RFC 3986.
// @see http://php.net/manual/function.rawurlencode.php#86506
$basename = " -._!$'\"()*@[]?&+%#,;=:\n\x00" . // "Special" ASCII characters.
"%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string.
"éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
$basename_encoded = '%20-._%21%24%27%22%28%29%2A%40%5B%5D%3F%26%2B%25%23%2C%3B%3D%3A__' .
'%2523%2525%2526%252B%252F%253F' .
'%C3%A9%C3%B8%C3%AF%D0%B2%CE%B2%E4%B8%AD%E5%9C%8B%E6%9B%B8%DB%9E';
// Public files should not be served by Drupal, so their URLs should not be
// routed through Drupal, whereas private files should be served by Drupal,
// so they need to be. The difference is most apparent when $script_path
// is not empty (i.e., when not using clean URLs).
$clean_url_settings = [
'clean' => '',
'unclean' => 'index.php/',
];
$public_directory_path = \Drupal::service('stream_wrapper_manager')->getViaScheme('public')->getDirectoryPath();
foreach ($clean_url_settings as $clean_url_setting => $script_path) {
$clean_urls = $clean_url_setting == 'clean';
$request = $this->prepareRequestForGenerator($clean_urls);
$base_path = $request->getSchemeAndHttpHost() . $request->getBasePath();
$this->checkUrl('public', '', $basename, $base_path . '/' . $public_directory_path . '/' . $basename_encoded);
$this->checkUrl('private', '', $basename, $base_path . '/' . $script_path . 'system/files/' . $basename_encoded);
}
$this->assertEqual(file_create_url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='), 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', t('Generated URL matches expected URL.'));
// Test public files with a different host name from settings.
$test_base_url = 'http://www.example.com/cdn';
$this->settingsSet('file_public_base_url', $test_base_url);
$filepath = file_create_filename('test.txt', '');
$directory_uri = 'public://' . dirname($filepath);
file_prepare_directory($directory_uri, FILE_CREATE_DIRECTORY);
$file = $this->createFile($filepath, NULL, 'public');
$url = file_create_url($file->getFileUri());
$expected_url = $test_base_url . '/' . basename($filepath);
$this->assertEqual($url, $expected_url);
}
/**
* Download a file from the URL generated by file_create_url().
*
* Create a file with the specified scheme, directory and filename; check that
* the URL generated by file_create_url() for the specified file equals the
* specified URL; fetch the URL and then compare the contents to the file.
*
* @param string $scheme
* A scheme, e.g. "public".
* @param string $directory
* A directory, possibly "".
* @param string $filename
* A filename.
* @param string $expected_url
* The expected URL.
*/
private function checkUrl($scheme, $directory, $filename, $expected_url) {
// Convert $filename to a valid filename, i.e. strip characters not
// supported by the filesystem, and create the file in the specified
// directory.
$filepath = file_create_filename($filename, $directory);
$directory_uri = $scheme . '://' . dirname($filepath);
file_prepare_directory($directory_uri, FILE_CREATE_DIRECTORY);
$file = $this->createFile($filepath, NULL, $scheme);
$url = file_create_url($file->getFileUri());
$this->assertEqual($url, $expected_url);
if ($scheme == 'private') {
// Tell the implementation of hook_file_download() in file_test.module
// that this file may be downloaded.
file_test_set_return('download', ['x-foo' => 'Bar']);
}
$this->drupalGet($url);
if ($this->assertResponse(200) == 'pass') {
$this->assertRaw(file_get_contents($file->getFileUri()), 'Contents of the file are correct.');
}
$file->delete();
}
}

View file

@ -1,169 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\node\Entity\Node;
use Drupal\user\RoleInterface;
use Drupal\file\Entity\File;
/**
* Confirm that file field submissions work correctly for anonymous visitors.
*
* @group file
*/
class FileFieldAnonymousSubmissionTest extends FileFieldTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Set up permissions for anonymous attacker user.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, [
'create article content' => TRUE,
'access content' => TRUE,
]);
}
/**
* Tests the basic node submission for an anonymous visitor.
*/
public function testAnonymousNode() {
$bundle_label = 'Article';
$node_title = 'test page';
// Load the node form.
$this->drupalLogout();
$this->drupalGet('node/add/article');
$this->assertResponse(200, 'Loaded the article node form.');
$this->assertText(strip_tags(t('Create @name', ['@name' => $bundle_label])));
$edit = [
'title[0][value]' => $node_title,
'body[0][value]' => 'Test article',
];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertResponse(200);
$t_args = ['@type' => $bundle_label, '%title' => $node_title];
$this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.');
$matches = [];
if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) {
$nid = end($matches);
$this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.');
$node = Node::load($nid);
$this->assertNotEqual($node, NULL, 'The node was loaded successfully.');
}
}
/**
* Tests file submission for an anonymous visitor.
*/
public function testAnonymousNodeWithFile() {
$bundle_label = 'Article';
$node_title = 'Test page';
$this->createFileField('field_image', 'node', 'article', [], ['file_extensions' => 'txt png']);
// Load the node form.
$this->drupalLogout();
$this->drupalGet('node/add/article');
$this->assertResponse(200, 'Loaded the article node form.');
$this->assertText(strip_tags(t('Create @name', ['@name' => $bundle_label])));
// Generate an image file.
$image = $this->getTestFile('image');
// Submit the form.
$edit = [
'title[0][value]' => $node_title,
'body[0][value]' => 'Test article',
'files[field_image_0]' => $this->container->get('file_system')->realpath($image->getFileUri()),
];
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertResponse(200);
$t_args = ['@type' => $bundle_label, '%title' => $node_title];
$this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.');
$matches = [];
if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) {
$nid = end($matches);
$this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.');
$node = Node::load($nid);
$this->assertNotEqual($node, NULL, 'The node was loaded successfully.');
$this->assertFileExists(File::load($node->field_image->target_id), 'The image was uploaded successfully.');
}
}
/**
* Tests file submission for an anonymous visitor with a missing node title.
*/
public function testAnonymousNodeWithFileWithoutTitle() {
$this->drupalLogout();
$this->doTestNodeWithFileWithoutTitle();
}
/**
* Tests file submission for an authenticated user with a missing node title.
*/
public function testAuthenticatedNodeWithFileWithoutTitle() {
$admin_user = $this->drupalCreateUser([
'bypass node access',
'access content overview',
'administer nodes',
]);
$this->drupalLogin($admin_user);
$this->doTestNodeWithFileWithoutTitle();
}
/**
* Helper method to test file submissions with missing node titles.
*/
protected function doTestNodeWithFileWithoutTitle() {
$bundle_label = 'Article';
$node_title = 'Test page';
$this->createFileField('field_image', 'node', 'article', [], ['file_extensions' => 'txt png']);
// Load the node form.
$this->drupalGet('node/add/article');
$this->assertResponse(200, 'Loaded the article node form.');
$this->assertText(strip_tags(t('Create @name', ['@name' => $bundle_label])));
// Generate an image file.
$image = $this->getTestFile('image');
// Submit the form but exclude the title field.
$edit = [
'body[0][value]' => 'Test article',
'files[field_image_0]' => $this->container->get('file_system')->realpath($image->getFileUri()),
];
if (!$this->loggedInUser) {
$label = 'Save';
}
else {
$label = 'Save and publish';
}
$this->drupalPostForm(NULL, $edit, $label);
$this->assertResponse(200);
$t_args = ['@type' => $bundle_label, '%title' => $node_title];
$this->assertNoText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.');
$this->assertText('Title field is required.');
// Submit the form again but this time with the missing title field. This
// should still work.
$edit = [
'title[0][value]' => $node_title,
];
$this->drupalPostForm(NULL, $edit, $label);
// Confirm the final submission actually worked.
$t_args = ['@type' => $bundle_label, '%title' => $node_title];
$this->assertText(strip_tags(t('@type %title has been created.', $t_args)), 'The node was created.');
$matches = [];
if (preg_match('@node/(\d+)$@', $this->getUrl(), $matches)) {
$nid = end($matches);
$this->assertNotEqual($nid, 0, 'The node ID was extracted from the URL.');
$node = Node::load($nid);
$this->assertNotEqual($node, NULL, 'The node was loaded successfully.');
$this->assertFileExists(File::load($node->field_image->target_id), 'The image was uploaded successfully.');
}
}
}

View file

@ -1,176 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\file\Entity\File;
/**
* Tests the display of file fields in node and views.
*
* @group file
*/
class FileFieldDisplayTest extends FileFieldTestBase {
/**
* Tests normal formatter display on node display.
*/
public function testNodeDisplay() {
$field_name = strtolower($this->randomMachineName());
$type_name = 'article';
$field_storage_settings = [
'display_field' => '1',
'display_default' => '1',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
];
$field_settings = [
'description_field' => '1',
];
$widget_settings = [];
$this->createFileField($field_name, 'node', $type_name, $field_storage_settings, $field_settings, $widget_settings);
// Create a new node *without* the file field set, and check that the field
// is not shown for each node display.
$node = $this->drupalCreateNode(['type' => $type_name]);
// Check file_default last as the assertions below assume that this is the
// case.
$file_formatters = ['file_table', 'file_url_plain', 'hidden', 'file_default'];
foreach ($file_formatters as $formatter) {
if ($formatter === 'hidden') {
$edit = [
"fields[$field_name][region]" => 'hidden',
];
}
else {
$edit = [
"fields[$field_name][type]" => $formatter,
"fields[$field_name][region]" => 'content',
];
}
$this->drupalPostForm("admin/structure/types/manage/$type_name/display", $edit, t('Save'));
$this->drupalGet('node/' . $node->id());
$this->assertNoText($field_name, format_string('Field label is hidden when no file attached for formatter %formatter', ['%formatter' => $formatter]));
}
$test_file = $this->getTestFile('text');
simpletest_generate_file('escaped-&-text', 64, 10, 'text');
$test_file = File::create([
'uri' => 'public://escaped-&-text.txt',
'name' => 'escaped-&-text',
'filesize' => filesize('public://escaped-&-text.txt'),
]);
// Create a new node with the uploaded file.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
// Check that the default formatter is displaying with the file name.
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$file_link = [
'#theme' => 'file_link',
'#file' => $node_file,
];
$default_output = \Drupal::service('renderer')->renderRoot($file_link);
$this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.');
// Turn the "display" option off and check that the file is no longer displayed.
$edit = [$field_name . '[0][display]' => FALSE];
$this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
$this->assertNoRaw($default_output, 'Field is hidden when "display" option is unchecked.');
// Add a description and make sure that it is displayed.
$description = $this->randomMachineName();
$edit = [
$field_name . '[0][description]' => $description,
$field_name . '[0][display]' => TRUE,
];
$this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
$this->assertText($description);
// Ensure the filename in the link's title attribute is escaped.
$this->assertRaw('title="escaped-&amp;-text.txt"');
// Test that fields appear as expected after during the preview.
// Add a second file.
$name = 'files[' . $field_name . '_1][]';
$edit[$name] = drupal_realpath($test_file->getFileUri());
// Uncheck the display checkboxes and go to the preview.
$edit[$field_name . '[0][display]'] = FALSE;
$edit[$field_name . '[1][display]'] = FALSE;
$this->drupalPostForm("node/$nid/edit", $edit, t('Preview'));
$this->clickLink(t('Back to content editing'));
$this->assertRaw($field_name . '[0][display]', 'First file appears as expected.');
$this->assertRaw($field_name . '[1][display]', 'Second file appears as expected.');
}
/**
* Tests default display of File Field.
*/
public function testDefaultFileFieldDisplay() {
$field_name = strtolower($this->randomMachineName());
$type_name = 'article';
$field_storage_settings = [
'display_field' => '1',
'display_default' => '0',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
];
$field_settings = [
'description_field' => '1',
];
$widget_settings = [];
$this->createFileField($field_name, 'node', $type_name, $field_storage_settings, $field_settings, $widget_settings);
$test_file = $this->getTestFile('text');
// Create a new node with the uploaded file.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$this->drupalGet('node/' . $nid . '/edit');
$this->assertFieldByXPath('//input[@type="checkbox" and @name="' . $field_name . '[0][display]"]', NULL, 'Default file display checkbox field exists.');
$this->assertFieldByXPath('//input[@type="checkbox" and @name="' . $field_name . '[0][display]" and not(@checked)]', NULL, 'Default file display is off.');
}
/**
* Tests description toggle for field instance configuration.
*/
public function testDescToggle() {
$type_name = 'test';
$field_type = 'file';
$field_name = strtolower($this->randomMachineName());
// Use the UI to add a new content type that also contains a file field.
$edit = [
'name' => $type_name,
'type' => $type_name,
];
$this->drupalPostForm('admin/structure/types/add', $edit, t('Save and manage fields'));
$edit = [
'new_storage_type' => $field_type,
'field_name' => $field_name,
'label' => $this->randomString(),
];
$this->drupalPostForm('/admin/structure/types/manage/' . $type_name . '/fields/add-field', $edit, t('Save and continue'));
$this->drupalPostForm(NULL, [], t('Save field settings'));
// Ensure the description field is selected on the field instance settings
// form. That's what this test is all about.
$edit = [
'settings[description_field]' => TRUE,
];
$this->drupalPostForm(NULL, $edit, t('Save settings'));
// Add a node of our new type and upload a file to it.
$file = current($this->drupalGetTestFiles('text'));
$title = $this->randomString();
$edit = [
'title[0][value]' => $title,
'files[field_' . $field_name . '_0]' => drupal_realpath($file->uri),
];
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$node = $this->drupalGetNodeByTitle($title);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertText(t('The description may be used as the label of the link to the file.'));
}
}

View file

@ -1,34 +0,0 @@
<?php
namespace Drupal\file\Tests;
/**
* Tests file formatter access.
* @group file
*/
class FileFieldFormatterAccessTest extends FileFieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'file', 'field_ui', 'file_test'];
/**
* Tests the custom access handler is invoked.
*/
public function testFileAccessHandler() {
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name);
\Drupal::state()->set('file_test_alternate_access_handler', TRUE);
\Drupal::entityManager()->clearCachedDefinitions();
$test_file = $this->getTestFile('text');
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$this->drupalGet('node/' . $nid);
$this->assertTrue(\Drupal::state()->get('file_access_formatter_check', FALSE));
}
}

View file

@ -1,93 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\file\Entity\File;
/**
* Tests that files are uploaded to proper locations.
*
* @group file
*/
class FileFieldPathTest extends FileFieldTestBase {
/**
* Tests the normal formatter display on node display.
*/
public function testUploadPath() {
/** @var \Drupal\node\NodeStorageInterface $node_storage */
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$field_name = strtolower($this->randomMachineName());
$type_name = 'article';
$this->createFileField($field_name, 'node', $type_name);
/** @var \Drupal\file\FileInterface $test_file */
$test_file = $this->getTestFile('text');
// Create a new node.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
// Check that the file was uploaded to the correct location.
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
/** @var \Drupal\file\FileInterface $node_file */
$node_file = $node->{$field_name}->entity;
$date_formatter = $this->container->get('date.formatter');
$expected_filename =
'public://' .
$date_formatter->format(REQUEST_TIME, 'custom', 'Y') . '-' .
$date_formatter->format(REQUEST_TIME, 'custom', 'm') . '/' .
$test_file->getFilename();
$this->assertPathMatch($expected_filename, $node_file->getFileUri(), format_string('The file %file was uploaded to the correct path.', ['%file' => $node_file->getFileUri()]));
// Change the path to contain multiple subdirectories.
$this->updateFileField($field_name, $type_name, ['file_directory' => 'foo/bar/baz']);
// Upload a new file into the subdirectories.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
// Check that the file was uploaded into the subdirectory.
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertPathMatch('public://foo/bar/baz/' . $test_file->getFilename(), $node_file->getFileUri(), format_string('The file %file was uploaded to the correct path.', ['%file' => $node_file->getFileUri()]));
// Check the path when used with tokens.
// Change the path to contain multiple token directories.
$this->updateFileField($field_name, $type_name, ['file_directory' => '[current-user:uid]/[current-user:name]']);
// Upload a new file into the token subdirectories.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
// Check that the file was uploaded into the subdirectory.
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
// Do token replacement using the same user which uploaded the file, not
// the user running the test case.
$data = ['user' => $this->adminUser];
$subdirectory = \Drupal::token()->replace('[user:uid]/[user:name]', $data);
$this->assertPathMatch('public://' . $subdirectory . '/' . $test_file->getFilename(), $node_file->getFileUri(), format_string('The file %file was uploaded to the correct path with token replacements.', ['%file' => $node_file->getFileUri()]));
}
/**
* Asserts that a file is uploaded to the right location.
*
* @param string $expected_path
* The location where the file is expected to be uploaded. Duplicate file
* names to not need to be taken into account.
* @param string $actual_path
* Where the file was actually uploaded.
* @param string $message
* The message to display with this assertion.
*/
public function assertPathMatch($expected_path, $actual_path, $message) {
// Strip off the extension of the expected path to allow for _0, _1, etc.
// suffixes when the file hits a duplicate name.
$pos = strrpos($expected_path, '.');
$base_path = substr($expected_path, 0, $pos);
$extension = substr($expected_path, $pos + 1);
$result = preg_match('/' . preg_quote($base_path, '/') . '(_[0-9]+)?\.' . preg_quote($extension, '/') . '/', $actual_path);
$this->assertTrue($result, $message);
}
}

View file

@ -1,71 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\file\Entity\File;
/**
* Ensure that files added to nodes appear correctly in RSS feeds.
*
* @group file
*/
class FileFieldRSSContentTest extends FileFieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'views'];
/**
* Tests RSS enclosure formatter display for RSS feeds.
*/
public function testFileFieldRSSContent() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$field_name = strtolower($this->randomMachineName());
$type_name = 'article';
$this->createFileField($field_name, 'node', $type_name);
// RSS display must be added manually.
$this->drupalGet("admin/structure/types/manage/$type_name/display");
$edit = [
"display_modes_custom[rss]" => '1',
];
$this->drupalPostForm(NULL, $edit, t('Save'));
// Change the format to 'RSS enclosure'.
$this->drupalGet("admin/structure/types/manage/$type_name/display/rss");
$edit = [
"fields[$field_name][type]" => 'file_rss_enclosure',
"fields[$field_name][region]" => 'content',
];
$this->drupalPostForm(NULL, $edit, t('Save'));
// Create a new node with a file field set. Promote to frontpage
// needs to be set so this node will appear in the RSS feed.
$node = $this->drupalCreateNode(['type' => $type_name, 'promote' => 1]);
$test_file = $this->getTestFile('text');
// Create a new node with the uploaded file.
$nid = $this->uploadNodeFile($test_file, $field_name, $node->id());
// Get the uploaded file from the node.
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
// Check that the RSS enclosure appears in the RSS feed.
$this->drupalGet('rss.xml');
$uploaded_filename = str_replace('public://', '', $node_file->getFileUri());
$selector = sprintf(
'enclosure[url="%s"][length="%s"][type="%s"]',
file_create_url("public://$uploaded_filename", ['absolute' => TRUE]),
$node_file->getSize(),
$node_file->getMimeType()
);
$this->assertTrue(!empty($this->cssSelect($selector)), 'File field RSS enclosure is displayed when viewing the RSS feed.');
}
}

View file

@ -1,142 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\file\Entity\File;
/**
* Tests creating and deleting revisions with files attached.
*
* @group file
*/
class FileFieldRevisionTest extends FileFieldTestBase {
/**
* Tests creating multiple revisions of a node and managing attached files.
*
* Expected behaviors:
* - Adding a new revision will make another entry in the field table, but
* the original file will not be duplicated.
* - Deleting a revision should not delete the original file if the file
* is in use by another revision.
* - When the last revision that uses a file is deleted, the original file
* should be deleted also.
*/
public function testRevisions() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name);
// Create the same fields for users.
$this->createFileField($field_name, 'user', 'user');
$test_file = $this->getTestFile('text');
// Create a new node with the uploaded file.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
// Check that the file exists on disk and in the database.
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file_r1 = File::load($node->{$field_name}->target_id);
$node_vid_r1 = $node->getRevisionId();
$this->assertFileExists($node_file_r1, 'New file saved to disk on node creation.');
$this->assertFileEntryExists($node_file_r1, 'File entry exists in database on node creation.');
$this->assertFileIsPermanent($node_file_r1, 'File is permanent.');
// Upload another file to the same node in a new revision.
$this->replaceNodeFile($test_file, $field_name, $nid);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file_r2 = File::load($node->{$field_name}->target_id);
$node_vid_r2 = $node->getRevisionId();
$this->assertFileExists($node_file_r2, 'Replacement file exists on disk after creating new revision.');
$this->assertFileEntryExists($node_file_r2, 'Replacement file entry exists in database after creating new revision.');
$this->assertFileIsPermanent($node_file_r2, 'Replacement file is permanent.');
// Check that the original file is still in place on the first revision.
$node = node_revision_load($node_vid_r1);
$current_file = File::load($node->{$field_name}->target_id);
$this->assertEqual($node_file_r1->id(), $current_file->id(), 'Original file still in place after replacing file in new revision.');
$this->assertFileExists($node_file_r1, 'Original file still in place after replacing file in new revision.');
$this->assertFileEntryExists($node_file_r1, 'Original file entry still in place after replacing file in new revision');
$this->assertFileIsPermanent($node_file_r1, 'Original file is still permanent.');
// Save a new version of the node without any changes.
// Check that the file is still the same as the previous revision.
$this->drupalPostForm('node/' . $nid . '/edit', ['revision' => '1'], t('Save and keep published'));
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file_r3 = File::load($node->{$field_name}->target_id);
$node_vid_r3 = $node->getRevisionId();
$this->assertEqual($node_file_r2->id(), $node_file_r3->id(), 'Previous revision file still in place after creating a new revision without a new file.');
$this->assertFileIsPermanent($node_file_r3, 'New revision file is permanent.');
// Revert to the first revision and check that the original file is active.
$this->drupalPostForm('node/' . $nid . '/revisions/' . $node_vid_r1 . '/revert', [], t('Revert'));
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file_r4 = File::load($node->{$field_name}->target_id);
$this->assertEqual($node_file_r1->id(), $node_file_r4->id(), 'Original revision file still in place after reverting to the original revision.');
$this->assertFileIsPermanent($node_file_r4, 'Original revision file still permanent after reverting to the original revision.');
// Delete the second revision and check that the file is kept (since it is
// still being used by the third revision).
$this->drupalPostForm('node/' . $nid . '/revisions/' . $node_vid_r2 . '/delete', [], t('Delete'));
$this->assertFileExists($node_file_r3, 'Second file is still available after deleting second revision, since it is being used by the third revision.');
$this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting second revision, since it is being used by the third revision.');
$this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting second revision, since it is being used by the third revision.');
// Attach the second file to a user.
$user = $this->drupalCreateUser();
$user->$field_name->target_id = $node_file_r3->id();
$user->$field_name->display = 1;
$user->save();
$this->drupalGet('user/' . $user->id() . '/edit');
// Delete the third revision and check that the file is not deleted yet.
$this->drupalPostForm('node/' . $nid . '/revisions/' . $node_vid_r3 . '/delete', [], t('Delete'));
$this->assertFileExists($node_file_r3, 'Second file is still available after deleting third revision, since it is being used by the user.');
$this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting third revision, since it is being used by the user.');
$this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting third revision, since it is being used by the user.');
// Delete the user and check that the file is also deleted.
$user->delete();
// TODO: This seems like a bug in File API. Clearing the stat cache should
// not be necessary here. The file really is deleted, but stream wrappers
// doesn't seem to think so unless we clear the PHP file stat() cache.
clearstatcache($node_file_r1->getFileUri());
clearstatcache($node_file_r2->getFileUri());
clearstatcache($node_file_r3->getFileUri());
clearstatcache($node_file_r4->getFileUri());
// Call file_cron() to clean up the file. Make sure the changed timestamp
// of the file is older than the system.file.temporary_maximum_age
// configuration value.
db_update('file_managed')
->fields([
'changed' => REQUEST_TIME - ($this->config('system.file')->get('temporary_maximum_age') + 1),
])
->condition('fid', $node_file_r3->id())
->execute();
\Drupal::service('cron')->run();
$this->assertFileNotExists($node_file_r3, 'Second file is now deleted after deleting third revision, since it is no longer being used by any other nodes.');
$this->assertFileEntryNotExists($node_file_r3, 'Second file entry is now deleted after deleting third revision, since it is no longer being used by any other nodes.');
// Delete the entire node and check that the original file is deleted.
$this->drupalPostForm('node/' . $nid . '/delete', [], t('Delete'));
// Call file_cron() to clean up the file. Make sure the changed timestamp
// of the file is older than the system.file.temporary_maximum_age
// configuration value.
db_update('file_managed')
->fields([
'changed' => REQUEST_TIME - ($this->config('system.file')->get('temporary_maximum_age') + 1),
])
->condition('fid', $node_file_r1->id())
->execute();
\Drupal::service('cron')->run();
$this->assertFileNotExists($node_file_r1, 'Original file is deleted after deleting the entire node with two revisions remaining.');
$this->assertFileEntryNotExists($node_file_r1, 'Original file entry is deleted after deleting the entire node with two revisions remaining.');
}
}

View file

@ -2,6 +2,8 @@
namespace Drupal\file\Tests;
@trigger_error('The ' . __NAMESPACE__ . '\FileFieldTestBase is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\file\Functional\FileFieldTestBase. See https://www.drupal.org/node/2969361.', E_USER_DEPRECATED);
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\file\FileInterface;
@ -227,7 +229,7 @@ abstract class FileFieldTestBase extends WebTestBase {
$edit[$name][] = $file_path;
}
}
$this->drupalPostForm("node/$nid/edit", $edit, t('Save and keep published'));
$this->drupalPostForm("node/$nid/edit", $edit, t('Save'));
return $nid;
}
@ -243,7 +245,7 @@ abstract class FileFieldTestBase extends WebTestBase {
];
$this->drupalPostForm('node/' . $nid . '/edit', [], t('Remove'));
$this->drupalPostForm(NULL, $edit, t('Save and keep published'));
$this->drupalPostForm(NULL, $edit, t('Save'));
}
/**
@ -251,12 +253,12 @@ abstract class FileFieldTestBase extends WebTestBase {
*/
public function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) {
$edit = [
'files[' . $field_name . '_0]' => drupal_realpath($file->getFileUri()),
'files[' . $field_name . '_0]' => \Drupal::service('file_system')->realpath($file->getFileUri()),
'revision' => (string) (int) $new_revision,
];
$this->drupalPostForm('node/' . $nid . '/edit', [], t('Remove'));
$this->drupalPostForm(NULL, $edit, t('Save and keep published'));
$this->drupalPostForm(NULL, $edit, t('Save'));
}
/**

View file

@ -1,188 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\file\Entity\File;
/**
* Tests validation functions such as file type, max file size, max size per
* node, and required.
*
* @group file
*/
class FileFieldValidateTest extends FileFieldTestBase {
/**
* Tests the required property on file fields.
*/
public function testRequired() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$storage = $this->createFileField($field_name, 'node', $type_name, [], ['required' => '1']);
$field = FieldConfig::loadByName('node', $type_name, $field_name);
$test_file = $this->getTestFile('text');
// Try to post a new node without uploading a file.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$this->assertRaw(t('@title field is required.', ['@title' => $field->getLabel()]), 'Node save failed when required file field was empty.');
// Create a new node with the uploaded file.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$this->assertTrue($nid !== FALSE, format_string('uploadNodeFile(@test_file, @field_name, @type_name) succeeded', ['@test_file' => $test_file->getFileUri(), '@field_name' => $field_name, '@type_name' => $type_name]));
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'File exists after uploading to the required field.');
$this->assertFileEntryExists($node_file, 'File entry exists after uploading to the required field.');
// Try again with a multiple value field.
$storage->delete();
$this->createFileField($field_name, 'node', $type_name, ['cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED], ['required' => '1']);
// Try to post a new node without uploading a file in the multivalue field.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$this->assertRaw(t('@title field is required.', ['@title' => $field->getLabel()]), 'Node save failed when required multiple value file field was empty.');
// Create a new node with the uploaded file into the multivalue field.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'File exists after uploading to the required multiple value field.');
$this->assertFileEntryExists($node_file, 'File entry exists after uploading to the required multiple value field.');
}
/**
* Tests the max file size validator.
*/
public function testFileMaxSize() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name, [], ['required' => '1']);
$small_file = $this->getTestFile('text', 131072); // 128KB.
$large_file = $this->getTestFile('text', 1310720); // 1.2MB
// Test uploading both a large and small file with different increments.
$sizes = [
'1M' => 1048576,
'1024K' => 1048576,
'1048576' => 1048576,
];
foreach ($sizes as $max_filesize => $file_limit) {
// Set the max file upload size.
$this->updateFileField($field_name, $type_name, ['max_filesize' => $max_filesize]);
// Create a new node with the small file, which should pass.
$nid = $this->uploadNodeFile($small_file, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, format_string('File exists after uploading a file (%filesize) under the max limit (%maxsize).', ['%filesize' => format_size($small_file->getSize()), '%maxsize' => $max_filesize]));
$this->assertFileEntryExists($node_file, format_string('File entry exists after uploading a file (%filesize) under the max limit (%maxsize).', ['%filesize' => format_size($small_file->getSize()), '%maxsize' => $max_filesize]));
// Check that uploading the large file fails (1M limit).
$this->uploadNodeFile($large_file, $field_name, $type_name);
$error_message = t('The file is %filesize exceeding the maximum file size of %maxsize.', ['%filesize' => format_size($large_file->getSize()), '%maxsize' => format_size($file_limit)]);
$this->assertRaw($error_message, format_string('Node save failed when file (%filesize) exceeded the max upload size (%maxsize).', ['%filesize' => format_size($large_file->getSize()), '%maxsize' => $max_filesize]));
}
// Turn off the max filesize.
$this->updateFileField($field_name, $type_name, ['max_filesize' => '']);
// Upload the big file successfully.
$nid = $this->uploadNodeFile($large_file, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, format_string('File exists after uploading a file (%filesize) with no max limit.', ['%filesize' => format_size($large_file->getSize())]));
$this->assertFileEntryExists($node_file, format_string('File entry exists after uploading a file (%filesize) with no max limit.', ['%filesize' => format_size($large_file->getSize())]));
}
/**
* Tests file extension checking.
*/
public function testFileExtension() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name);
$test_file = $this->getTestFile('image');
list(, $test_file_extension) = explode('.', $test_file->getFilename());
// Disable extension checking.
$this->updateFileField($field_name, $type_name, ['file_extensions' => '']);
// Check that the file can be uploaded with no extension checking.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.');
$this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.');
// Enable extension checking for text files.
$this->updateFileField($field_name, $type_name, ['file_extensions' => 'txt']);
// Check that the file with the wrong extension cannot be uploaded.
$this->uploadNodeFile($test_file, $field_name, $type_name);
$error_message = t('Only files with the following extensions are allowed: %files-allowed.', ['%files-allowed' => 'txt']);
$this->assertRaw($error_message, 'Node save failed when file uploaded with the wrong extension.');
// Enable extension checking for text and image files.
$this->updateFileField($field_name, $type_name, ['file_extensions' => "txt $test_file_extension"]);
// Check that the file can be uploaded with extension checking.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'File exists after uploading a file with extension checking.');
$this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with extension checking.');
}
/**
* Checks that a file can always be removed if it does not pass validation.
*/
public function testFileRemoval() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
$field_name = 'file_test';
$this->createFileField($field_name, 'node', $type_name);
$test_file = $this->getTestFile('image');
// Disable extension checking.
$this->updateFileField($field_name, $type_name, ['file_extensions' => '']);
// Check that the file can be uploaded with no extension checking.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
$this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.');
$this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.');
// Enable extension checking for text files.
$this->updateFileField($field_name, $type_name, ['file_extensions' => 'txt']);
// Check that the file can still be removed.
$this->removeNodeFile($nid);
$this->assertNoText('Only files with the following extensions are allowed: txt.');
$this->assertText('Article ' . $node->getTitle() . ' has been updated.');
}
}

View file

@ -4,7 +4,6 @@ namespace Drupal\file\Tests;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
@ -121,7 +120,7 @@ class FileFieldWidgetTest extends FileFieldTestBase {
$this->assertTrue(isset($label[0]), 'Label for upload found.');
// Save the node and ensure it does not have the file.
$this->drupalPostForm(NULL, [], t('Save and keep published'));
$this->drupalPostForm(NULL, [], t('Save'));
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertTrue(empty($node->{$field_name}->target_id), 'File was successfully removed from the node.');
@ -160,7 +159,7 @@ class FileFieldWidgetTest extends FileFieldTestBase {
$this->drupalGet("node/add/$type_name");
foreach ([$field_name2, $field_name] as $each_field_name) {
for ($delta = 0; $delta < 3; $delta++) {
$edit = ['files[' . $each_field_name . '_' . $delta . '][]' => drupal_realpath($test_file->getFileUri())];
$edit = ['files[' . $each_field_name . '_' . $delta . '][]' => \Drupal::service('file_system')->realpath($test_file->getFileUri())];
// If the Upload button doesn't exist, drupalPostForm() will automatically
// fail with an assertion message.
$this->drupalPostForm(NULL, $edit, t('Upload'));
@ -238,8 +237,7 @@ class FileFieldWidgetTest extends FileFieldTestBase {
$this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), format_string('After removing all files, there is no "Remove" button displayed (JSMode=%type).', ['%type' => $type]));
// Save the node and ensure it does not have any files.
$this->drupalPostForm(NULL, ['title[0][value]' => $this->randomMachineName()], t('Save and publish'));
$matches = [];
$this->drupalPostForm(NULL, ['title[0][value]' => $this->randomMachineName()], t('Save'));
preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
$nid = $matches[1];
$node_storage->resetCache([$nid]);
@ -271,7 +269,7 @@ class FileFieldWidgetTest extends FileFieldTestBase {
// Try to upload exactly the allowed number of files on revision. Create an
// empty node first, to fill it in its first revision.
$node = $this->drupalCreateNode([
'type' => $type_name
'type' => $type_name,
]);
$this->uploadNodeFile($test_file, $field_name, $node->id(), 1);
$node_storage->resetCache([$nid]);
@ -368,13 +366,13 @@ class FileFieldWidgetTest extends FileFieldTestBase {
$edit = [
'title[0][value]' => $this->randomMachineName(),
];
$this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
$this->drupalPostForm('node/add/article', $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
// Add a comment with a file.
$text_file = $this->getTestFile('text');
$edit = [
'files[field_' . $name . '_' . 0 . ']' => drupal_realpath($text_file->getFileUri()),
'files[field_' . $name . '_' . 0 . ']' => \Drupal::service('file_system')->realpath($text_file->getFileUri()),
'comment_body[0][value]' => $comment_body = $this->randomMachineName(),
];
$this->drupalPostForm('node/' . $node->id(), $edit, t('Save'));
@ -402,7 +400,8 @@ class FileFieldWidgetTest extends FileFieldTestBase {
// Unpublishes node.
$this->drupalLogin($this->adminUser);
$this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save and unpublish'));
$edit = ['status[value]' => FALSE];
$this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
// Ensures normal user can no longer download the file.
$this->drupalLogin($user);
@ -429,7 +428,7 @@ class FileFieldWidgetTest extends FileFieldTestBase {
$name = 'files[' . $field_name . '_0]';
// Upload file with incorrect extension, check for validation error.
$edit[$name] = drupal_realpath($test_file_image->getFileUri());
$edit[$name] = \Drupal::service('file_system')->realpath($test_file_image->getFileUri());
switch ($type) {
case 'nojs':
$this->drupalPostForm(NULL, $edit, t('Upload'));
@ -443,7 +442,7 @@ class FileFieldWidgetTest extends FileFieldTestBase {
$this->assertRaw($error_message, t('Validation error when file with wrong extension uploaded (JSMode=%type).', ['%type' => $type]));
// Upload file with correct extension, check that error message is removed.
$edit[$name] = drupal_realpath($test_file_text->getFileUri());
$edit[$name] = \Drupal::service('file_system')->realpath($test_file_text->getFileUri());
switch ($type) {
case 'nojs':
$this->drupalPostForm(NULL, $edit, t('Upload'));
@ -461,7 +460,7 @@ class FileFieldWidgetTest extends FileFieldTestBase {
* Tests file widget element.
*/
public function testWidgetElement() {
$field_name = Unicode::strtolower($this->randomMachineName());
$field_name = mb_strtolower($this->randomMachineName());
$html_name = str_replace('_', '-', $field_name);
$this->createFileField($field_name, 'node', 'article', ['cardinality' => FieldStorageConfig::CARDINALITY_UNLIMITED]);
$file = $this->getTestFile('text');

View file

@ -1,222 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\node\Entity\Node;
use Drupal\file\Entity\File;
use Drupal\entity_test\Entity\EntityTestConstraints;
/**
* Tests file listing page functionality.
*
* @group file
*/
class FileListingTest extends FileFieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['views', 'file', 'image', 'entity_test'];
/**
* An authenticated user.
*
* @var \Drupal\user\UserInterface
*/
protected $baseUser;
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['access files overview', 'bypass node access']);
$this->baseUser = $this->drupalCreateUser();
$this->createFileField('file', 'node', 'article', [], ['file_extensions' => 'txt png']);
}
/**
* Calculates total count of usages for a file.
*
* @param $usage array
* Array of file usage information as returned from file_usage subsystem.
* @return int
* Total usage count.
*/
protected function sumUsages($usage) {
$count = 0;
foreach ($usage as $module) {
foreach ($module as $entity_type) {
foreach ($entity_type as $entity) {
$count += $entity;
}
}
}
return $count;
}
/**
* Tests file overview with different user permissions.
*/
public function testFileListingPages() {
$file_usage = $this->container->get('file.usage');
// Users without sufficient permissions should not see file listing.
$this->drupalLogin($this->baseUser);
$this->drupalGet('admin/content/files');
$this->assertResponse(403);
// Log in with user with right permissions and test listing.
$this->drupalLogin($this->adminUser);
for ($i = 0; $i < 5; $i++) {
$nodes[] = $this->drupalCreateNode(['type' => 'article']);
}
$this->drupalGet('admin/content/files');
$this->assertResponse(200);
$this->assertText('No files available.');
$this->drupalGet('admin/content/files');
$this->assertResponse(200);
// Create a file with no usage.
$file = $this->createFile();
$this->drupalGet('admin/content/files/usage/' . $file->id());
$this->assertResponse(200);
$this->assertTitle(t('File usage information for @file | Drupal', ['@file' => $file->getFilename()]));
foreach ($nodes as &$node) {
$this->drupalGet('node/' . $node->id() . '/edit');
$file = $this->getTestFile('image');
$edit = [
'files[file_0]' => drupal_realpath($file->getFileUri()),
];
$this->drupalPostForm(NULL, $edit, t('Save'));
$node = Node::load($node->id());
}
$this->drupalGet('admin/content/files');
foreach ($nodes as $node) {
$file = File::load($node->file->target_id);
$this->assertText($file->getFilename());
$this->assertLinkByHref(file_create_url($file->getFileUri()));
$this->assertLinkByHref('admin/content/files/usage/' . $file->id());
}
$this->assertFalse(preg_match('/views-field-status priority-low\">\s*' . t('Temporary') . '/', $this->getRawContent()), 'All files are stored as permanent.');
// Use one file two times and check usage information.
$orphaned_file = $nodes[1]->file->target_id;
$used_file = $nodes[0]->file->target_id;
$nodes[1]->file->target_id = $used_file;
$nodes[1]->save();
$this->drupalGet('admin/content/files');
$file = File::load($orphaned_file);
$usage = $this->sumUsages($file_usage->listUsage($file));
$this->assertRaw('admin/content/files/usage/' . $file->id() . '">' . $usage);
$file = File::load($used_file);
$usage = $this->sumUsages($file_usage->listUsage($file));
$this->assertRaw('admin/content/files/usage/' . $file->id() . '">' . $usage);
$result = $this->xpath("//td[contains(@class, 'views-field-status') and contains(text(), :value)]", [':value' => t('Temporary')]);
$this->assertEqual(1, count($result), 'Unused file marked as temporary.');
// Test file usage page.
foreach ($nodes as $node) {
$file = File::load($node->file->target_id);
$usage = $file_usage->listUsage($file);
$this->drupalGet('admin/content/files/usage/' . $file->id());
$this->assertResponse(200);
$this->assertText($node->getTitle(), 'Node title found on usage page.');
$this->assertText('node', 'Registering entity type found on usage page.');
$this->assertText('file', 'Registering module found on usage page.');
foreach ($usage as $module) {
foreach ($module as $entity_type) {
foreach ($entity_type as $entity) {
$this->assertText($entity, 'Usage count found on usage page.');
}
}
}
$this->assertLinkByHref('node/' . $node->id(), 0, 'Link to registering entity found on usage page.');
}
}
/**
* Tests file listing usage page for entities with no canonical link template.
*/
public function testFileListingUsageNoLink() {
// Login with user with right permissions and test listing.
$this->drupalLogin($this->adminUser);
// Create a bundle and attach a File field to the bundle.
$bundle = $this->randomMachineName();
entity_test_create_bundle($bundle, NULL, 'entity_test_constraints');
$this->createFileField('field_test_file', 'entity_test_constraints', $bundle, [], ['file_extensions' => 'txt png']);
// Create file to attach to entity.
$file = File::create([
'filename' => 'druplicon.txt',
'uri' => 'public://druplicon.txt',
'filemime' => 'text/plain',
]);
$file->setPermanent();
file_put_contents($file->getFileUri(), 'hello world');
$file->save();
// Create entity and attach the created file.
$entity_name = $this->randomMachineName();
$entity = EntityTestConstraints::create([
'uid' => 1,
'name' => $entity_name,
'type' => $bundle,
'field_test_file' => [
'target_id' => $file->id(),
],
]);
$entity->save();
// Create node entity and attach the created file.
$node = $this->drupalCreateNode(['type' => 'article', 'file' => $file]);
$node->save();
// Load the file usage page for the created and attached file.
$this->drupalGet('admin/content/files/usage/' . $file->id());
$this->assertResponse(200);
// Entity name should be displayed, but not linked if Entity::toUrl
// throws an exception
$this->assertText($entity_name, 'Entity name is added to file usage listing.');
$this->assertNoLink($entity_name, 'Linked entity name not added to file usage listing.');
$this->assertLink($node->getTitle());
}
/**
* Creates and saves a test file.
*
* @return \Drupal\Core\Entity\EntityInterface
* A file entity.
*/
protected function createFile() {
// Create a new file entity.
$file = File::create([
'uid' => 1,
'filename' => 'druplicon.txt',
'uri' => 'public://druplicon.txt',
'filemime' => 'text/plain',
'created' => 1,
'changed' => 1,
'status' => FILE_STATUS_PERMANENT,
]);
file_put_contents($file->getFileUri(), 'hello world');
// Save it, inserting a new record.
$file->save();
return $file;
}
}

View file

@ -1,197 +0,0 @@
<?php
namespace Drupal\file\Tests;
/**
* Tests the 'managed_file' element type.
*
* @group file
* @todo Create a FileTestBase class and move FileFieldTestBase methods
* that aren't related to fields into it.
*/
class FileManagedFileElementTest extends FileFieldTestBase {
/**
* Tests the managed_file element type.
*/
public function testManagedFile() {
// Check that $element['#size'] is passed to the child upload element.
$this->drupalGet('file/test');
$this->assertFieldByXpath('//input[@name="files[nested_file]" and @size="13"]', NULL, 'The custom #size attribute is passed to the child upload element.');
// Perform the tests with all permutations of $form['#tree'],
// $element['#extended'], and $element['#multiple'].
$test_file = $this->getTestFile('text');
foreach ([0, 1] as $tree) {
foreach ([0, 1] as $extended) {
foreach ([0, 1] as $multiple) {
$path = 'file/test/' . $tree . '/' . $extended . '/' . $multiple;
$input_base_name = $tree ? 'nested_file' : 'file';
$file_field_name = $multiple ? 'files[' . $input_base_name . '][]' : 'files[' . $input_base_name . ']';
// Submit without a file.
$this->drupalPostForm($path, [], t('Save'));
$this->assertRaw(t('The file ids are %fids.', ['%fids' => implode(',', [])]), 'Submitted without a file.');
// Submit with a file, but with an invalid form token. Ensure the file
// was not saved.
$last_fid_prior = $this->getLastFileId();
$edit = [
$file_field_name => drupal_realpath($test_file->getFileUri()),
'form_token' => 'invalid token',
];
$this->drupalPostForm($path, $edit, t('Save'));
$this->assertText('The form has become outdated. Copy any unsaved work in the form below');
$last_fid = $this->getLastFileId();
$this->assertEqual($last_fid_prior, $last_fid, 'File was not saved when uploaded with an invalid form token.');
// Submit a new file, without using the Upload button.
$last_fid_prior = $this->getLastFileId();
$edit = [$file_field_name => drupal_realpath($test_file->getFileUri())];
$this->drupalPostForm($path, $edit, t('Save'));
$last_fid = $this->getLastFileId();
$this->assertTrue($last_fid > $last_fid_prior, 'New file got saved.');
$this->assertRaw(t('The file ids are %fids.', ['%fids' => implode(',', [$last_fid])]), 'Submit handler has correct file info.');
// Submit no new input, but with a default file.
$this->drupalPostForm($path . '/' . $last_fid, [], t('Save'));
$this->assertRaw(t('The file ids are %fids.', ['%fids' => implode(',', [$last_fid])]), 'Empty submission did not change an existing file.');
// Now, test the Upload and Remove buttons, with and without Ajax.
foreach ([FALSE, TRUE] as $ajax) {
// Upload, then Submit.
$last_fid_prior = $this->getLastFileId();
$this->drupalGet($path);
$edit = [$file_field_name => drupal_realpath($test_file->getFileUri())];
if ($ajax) {
$this->drupalPostAjaxForm(NULL, $edit, $input_base_name . '_upload_button');
}
else {
$this->drupalPostForm(NULL, $edit, t('Upload'));
}
$last_fid = $this->getLastFileId();
$this->assertTrue($last_fid > $last_fid_prior, 'New file got uploaded.');
$this->drupalPostForm(NULL, [], t('Save'));
$this->assertRaw(t('The file ids are %fids.', ['%fids' => implode(',', [$last_fid])]), 'Submit handler has correct file info.');
// Remove, then Submit.
$remove_button_title = $multiple ? t('Remove selected') : t('Remove');
$remove_edit = [];
if ($multiple) {
$selected_checkbox = ($tree ? 'nested[file]' : 'file') . '[file_' . $last_fid . '][selected]';
$remove_edit = [$selected_checkbox => '1'];
}
$this->drupalGet($path . '/' . $last_fid);
if ($ajax) {
$this->drupalPostAjaxForm(NULL, $remove_edit, $input_base_name . '_remove_button');
}
else {
$this->drupalPostForm(NULL, $remove_edit, $remove_button_title);
}
$this->drupalPostForm(NULL, [], t('Save'));
$this->assertRaw(t('The file ids are %fids.', ['%fids' => '']), 'Submission after file removal was successful.');
// Upload, then Remove, then Submit.
$this->drupalGet($path);
$edit = [$file_field_name => drupal_realpath($test_file->getFileUri())];
if ($ajax) {
$this->drupalPostAjaxForm(NULL, $edit, $input_base_name . '_upload_button');
}
else {
$this->drupalPostForm(NULL, $edit, t('Upload'));
}
$remove_edit = [];
if ($multiple) {
$selected_checkbox = ($tree ? 'nested[file]' : 'file') . '[file_' . $this->getLastFileId() . '][selected]';
$remove_edit = [$selected_checkbox => '1'];
}
if ($ajax) {
$this->drupalPostAjaxForm(NULL, $remove_edit, $input_base_name . '_remove_button');
}
else {
$this->drupalPostForm(NULL, $remove_edit, $remove_button_title);
}
$this->drupalPostForm(NULL, [], t('Save'));
$this->assertRaw(t('The file ids are %fids.', ['%fids' => '']), 'Submission after file upload and removal was successful.');
}
}
}
}
// The multiple file upload has additional conditions that need checking.
$path = 'file/test/1/1/1';
$edit = ['files[nested_file][]' => drupal_realpath($test_file->getFileUri())];
$fid_list = [];
$this->drupalGet($path);
// Add a single file to the upload field.
$this->drupalPostForm(NULL, $edit, t('Upload'));
$fid_list[] = $this->getLastFileId();
$this->assertFieldByXpath('//input[@name="nested[file][file_' . $fid_list[0] . '][selected]"]', NULL, 'First file successfully uploaded to multiple file element.');
// Add another file to the same upload field.
$this->drupalPostForm(NULL, $edit, t('Upload'));
$fid_list[] = $this->getLastFileId();
$this->assertFieldByXpath('//input[@name="nested[file][file_' . $fid_list[1] . '][selected]"]', NULL, 'Second file successfully uploaded to multiple file element.');
// Save the entire form.
$this->drupalPostForm(NULL, [], t('Save'));
$this->assertRaw(t('The file ids are %fids.', ['%fids' => implode(',', $fid_list)]), 'Two files saved into a single multiple file element.');
// Delete only the first file.
$edit = [
'nested[file][file_' . $fid_list[0] . '][selected]' => '1',
];
$this->drupalPostForm($path . '/' . implode(',', $fid_list), $edit, t('Remove selected'));
// Check that the first file has been deleted but not the second.
$this->assertNoFieldByXpath('//input[@name="nested[file][file_' . $fid_list[0] . '][selected]"]', NULL, 'An individual file can be deleted from a multiple file element.');
$this->assertFieldByXpath('//input[@name="nested[file][file_' . $fid_list[1] . '][selected]"]', NULL, 'Second individual file not deleted when the first file is deleted from a multiple file element.');
}
/**
* Ensure that warning is shown if file on the field has been removed.
*/
public function testManagedFileRemoved() {
$this->drupalGet('file/test/1/0/1');
$test_file = $this->getTestFile('text');
$file_field_name = 'files[nested_file][]';
$edit = [$file_field_name => drupal_realpath($test_file->getFileUri())];
$this->drupalPostForm(NULL, $edit, t('Upload'));
$fid = $this->getLastFileId();
$file = \Drupal::entityManager()->getStorage('file')->load($fid);
$file->delete();
$this->drupalPostForm(NULL, $edit, t('Upload'));
// We expect the title 'Managed <em>file & butter</em>' which got escaped
// via a t() call before.
$this->assertRaw('The file referenced by the Managed <em>file &amp; butter</em> field does not exist.');
}
/**
* Ensure a file entity can be saved when the file does not exist on disk.
*/
public function testFileRemovedFromDisk() {
$this->drupalGet('file/test/1/0/1');
$test_file = $this->getTestFile('text');
$file_field_name = 'files[nested_file][]';
$edit = [$file_field_name => drupal_realpath($test_file->getFileUri())];
$this->drupalPostForm(NULL, $edit, t('Upload'));
$this->drupalPostForm(NULL, [], t('Save'));
$fid = $this->getLastFileId();
/** @var $file \Drupal\file\FileInterface */
$file = $this->container->get('entity_type.manager')->getStorage('file')->load($fid);
$file->setPermanent();
$file->save();
$this->assertTrue(file_unmanaged_delete($file->getFileUri()));
$file->save();
$this->assertTrue($file->isPermanent());
$file->delete();
}
}

View file

@ -2,6 +2,8 @@
namespace Drupal\file\Tests;
@trigger_error('The ' . __NAMESPACE__ . '\FileManagedTestBase is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Instead, use \Drupal\Tests\file\Functional\FileManagedTestBase. See https://www.drupal.org/node/2969361.', E_USER_DEPRECATED);
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\simpletest\WebTestBase;

View file

@ -1,200 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\file\Entity\File;
/**
* Uploads files to translated nodes.
*
* @group file
*/
class FileOnTranslatedEntityTest extends FileFieldTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['language', 'content_translation'];
/**
* The name of the file field used in the test.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create the "Basic page" node type.
// @todo Remove the disabling of new revision creation in
// https://www.drupal.org/node/1239558.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page', 'new_revision' => FALSE]);
// Create a file field on the "Basic page" node type.
$this->fieldName = strtolower($this->randomMachineName());
$this->createFileField($this->fieldName, 'node', 'page');
// Create and log in user.
$permissions = [
'access administration pages',
'administer content translation',
'administer content types',
'administer languages',
'create content translations',
'create page content',
'edit any page content',
'translate any entity',
'delete any page content',
];
$admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($admin_user);
// Add a second and third language.
$edit = [];
$edit['predefined_langcode'] = 'fr';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$edit = [];
$edit['predefined_langcode'] = 'nl';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// Enable translation for "Basic page" nodes.
$edit = [
'entity_types[node]' => 1,
'settings[node][page][translatable]' => 1,
"settings[node][page][fields][$this->fieldName]" => 1,
];
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
\Drupal::entityManager()->clearCachedDefinitions();
}
/**
* Tests synced file fields on translated nodes.
*/
public function testSyncedFiles() {
// Verify that the file field on the "Basic page" node type is translatable.
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'page');
$this->assertTrue($definitions[$this->fieldName]->isTranslatable(), 'Node file field is translatable.');
// Create a default language node.
$default_language_node = $this->drupalCreateNode(['type' => 'page', 'title' => 'Lost in translation']);
// Edit the node to upload a file.
$edit = [];
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[0]->uri);
$this->drupalPostForm('node/' . $default_language_node->id() . '/edit', $edit, t('Save'));
$first_fid = $this->getLastFileId();
// Translate the node into French: remove the existing file.
$this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/fr', [], t('Remove'));
// Upload a different file.
$edit = [];
$edit['title[0][value]'] = 'Bill Murray';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[1]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
// This inspects the HTML after the post of the translation, the file
// should be displayed on the original node.
$this->assertRaw('file--mime-text-plain');
$second_fid = $this->getLastFileId();
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
/* @var $file \Drupal\file\FileInterface */
// Ensure the file status of the first file permanent.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the second file is permanent.
$file = File::load($second_fid);
$this->assertTrue($file->isPermanent());
// Translate the node into dutch: remove the existing file.
$this->drupalPostForm('node/' . $default_language_node->id() . '/translations/add/en/nl', [], t('Remove'));
// Upload a different file.
$edit = [];
$edit['title[0][value]'] = 'Scarlett Johansson';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[2]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$third_fid = $this->getLastFileId();
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first file is untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
// This inspects the HTML after the post of the translation, the file
// should be displayed on the original node.
$this->assertRaw('file--mime-text-plain');
// Ensure the file status of the second file is permanent.
$file = File::load($second_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the third file is permanent.
$file = File::load($third_fid);
$this->assertTrue($file->isPermanent());
// Edit the second translation: remove the existing file.
$this->drupalPostForm('fr/node/' . $default_language_node->id() . '/edit', [], t('Remove'));
// Upload a different file.
$edit = [];
$edit['title[0][value]'] = 'David Bowie';
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[3]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$replaced_second_fid = $this->getLastFileId();
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first and third files are untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
$file = File::load($third_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the replaced second file is permanent.
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isPermanent());
// Delete the third translation.
$this->drupalPostForm('nl/node/' . $default_language_node->id() . '/delete', [], t('Delete Dutch translation'));
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the first and replaced second files are untouched.
$file = File::load($first_fid);
$this->assertTrue($file->isPermanent(), 'First file still exists and is permanent.');
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isPermanent());
// Ensure the file status of the third file is now temporary.
$file = File::load($third_fid);
$this->assertTrue($file->isTemporary());
// Delete the all translations.
$this->drupalPostForm('node/' . $default_language_node->id() . '/delete', [], t('Delete all translations'));
\Drupal::entityTypeManager()->getStorage('file')->resetCache();
// Ensure the file status of the all files are now temporary.
$file = File::load($first_fid);
$this->assertTrue($file->isTemporary(), 'First file still exists and is temporary.');
$file = File::load($replaced_second_fid);
$this->assertTrue($file->isTemporary());
}
}

View file

@ -1,228 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\Core\Entity\Plugin\Validation\Constraint\ReferenceAccessConstraint;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\file\Entity\File;
use Drupal\node\Entity\NodeType;
use Drupal\user\RoleInterface;
/**
* Uploads a test to a private node and checks access.
*
* @group file
*/
class FilePrivateTest extends FileFieldTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node_access_test', 'field_test'];
protected function setUp() {
parent::setUp();
node_access_test_add_field(NodeType::load('article'));
node_access_rebuild();
\Drupal::state()->set('node_access_test.private', TRUE);
}
/**
* Tests file access for file uploaded to a private node.
*/
public function testPrivateFile() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$type_name = 'article';
$field_name = strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name, ['uri_scheme' => 'private']);
$test_file = $this->getTestFile('text');
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, ['private' => TRUE]);
\Drupal::entityManager()->getStorage('node')->resetCache([$nid]);
/* @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($nid);
$node_file = File::load($node->{$field_name}->target_id);
// Ensure the file can be viewed.
$this->drupalGet('node/' . $node->id());
$this->assertRaw($node_file->getFilename(), 'File reference is displayed after attaching it');
// Ensure the file can be downloaded.
$this->drupalGet(file_create_url($node_file->getFileUri()));
$this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
$this->drupalLogOut();
$this->drupalGet(file_create_url($node_file->getFileUri()));
$this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
// Create a field with no view access. See
// field_test_entity_field_access().
$no_access_field_name = 'field_no_view_access';
$this->createFileField($no_access_field_name, 'node', $type_name, ['uri_scheme' => 'private']);
// Test with the field that should deny access through field access.
$this->drupalLogin($this->adminUser);
$nid = $this->uploadNodeFile($test_file, $no_access_field_name, $type_name, TRUE, ['private' => TRUE]);
\Drupal::entityManager()->getStorage('node')->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file = File::load($node->{$no_access_field_name}->target_id);
// Ensure the file cannot be downloaded.
$file_url = file_create_url($node_file->getFileUri());
$this->drupalGet($file_url);
$this->assertResponse(403, 'Confirmed that access is denied for the file without view field access permission.');
// Attempt to reuse the file when editing a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$edit[$field_name . '[0][fids]'] = $node_file->id();
$this->drupalPostForm('node/' . $new_node->id() . '/edit', $edit, t('Save and keep published'));
// Make sure the form submit failed - we stayed on the edit form.
$this->assertUrl('node/' . $new_node->id() . '/edit');
// Check that we got the expected constraint form error.
$constraint = new ReferenceAccessConstraint();
$this->assertRaw(SafeMarkup::format($constraint->message, ['%type' => 'file', '%id' => $node_file->id()]));
// Attempt to reuse the existing file when creating a new node, and confirm
// that access is still denied.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$edit[$field_name . '[0][fids]'] = $node_file->id();
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertTrue(empty($new_node), 'Node was not created.');
$this->assertUrl('node/add/' . $type_name);
$this->assertRaw(SafeMarkup::format($constraint->message, ['%type' => 'file', '%id' => $node_file->id()]));
// Now make file_test_file_download() return everything.
\Drupal::state()->set('file_test.allow_all', TRUE);
// Delete the node.
$node->delete();
// Ensure the file can still be downloaded by the owner.
$this->drupalGet($file_url);
$this->assertResponse(200, 'Confirmed that the owner still has access to the temporary file.');
// Ensure the file cannot be downloaded by an anonymous user.
$this->drupalLogout();
$this->drupalGet($file_url);
$this->assertResponse(403, 'Confirmed that access is denied for an anonymous user to the temporary file.');
// Ensure the file cannot be downloaded by another user.
$account = $this->drupalCreateUser();
$this->drupalLogin($account);
$this->drupalGet($file_url);
$this->assertResponse(403, 'Confirmed that access is denied for another user to the temporary file.');
// As an anonymous user, create a temporary file with no references and
// confirm that only the session that uploaded it may view it.
$this->drupalLogout();
user_role_change_permissions(
RoleInterface::ANONYMOUS_ID,
[
"create $type_name content" => TRUE,
'access content' => TRUE,
]
);
$test_file = $this->getTestFile('text');
$this->drupalGet('node/add/' . $type_name);
$edit = ['files[' . $field_name . '_0]' => drupal_realpath($test_file->getFileUri())];
$this->drupalPostForm(NULL, $edit, t('Upload'));
/** @var \Drupal\file\FileStorageInterface $file_storage */
$file_storage = $this->container->get('entity.manager')->getStorage('file');
$files = $file_storage->loadByProperties(['uid' => 0]);
$this->assertEqual(1, count($files), 'Loaded one anonymous file.');
$file = end($files);
$this->assertTrue($file->isTemporary(), 'File is temporary.');
$usage = $this->container->get('file.usage')->listUsage($file);
$this->assertFalse($usage, 'No file usage found.');
$file_url = file_create_url($file->getFileUri());
$this->drupalGet($file_url);
$this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the temporary file.');
// Close the prior connection and remove the session cookie.
$this->curlClose();
$this->curlCookies = [];
$this->cookies = [];
$this->drupalGet($file_url);
$this->assertResponse(403, 'Confirmed that another anonymous user cannot access the temporary file.');
// As an anonymous user, create a permanent file, then remove all
// references to the file (so that it becomes temporary again) and confirm
// that only the session that uploaded it may view it.
$test_file = $this->getTestFile('text');
$this->drupalGet('node/add/' . $type_name);
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$edit['files[' . $field_name . '_0]'] = drupal_realpath($test_file->getFileUri());
$this->drupalPostForm(NULL, $edit, t('Save'));
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$file_id = $new_node->{$field_name}->target_id;
$file = File::load($file_id);
$this->assertTrue($file->isPermanent(), 'File is permanent.');
// Remove the reference to this file.
$new_node->{$field_name} = [];
$new_node->save();
$file = File::load($file_id);
$this->assertTrue($file->isTemporary(), 'File is temporary.');
$usage = $this->container->get('file.usage')->listUsage($file);
$this->assertFalse($usage, 'No file usage found.');
$file_url = file_create_url($file->getFileUri());
$this->drupalGet($file_url);
$this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the file whose references were removed.');
// Close the prior connection and remove the session cookie.
$this->curlClose();
$this->curlCookies = [];
$this->cookies = [];
$this->drupalGet($file_url);
$this->assertResponse(403, 'Confirmed that another anonymous user cannot access the file whose references were removed.');
// As an anonymous user, create a permanent file that is referenced by a
// published node and confirm that all anonymous users may view it.
$test_file = $this->getTestFile('text');
$this->drupalGet('node/add/' . $type_name);
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$edit['files[' . $field_name . '_0]'] = drupal_realpath($test_file->getFileUri());
$this->drupalPostForm(NULL, $edit, t('Save'));
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$file = File::load($new_node->{$field_name}->target_id);
$this->assertTrue($file->isPermanent(), 'File is permanent.');
$usage = $this->container->get('file.usage')->listUsage($file);
$this->assertTrue($usage, 'File usage found.');
$file_url = file_create_url($file->getFileUri());
$this->drupalGet($file_url);
$this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the permanent file that is referenced by a published node.');
// Close the prior connection and remove the session cookie.
$this->curlClose();
$this->curlCookies = [];
$this->cookies = [];
$this->drupalGet($file_url);
$this->assertResponse(200, 'Confirmed that another anonymous user also has access to the permanent file that is referenced by a published node.');
// As an anonymous user, create a permanent file that is referenced by an
// unpublished node and confirm that no anonymous users may view it (even
// the session that uploaded the file) because they cannot view the
// unpublished node.
$test_file = $this->getTestFile('text');
$this->drupalGet('node/add/' . $type_name);
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$edit['files[' . $field_name . '_0]'] = drupal_realpath($test_file->getFileUri());
$this->drupalPostForm(NULL, $edit, t('Save'));
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$new_node->setPublished(FALSE);
$new_node->save();
$file = File::load($new_node->{$field_name}->target_id);
$this->assertTrue($file->isPermanent(), 'File is permanent.');
$usage = $this->container->get('file.usage')->listUsage($file);
$this->assertTrue($usage, 'File usage found.');
$file_url = file_create_url($file->getFileUri());
$this->drupalGet($file_url);
$this->assertResponse(403, 'Confirmed that the anonymous uploader cannot access the permanent file when it is referenced by an unpublished node.');
// Close the prior connection and remove the session cookie.
$this->curlClose();
$this->curlCookies = [];
$this->cookies = [];
$this->drupalGet($file_url);
$this->assertResponse(403, 'Confirmed that another anonymous user cannot access the permanent file when it is referenced by an unpublished node.');
}
}

View file

@ -1,97 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\file\Entity\File;
/**
* Generates text using placeholders for dummy content to check file token
* replacement.
*
* @group file
*/
class FileTokenReplaceTest extends FileFieldTestBase {
/**
* Creates a file, then tests the tokens generated from it.
*/
public function testFileTokenReplacement() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$token_service = \Drupal::token();
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
// Create file field.
$type_name = 'article';
$field_name = 'field_' . strtolower($this->randomMachineName());
$this->createFileField($field_name, 'node', $type_name);
$test_file = $this->getTestFile('text');
// Coping a file to test uploads with non-latin filenames.
$filename = drupal_dirname($test_file->getFileUri()) . '/текстовый файл.txt';
$test_file = file_copy($test_file, $filename);
// Create a new node with the uploaded file.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
// Load the node and the file.
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$file = File::load($node->{$field_name}->target_id);
// Generate and test sanitized tokens.
$tests = [];
$tests['[file:fid]'] = $file->id();
$tests['[file:name]'] = Html::escape($file->getFilename());
$tests['[file:path]'] = Html::escape($file->getFileUri());
$tests['[file:mime]'] = Html::escape($file->getMimeType());
$tests['[file:size]'] = format_size($file->getSize());
$tests['[file:url]'] = Html::escape(file_create_url($file->getFileUri()));
$tests['[file:created]'] = format_date($file->getCreatedTime(), 'medium', '', NULL, $language_interface->getId());
$tests['[file:created:short]'] = format_date($file->getCreatedTime(), 'short', '', NULL, $language_interface->getId());
$tests['[file:changed]'] = format_date($file->getChangedTime(), 'medium', '', NULL, $language_interface->getId());
$tests['[file:changed:short]'] = format_date($file->getChangedTime(), 'short', '', NULL, $language_interface->getId());
$tests['[file:owner]'] = Html::escape($this->adminUser->getDisplayName());
$tests['[file:owner:uid]'] = $file->getOwnerId();
$base_bubbleable_metadata = BubbleableMetadata::createFromObject($file);
$metadata_tests = [];
$metadata_tests['[file:fid]'] = $base_bubbleable_metadata;
$metadata_tests['[file:name]'] = $base_bubbleable_metadata;
$metadata_tests['[file:path]'] = $base_bubbleable_metadata;
$metadata_tests['[file:mime]'] = $base_bubbleable_metadata;
$metadata_tests['[file:size]'] = $base_bubbleable_metadata;
$bubbleable_metadata = clone $base_bubbleable_metadata;
$metadata_tests['[file:url]'] = $bubbleable_metadata->addCacheContexts(['url.site']);
$bubbleable_metadata = clone $base_bubbleable_metadata;
$metadata_tests['[file:created]'] = $bubbleable_metadata->addCacheTags(['rendered']);
$metadata_tests['[file:created:short]'] = $bubbleable_metadata;
$metadata_tests['[file:changed]'] = $bubbleable_metadata;
$metadata_tests['[file:changed:short]'] = $bubbleable_metadata;
$bubbleable_metadata = clone $base_bubbleable_metadata;
$metadata_tests['[file:owner]'] = $bubbleable_metadata->addCacheTags(['user:2']);
$metadata_tests['[file:owner:uid]'] = $bubbleable_metadata;
// Test to make sure that we generated something for each token.
$this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
foreach ($tests as $input => $expected) {
$bubbleable_metadata = new BubbleableMetadata();
$output = $token_service->replace($input, ['file' => $file], ['langcode' => $language_interface->getId()], $bubbleable_metadata);
$this->assertEqual($output, $expected, format_string('Sanitized file token %token replaced.', ['%token' => $input]));
$this->assertEqual($bubbleable_metadata, $metadata_tests[$input]);
}
// Generate and test unsanitized tokens.
$tests['[file:name]'] = $file->getFilename();
$tests['[file:path]'] = $file->getFileUri();
$tests['[file:mime]'] = $file->getMimeType();
$tests['[file:size]'] = format_size($file->getSize());
foreach ($tests as $input => $expected) {
$output = $token_service->replace($input, ['file' => $file], ['langcode' => $language_interface->getId(), 'sanitize' => FALSE]);
$this->assertEqual($output, $expected, format_string('Unsanitized file token %token replaced.', ['%token' => $input]));
}
}
}

View file

@ -1,125 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\file\Entity\File;
use Drupal\node\Entity\Node;
/**
* Uploads private files to translated node and checks access.
*
* @group file
*/
class PrivateFileOnTranslatedEntityTest extends FileFieldTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['language', 'content_translation'];
/**
* The name of the file field used in the test.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create the "Basic page" node type.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create a file field on the "Basic page" node type.
$this->fieldName = strtolower($this->randomMachineName());
$this->createFileField($this->fieldName, 'node', 'page', ['uri_scheme' => 'private']);
// Create and log in user.
$permissions = [
'access administration pages',
'administer content translation',
'administer content types',
'administer languages',
'create content translations',
'create page content',
'edit any page content',
'translate any entity',
];
$admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($admin_user);
// Add a second language.
$edit = [];
$edit['predefined_langcode'] = 'fr';
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// Enable translation for "Basic page" nodes.
$edit = [
'entity_types[node]' => 1,
'settings[node][page][translatable]' => 1,
"settings[node][page][fields][$this->fieldName]" => 1,
];
$this->drupalPostForm('admin/config/regional/content-language', $edit, t('Save configuration'));
\Drupal::entityManager()->clearCachedDefinitions();
}
/**
* Tests private file fields on translated nodes.
*/
public function testPrivateLanguageFile() {
// Verify that the file field on the "Basic page" node type is translatable.
$definitions = \Drupal::entityManager()->getFieldDefinitions('node', 'page');
$this->assertTrue($definitions[$this->fieldName]->isTranslatable(), 'Node file field is translatable.');
// Create a default language node.
$default_language_node = $this->drupalCreateNode(['type' => 'page']);
// Edit the node to upload a file.
$edit = [];
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[0]->uri);
$this->drupalPostForm('node/' . $default_language_node->id() . '/edit', $edit, t('Save'));
$last_fid_prior = $this->getLastFileId();
// Languages are cached on many levels, and we need to clear those caches.
$this->rebuildContainer();
// Ensure the file can be downloaded.
\Drupal::entityManager()->getStorage('node')->resetCache([$default_language_node->id()]);
$node = Node::load($default_language_node->id());
$node_file = File::load($node->{$this->fieldName}->target_id);
$this->drupalGet(file_create_url($node_file->getFileUri()));
$this->assertResponse(200, 'Confirmed that the file attached to the English node can be downloaded.');
// Translate the node into French.
$this->drupalGet('node/' . $default_language_node->id() . '/translations');
$this->clickLink(t('Add'));
// Remove the existing file.
$this->drupalPostForm(NULL, [], t('Remove'));
// Upload a different file.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$name = 'files[' . $this->fieldName . '_0]';
$edit[$name] = drupal_realpath($this->drupalGetTestFiles('text')[1]->uri);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$last_fid = $this->getLastFileId();
// Verify the translation was created.
\Drupal::entityManager()->getStorage('node')->resetCache([$default_language_node->id()]);
$default_language_node = Node::load($default_language_node->id());
$this->assertTrue($default_language_node->hasTranslation('fr'), 'Node found in database.');
$this->assertTrue($last_fid > $last_fid_prior, 'New file got saved.');
// Ensure the file attached to the translated node can be downloaded.
$french_node = $default_language_node->getTranslation('fr');
$node_file = File::load($french_node->{$this->fieldName}->target_id);
$this->drupalGet(file_create_url($node_file->getFileUri()));
$this->assertResponse(200, 'Confirmed that the file attached to the French node can be downloaded.');
}
}

View file

@ -1,24 +0,0 @@
<?php
namespace Drupal\file\Tests;
/**
* Tests the file uploading functions.
*
* @group file
*/
class RemoteFileSaveUploadTest extends SaveUploadTest {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['file_test'];
protected function setUp() {
parent::setUp();
$this->config('system.file')->set('default_scheme', 'dummy-remote')->save();
}
}

View file

@ -1,362 +0,0 @@
<?php
namespace Drupal\file\Tests;
use Drupal\file\Entity\File;
/**
* Tests the file_save_upload() function.
*
* @group file
*/
class SaveUploadTest extends FileManagedTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['dblog'];
/**
* An image file path for uploading.
*
* @var \Drupal\file\FileInterface
*/
protected $image;
/**
* A PHP file path for upload security testing.
*/
protected $phpfile;
/**
* The largest file id when the test starts.
*/
protected $maxFidBefore;
/**
* Extension of the image filename.
*
* @var string
*/
protected $imageExtension;
protected function setUp() {
parent::setUp();
$account = $this->drupalCreateUser(['access site reports']);
$this->drupalLogin($account);
$image_files = $this->drupalGetTestFiles('image');
$this->image = File::create((array) current($image_files));
list(, $this->imageExtension) = explode('.', $this->image->getFilename());
$this->assertTrue(is_file($this->image->getFileUri()), "The image file we're going to upload exists.");
$this->phpfile = current($this->drupalGetTestFiles('php'));
$this->assertTrue(is_file($this->phpfile->uri), 'The PHP file we are going to upload exists.');
$this->maxFidBefore = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
// Upload with replace to guarantee there's something there.
$edit = [
'file_test_replace' => FILE_EXISTS_REPLACE,
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called then clean out the hook
// counters.
$this->assertFileHooksCalled(['validate', 'insert']);
file_test_reset();
}
/**
* Test the file_save_upload() function.
*/
public function testNormal() {
$max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
$this->assertTrue($max_fid_after > $this->maxFidBefore, 'A new file was created.');
$file1 = File::load($max_fid_after);
$this->assertTrue($file1, 'Loaded the file.');
// MIME type of the uploaded image may be either image/jpeg or image/png.
$this->assertEqual(substr($file1->getMimeType(), 0, 5), 'image', 'A MIME type was set.');
// Reset the hook counters to get rid of the 'load' we just called.
file_test_reset();
// Upload a second file.
$image2 = current($this->drupalGetTestFiles('image'));
$edit = ['files[file_test_upload]' => drupal_realpath($image2->uri)];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('You WIN!'));
$max_fid_after = db_query('SELECT MAX(fid) AS fid FROM {file_managed}')->fetchField();
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
$file2 = File::load($max_fid_after);
$this->assertTrue($file2, 'Loaded the file');
// MIME type of the uploaded image may be either image/jpeg or image/png.
$this->assertEqual(substr($file2->getMimeType(), 0, 5), 'image', 'A MIME type was set.');
// Load both files using File::loadMultiple().
$files = File::loadMultiple([$file1->id(), $file2->id()]);
$this->assertTrue(isset($files[$file1->id()]), 'File was loaded successfully');
$this->assertTrue(isset($files[$file2->id()]), 'File was loaded successfully');
// Upload a third file to a subdirectory.
$image3 = current($this->drupalGetTestFiles('image'));
$image3_realpath = drupal_realpath($image3->uri);
$dir = $this->randomMachineName();
$edit = [
'files[file_test_upload]' => $image3_realpath,
'file_subdir' => $dir,
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('You WIN!'));
$this->assertTrue(is_file('temporary://' . $dir . '/' . trim(drupal_basename($image3_realpath))));
}
/**
* Test extension handling.
*/
public function testHandleExtension() {
// The file being tested is a .gif which is in the default safe list
// of extensions to allow when the extension validator isn't used. This is
// implicitly tested at the testNormal() test. Here we tell
// file_save_upload() to only allow ".foo".
$extensions = 'foo';
$edit = [
'file_test_replace' => FILE_EXISTS_REPLACE,
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
'extensions' => $extensions,
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$message = t('Only files with the following extensions are allowed:') . ' <em class="placeholder">' . $extensions . '</em>';
$this->assertRaw($message, 'Cannot upload a disallowed extension');
$this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate']);
// Reset the hook counters.
file_test_reset();
$extensions = 'foo ' . $this->imageExtension;
// Now tell file_save_upload() to allow the extension of our test image.
$edit = [
'file_test_replace' => FILE_EXISTS_REPLACE,
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
'extensions' => $extensions,
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload an allowed extension.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'load', 'update']);
// Reset the hook counters.
file_test_reset();
// Now tell file_save_upload() to allow any extension.
$edit = [
'file_test_replace' => FILE_EXISTS_REPLACE,
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
'allow_all_extensions' => TRUE,
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertNoRaw(t('Only files with the following extensions are allowed:'), 'Can upload any extension.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'load', 'update']);
}
/**
* Test dangerous file handling.
*/
public function testHandleDangerousFile() {
$config = $this->config('system.file');
// Allow the .php extension and make sure it gets renamed to .txt for
// safety. Also check to make sure its MIME type was changed.
$edit = [
'file_test_replace' => FILE_EXISTS_REPLACE,
'files[file_test_upload]' => drupal_realpath($this->phpfile->uri),
'is_image_file' => FALSE,
'extensions' => 'php',
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>';
$this->assertRaw($message, 'Dangerous file was renamed.');
$this->assertRaw(t('File MIME type is text/plain.'), "Dangerous file's MIME type was changed.");
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
// Ensure dangerous files are not renamed when insecure uploads is TRUE.
// Turn on insecure uploads.
$config->set('allow_insecure_uploads', 1)->save();
// Reset the hook counters.
file_test_reset();
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.');
$this->assertRaw(t('File name is @filename', ['@filename' => $this->phpfile->filename]), 'Dangerous file was not renamed when insecure uploads is TRUE.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
// Turn off insecure uploads.
$config->set('allow_insecure_uploads', 0)->save();
}
/**
* Test file munge handling.
*/
public function testHandleFileMunge() {
// Ensure insecure uploads are disabled for this test.
$this->config('system.file')->set('allow_insecure_uploads', 0)->save();
$this->image = file_move($this->image, $this->image->getFileUri() . '.foo.' . $this->imageExtension);
// Reset the hook counters to get rid of the 'move' we just called.
file_test_reset();
$extensions = $this->imageExtension;
$edit = [
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
'extensions' => $extensions,
];
$munged_filename = $this->image->getFilename();
$munged_filename = substr($munged_filename, 0, strrpos($munged_filename, '.'));
$munged_filename .= '_.' . $this->imageExtension;
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('For security reasons, your upload has been renamed'), 'Found security message.');
$this->assertRaw(t('File name is @filename', ['@filename' => $munged_filename]), 'File was successfully munged.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
// Ensure we don't munge files if we're allowing any extension.
// Reset the hook counters.
file_test_reset();
$edit = [
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri()),
'allow_all_extensions' => TRUE,
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertNoRaw(t('For security reasons, your upload has been renamed'), 'Found no security message.');
$this->assertRaw(t('File name is @filename', ['@filename' => $this->image->getFilename()]), 'File was not munged when allowing any extension.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
}
/**
* Test renaming when uploading over a file that already exists.
*/
public function testExistingRename() {
$edit = [
'file_test_replace' => FILE_EXISTS_RENAME,
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri())
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'insert']);
}
/**
* Test replacement when uploading over a file that already exists.
*/
public function testExistingReplace() {
$edit = [
'file_test_replace' => FILE_EXISTS_REPLACE,
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri())
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('You WIN!'), 'Found the success message.');
// Check that the correct hooks were called.
$this->assertFileHooksCalled(['validate', 'load', 'update']);
}
/**
* Test for failure when uploading over a file that already exists.
*/
public function testExistingError() {
$edit = [
'file_test_replace' => FILE_EXISTS_ERROR,
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri())
];
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
// Check that the no hooks were called while failing.
$this->assertFileHooksCalled([]);
}
/**
* Test for no failures when not uploading a file.
*/
public function testNoUpload() {
$this->drupalPostForm('file-test/upload', [], t('Submit'));
$this->assertNoRaw(t('Epic upload FAIL!'), 'Failure message not found.');
}
/**
* Tests for log entry on failing destination.
*/
public function testDrupalMovingUploadedFileError() {
// Create a directory and make it not writable.
$test_directory = 'test_drupal_move_uploaded_file_fail';
drupal_mkdir('temporary://' . $test_directory, 0000);
$this->assertTrue(is_dir('temporary://' . $test_directory));
$edit = [
'file_subdir' => $test_directory,
'files[file_test_upload]' => drupal_realpath($this->image->getFileUri())
];
\Drupal::state()->set('file_test.disable_error_collection', TRUE);
$this->drupalPostForm('file-test/upload', $edit, t('Submit'));
$this->assertResponse(200, 'Received a 200 response for posted test file.');
$this->assertRaw(t('File upload error. Could not move uploaded file.'), 'Found the failure message.');
$this->assertRaw(t('Epic upload FAIL!'), 'Found the failure message.');
// Uploading failed. Now check the log.
$this->drupalGet('admin/reports/dblog');
$this->assertResponse(200);
$this->assertRaw(t('Upload error. Could not move uploaded file @file to destination @destination.', [
'@file' => $this->image->getFilename(),
'@destination' => 'temporary://' . $test_directory . '/' . $this->image->getFilename()
]), 'Found upload error log entry.');
}
}

View file

@ -1,96 +0,0 @@
<?php
namespace Drupal\file\Tests\Views;
use Drupal\field\Entity\FieldConfig;
use Drupal\file\Entity\File;
use Drupal\views\Tests\ViewTestBase;
use Drupal\views\Views;
use Drupal\views\Tests\ViewTestData;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests file on user relationship handler.
*
* @group file
*/
class RelationshipUserFileDataTest extends ViewTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['file', 'file_test_views', 'user'];
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_file_user_file_data'];
protected function setUp() {
parent::setUp();
// Create the user profile field and instance.
FieldStorageConfig::create([
'entity_type' => 'user',
'field_name' => 'user_file',
'type' => 'file',
'translatable' => '0',
])->save();
FieldConfig::create([
'label' => 'User File',
'description' => '',
'field_name' => 'user_file',
'entity_type' => 'user',
'bundle' => 'user',
'required' => 0,
])->save();
ViewTestData::createTestViews(get_class($this), ['file_test_views']);
}
/**
* Tests using the views file relationship.
*/
public function testViewsHandlerRelationshipUserFileData() {
$file = File::create([
'fid' => 2,
'uid' => 2,
'filename' => 'image-test.jpg',
'uri' => "public://image-test.jpg",
'filemime' => 'image/jpeg',
'created' => 1,
'changed' => 1,
'status' => FILE_STATUS_PERMANENT,
]);
$file->enforceIsNew();
file_put_contents($file->getFileUri(), file_get_contents('core/modules/simpletest/files/image-1.png'));
$file->save();
$account = $this->drupalCreateUser();
$account->user_file->target_id = 2;
$account->save();
$view = Views::getView('test_file_user_file_data');
// Tests \Drupal\taxonomy\Plugin\views\relationship\NodeTermData::calculateDependencies().
$expected = [
'module' => [
'file',
'user',
],
];
$this->assertIdentical($expected, $view->getDependencies());
$this->executeView($view);
$expected_result = [
[
'file_managed_user__user_file_fid' => '2',
],
];
$column_map = ['file_managed_user__user_file_fid' => 'file_managed_user__user_file_fid'];
$this->assertIdenticalResultset($view, $expected_result, $column_map);
}
}