diff --git a/composer.json b/composer.json index 727e031..c55a9a3 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "wikimedia/composer-merge-plugin": "~1.3" }, "replace": { - "drupal/core": "~8.2" + "drupal/core": "~8.3" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/composer.lock b/composer.lock index 8d2b325..d1a3af0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "7d101b08e5ae002d827cd42ae9a4e344", - "content-hash": "60f7057617c6d995bf9946d0b12f0b5d", + "hash": "64b08387a4402f685cc35b1ad9197380", + "content-hash": "0e7de9d6c3256344615aad4b059a850a", "packages": [ { "name": "asm89/stack-cors", diff --git a/core/core.services.yml b/core/core.services.yml index 6310c21..4872750 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -298,7 +298,7 @@ services: - { name: event_subscriber } config.installer: class: Drupal\Core\Config\ConfigInstaller - arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher'] + arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher', '%install_profile%'] lazy: true config.storage: class: Drupal\Core\Config\CachedStorage @@ -323,7 +323,7 @@ services: - { name: backend_overridable } config.storage.schema: class: Drupal\Core\Config\ExtensionInstallStorage - arguments: ['@config.storage', 'config/schema'] + arguments: ['@config.storage', '%install_profile%', 'config/schema'] config.typed: class: Drupal\Core\Config\TypedConfigManager arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler'] diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index ec12097..897e54e 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -12,7 +12,6 @@ use Drupal\Core\Render\Markup; use Drupal\Component\Render\MarkupInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Site\Settings; use Drupal\Core\Utility\Error; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -716,31 +715,19 @@ function drupal_installation_attempted() { * When this function is called during Drupal's initial installation process, * the name of the profile that's about to be installed is stored in the global * installation state. At all other times, the "install_profile" setting will be - * available in settings.php. + * available in settings.php, or declared as a Distribution. * * @return string|null $profile * The name of the installation profile or NULL if no installation profile is * currently active. This is the case for example during the first steps of * the installer or during unit tests. + * + * @deprecated in Drupal 8.2.0, will be removed before Drupal 9.0.0. + * Use the install_profile container parameter or \Drupal::installProfile() + * instead. */ function drupal_get_profile() { - global $install_state; - - if (drupal_installation_attempted()) { - // If the profile has been selected return it. - if (isset($install_state['parameters']['profile'])) { - $profile = $install_state['parameters']['profile']; - } - else { - $profile = NULL; - } - } - else { - // Fall back to NULL, if there is no 'install_profile' setting. - $profile = Settings::get('install_profile'); - } - - return $profile; + return \Drupal::installProfile(); } /** diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 1019270..6e9636f 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -13,6 +13,7 @@ use Drupal\Core\Installer\Exception\AlreadyInstalledException; use Drupal\Core\Installer\Exception\InstallerException; use Drupal\Core\Installer\Exception\NoProfilesException; +use Drupal\Core\Installer\Exception\TooManyDistributionsException; use Drupal\Core\Installer\InstallerKernel; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManager; @@ -1212,13 +1213,16 @@ function _install_select_profile(&$install_state) { return $profile; } } - // Check for a distribution profile. - foreach ($install_state['profiles'] as $profile) { - $profile_info = install_profile_info($profile->getName()); - if (!empty($profile_info['distribution'])) { - return $profile->getName(); + // Check for a distribution. If the site has more than one distribution force + // the user to choose which will ensure that settings.php has to be written. + try { + if ($distribution = \Drupal::service('kernel')->getDistribution()) { + return $distribution; } } + catch (TooManyDistributionsException $e) { + // The user must choose. + } // Get all visible (not hidden) profiles. $visible_profiles = array_filter($install_state['profiles'], function ($profile) { @@ -2170,13 +2174,34 @@ function install_display_requirements($install_state, $requirements) { } /** - * Installation task; ensures install profile is written to settings.php. + * Installation task; writes profile to settings.php (absent a distribution). * * @param array $install_state * An array of information about the current installation state. + * + * @see _install_select_profile() */ function install_write_profile($install_state) { - if (Settings::get('install_profile') !== $install_state['parameters']['profile']) { + $settings_value = Settings::get('install_profile'); + // We need to write to settings.php if the value in settings.php does not + // equal the selected profile. + $need_to_write = $settings_value !== $install_state['parameters']['profile']; + // However, if we're dealing with a distribution and the profile is not + // writable do not write the value to settings.php if the current value is not + // set. + $distribution = FALSE; + try { + $distribution = \Drupal::service('kernel')->getDistribution(); + } + catch (TooManyDistributionsException $e) { + // The user will have chosen. + } + + if ($settings_value == '' && $distribution && !is_writable(\Drupal::service('site.path') . '/settings.php')) { + $need_to_write = FALSE; + } + + if ($need_to_write) { // Remember the profile which was used. $settings['settings']['install_profile'] = (object) array( 'value' => $install_state['parameters']['profile'], diff --git a/core/includes/install.inc b/core/includes/install.inc index 3a9c2bc..6a4070c 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -334,7 +334,7 @@ function drupal_rewrite_settings($settings = array(), $settings_file = NULL) { } // Write the new settings file. - if (file_put_contents($settings_file, $buffer) === FALSE) { + if (@file_put_contents($settings_file, $buffer) === FALSE) { throw new Exception(t('Failed to modify %settings. Verify the file permissions.', array('%settings' => $settings_file))); } else { @@ -621,6 +621,12 @@ function drupal_install_system($install_state) { // Install base system configuration. \Drupal::service('config.installer')->installDefaultConfig('core', 'core'); + // Ensure to also store the installation profile in configuration for later + // use. + \Drupal::configFactory()->getEditable('core.extension') + ->set('profile', $install_state['parameters']['profile']) + ->save(); + // Install System module and rebuild the newly available routes. $kernel->getContainer()->get('module_installer')->install(array('system'), FALSE); \Drupal::service('router.builder')->rebuild(); diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 5d1588d..b7865b3 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -5,6 +5,7 @@ * Contains \Drupal. */ +use Drupal\Core\Config\BootstrapConfigStorageFactory; use Drupal\Core\DependencyInjection\ContainerNotInitializedException; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Url; @@ -81,7 +82,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '8.2.0-dev'; + const VERSION = '8.3.0-dev'; /** * Core API compatibility. @@ -182,6 +183,22 @@ public static function root() { } /** + * Gets the active install profile. + * + * @return string|null + * The name of the any active install profile or distribution. + */ + public static function installProfile() { + if (static::hasContainer()) { + return static::getContainer()->getParameter('install_profile'); + } + else { + $config_storage = BootstrapConfigStorageFactory::getDatabaseStorage(); + return $config_storage->read('core.extension')['profile']; + } + } + + /** * Indicates if there is a currently active request object. * * @return bool diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 97c3688..329592a 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -6,7 +6,6 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigEntityDependency; -use Drupal\Core\Site\Settings; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class ConfigInstaller implements ConfigInstallerInterface { @@ -61,6 +60,13 @@ class ConfigInstaller implements ConfigInstallerInterface { protected $isSyncing = FALSE; /** + * The name of the currently active installation profile. + * + * @var string + */ + protected $installProfile; + + /** * Constructs the configuration installer. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory @@ -73,13 +79,16 @@ class ConfigInstaller implements ConfigInstallerInterface { * The configuration manager. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher. + * @param string $install_profile + * The name of the currently active installation profile. */ - public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher) { + public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher, $install_profile) { $this->configFactory = $config_factory; $this->activeStorages[$active_storage->getCollectionName()] = $active_storage; $this->typedConfig = $typed_config; $this->configManager = $config_manager; $this->eventDispatcher = $event_dispatcher; + $this->installProfile = $install_profile; } /** @@ -140,7 +149,7 @@ public function installDefaultConfig($type, $name) { // Install any optional configuration entities whose dependencies can now // be met. This searches all the installed modules config/optional // directories. - $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE); + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), $this->installProfile, InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE); $this->installOptionalConfig($storage, [$type => $name]); } @@ -156,7 +165,7 @@ public function installOptionalConfig(StorageInterface $storage = NULL, $depende $optional_profile_config = []; if (!$storage) { // Search the install profile's optional configuration too. - $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE); + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), $this->installProfile, InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE); // The extension install storage ensures that overrides are used. $profile_storage = NULL; } @@ -331,7 +340,7 @@ protected function createConfiguration($collection, array $config_to_create) { * {@inheritdoc} */ public function installCollectionDefaultConfig($collection) { - $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, $this->drupalInstallationAttempted()); + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), $this->installProfile, InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, $this->drupalInstallationAttempted()); // Only install configuration for enabled extensions. $enabled_extensions = $this->getEnabledExtensions(); $config_to_install = array_filter($storage->listAll(), function ($config_name) use ($enabled_extensions) { @@ -644,9 +653,7 @@ protected function drupalGetPath($type, $name) { * of the installer or during unit tests. */ protected function drupalGetProfile() { - // Settings is safe to use because settings.php is written before any module - // is installed. - return Settings::get('install_profile'); + return $this->installProfile; } /** diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php index 14e80dd..96e1e38 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Config; -use Drupal\Core\Site\Settings; use Drupal\Core\Extension\ExtensionDiscovery; /** @@ -28,11 +27,20 @@ class ExtensionInstallStorage extends InstallStorage { protected $includeProfile = TRUE; /** + * The name of the currently active installation profile. + * + * @var string + */ + protected $installProfile; + + /** * Overrides \Drupal\Core\Config\InstallStorage::__construct(). * * @param \Drupal\Core\Config\StorageInterface $config_storage * The active configuration store where the list of enabled modules and * themes is stored. + * @param string $profile + * The current installation profile. * @param string $directory * The directory to scan in each extension to scan for files. Defaults to * 'config/install'. @@ -43,10 +51,12 @@ class ExtensionInstallStorage extends InstallStorage { * (optional) Whether to include the install profile in extensions to * search and to get overrides from. */ - public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE) { + public function __construct(StorageInterface $config_storage, $profile, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION, $include_profile = TRUE) { parent::__construct($directory, $collection); + $this->configStorage = $config_storage; $this->includeProfile = $include_profile; + $this->installProfile = $profile; } /** @@ -77,22 +87,20 @@ protected function getAllFolders() { $this->folders = array(); $this->folders += $this->getCoreNames(); - $install_profile = Settings::get('install_profile'); - $profile = drupal_get_profile(); $extensions = $this->configStorage->read('core.extension'); // @todo Remove this scan as part of https://www.drupal.org/node/2186491 $listing = new ExtensionDiscovery(\Drupal::root()); if (!empty($extensions['module'])) { $modules = $extensions['module']; // Remove the install profile as this is handled later. - unset($modules[$install_profile]); + unset($modules[$this->installProfile]); $profile_list = $listing->scan('profile'); - if ($profile && isset($profile_list[$profile])) { + if ($this->installProfile && isset($profile_list[$this->installProfile])) { // Prime the drupal_get_filename() static cache with the profile info // file location so we can use drupal_get_path() on the active profile // during the module scan. // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('profile', $profile, $profile_list[$profile]->getPathname()); + drupal_get_filename('profile', $this->installProfile, $profile_list[$this->installProfile]->getPathname()); } $module_list_scan = $listing->scan('module'); $module_list = array(); @@ -117,12 +125,12 @@ protected function getAllFolders() { // The install profile can override module default configuration. We do // this by replacing the config file path from the module/theme with the // install profile version if there are any duplicates. - if (isset($profile)) { + if ($this->installProfile) { if (!isset($profile_list)) { $profile_list = $listing->scan('profile'); } - if (isset($profile_list[$profile])) { - $profile_folders = $this->getComponentNames(array($profile_list[$profile])); + if (isset($profile_list[$this->installProfile])) { + $profile_folders = $this->getComponentNames(array($profile_list[$this->installProfile])); $this->folders = $profile_folders + $this->folders; } } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 3ccb2d7..f1fc596 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -14,8 +14,10 @@ use Drupal\Core\DependencyInjection\ServiceProviderInterface; use Drupal\Core\DependencyInjection\YamlFileLoader; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\InfoParser; use Drupal\Core\File\MimeType\MimeTypeGuesser; use Drupal\Core\Http\TrustedHostsRequestFactory; +use Drupal\Core\Installer\Exception\TooManyDistributionsException; use Drupal\Core\Language\Language; use Drupal\Core\Site\Settings; use Symfony\Cmf\Component\Routing\RouteObjectInterface; @@ -1153,6 +1155,7 @@ protected function compileContainer() { $container = $this->getContainerBuilder(); $container->set('kernel', $this); $container->setParameter('container.modules', $this->getModulesParameter()); + $container->setParameter('install_profile', $this->getInstallProfile()); // Get a list of namespaces and put it onto the container. $namespaces = $this->getModuleNamespacesPsr4($this->getModuleFileNames()); @@ -1503,4 +1506,63 @@ protected function addServiceFiles(array $service_yamls) { $this->serviceYamls['site'] = array_filter($service_yamls, 'file_exists'); } + /** + * Gets the active install profile. + * + * @return string|null + * The name of the any active install profile or distribution. + */ + protected function getInstallProfile() { + $install_profile = Settings::get('install_profile'); + + if (empty($install_profile) && ($config = $this->getConfigStorage()->read('core.extension')) && isset($config['profile'])) { + $install_profile = $config['profile']; + } + elseif (empty($install_profile)) { + $install_profile = $this->getDistribution(); + } + return $install_profile; + } + + /** + * Get the name of any discovered profile that is a distribution. + * + * Scans the filesystem looking for all installation profiles. Returns the one + * that has a 'distribution' entry in its info.yml file. If multiple profiles + * are distributions, an exception will be thrown. + * + * This is used in two places: + * 1) During installation, if there is a single distribution, then + * the installer will not write the installation profile name + * to settings.php. + * 2) Whenever DrupalKernel::getInstallProfile() is called, if there + * is no installation profile name noted in settings.php, then + * it will call this function to determine the distribution + * to use. + * + * @return string|FALSE + * The machine name of any discovered distribution. FALSE if there are no + * distributions. + * + * @throws \Drupal\Core\Installer\Exception\TooManyDistributionsException + * Thrown when a site has more than one distribution installation profile. + */ + protected function getDistribution() { + $listing = new ExtensionDiscovery($this->root); + $listing->setProfileDirectories([]); + $info_parser = new InfoParser(); + $distributions = []; + foreach ($listing->scan('profile') as $profile) { + $info = $info_parser->parse($profile->getPathname()); + if (!empty($info['distribution'])) { + $distributions[] = $profile->getName(); + } + } + // There can be only one. + if (count($distributions) > 1) { + throw new TooManyDistributionsException('A site can only have one distribution, multiple installation profiles discovered: ' . implode(', ', $distributions)); + } + return reset($distributions); + } + } diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php index 0d9283c..69b349a 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -516,4 +516,11 @@ protected function getInfoParser() { return $this->infoParser; } + /** + * Reset the discovered files. + */ + public static function reset() { + static::$files = []; + } + } diff --git a/core/lib/Drupal/Core/Installer/Exception/TooManyDistributionsException.php b/core/lib/Drupal/Core/Installer/Exception/TooManyDistributionsException.php new file mode 100644 index 0000000..d9028db --- /dev/null +++ b/core/lib/Drupal/Core/Installer/Exception/TooManyDistributionsException.php @@ -0,0 +1,11 @@ +getStorage('block')->loadByProperties(['theme' => $theme]); foreach ($blocks as $block_id => $block) { // Disable blocks in invalid regions. - $region = $block->getRegion(); - if ($region !== BlockInterface::BLOCK_REGION_NONE) { - if (!empty($region) && !isset($regions[$region]) && $block->status()) { - drupal_set_message(t('The block %info was assigned to the invalid region %region and has been disabled.', ['%info' => $block_id, '%region' => $region]), 'warning'); - $block->disable(); - } - // Set region to none if not enabled. - if (!$block->status()) { - $block->setRegion(BlockInterface::BLOCK_REGION_NONE); - $block->save(); + if (!isset($regions[$block->getRegion()])) { + if ($block->status()) { + drupal_set_message(t('The block %info was assigned to the invalid region %region and has been disabled.', ['%info' => $block_id, '%region' => $block->getRegion()]), 'warning'); } + $block + ->setRegion(system_default_region($theme)) + ->disable() + ->save(); } } } diff --git a/core/modules/block/block.post_update.php b/core/modules/block/block.post_update.php index f208f65..7bab413 100644 --- a/core/modules/block/block.post_update.php +++ b/core/modules/block/block.post_update.php @@ -75,5 +75,12 @@ function block_post_update_disable_blocks_with_missing_contexts() { } /** + * Disable blocks that are placed into the "disabled" region. + */ +function block_post_update_disabled_region_update() { + // An empty update will flush caches, forcing block_rebuild() to run. +} + +/** * @} End of "addtogroup updates-8.0.0-beta". */ diff --git a/core/modules/block/block.routing.yml b/core/modules/block/block.routing.yml index 396bf0d..e12cd8b 100644 --- a/core/modules/block/block.routing.yml +++ b/core/modules/block/block.routing.yml @@ -25,6 +25,22 @@ entity.block.edit_form: requirements: _entity_access: 'block.update' +entity.block.enable: + path: '/admin/structure/block/manage/{block}/enable' + defaults: + _controller: '\Drupal\block\Controller\BlockController::performOperation' + op: enable + requirements: + _entity_access: 'block.enable' + +entity.block.disable: + path: '/admin/structure/block/manage/{block}/disable' + defaults: + _controller: '\Drupal\block\Controller\BlockController::performOperation' + op: disable + requirements: + _entity_access: 'block.disable' + block.admin_display: path: '/admin/structure/block' defaults: diff --git a/core/modules/block/css/block.admin.css b/core/modules/block/css/block.admin.css index ed12038..7fde3d7 100644 --- a/core/modules/block/css/block.admin.css +++ b/core/modules/block/css/block.admin.css @@ -40,3 +40,7 @@ a.block-demo-backlink:hover { .block-form .form-item-settings-admin-label label:after { content: ':'; } +.block-disabled:not(:hover) { + background: #fcfcfa; + opacity: 0.675; +} diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php index 1b1f427..9b1b027 100644 --- a/core/modules/block/src/BlockForm.php +++ b/core/modules/block/src/BlockForm.php @@ -188,7 +188,7 @@ public function form(array $form, FormStateInterface $form_state) { '#title' => $this->t('Region'), '#description' => $this->t('Select the region where this block should be displayed.'), '#default_value' => $region, - '#empty_value' => BlockInterface::BLOCK_REGION_NONE, + '#required' => TRUE, '#options' => system_region_list($theme, REGIONS_VISIBLE), '#prefix' => '
', '#suffix' => '
', diff --git a/core/modules/block/src/BlockInterface.php b/core/modules/block/src/BlockInterface.php index bd46671..0056523 100644 --- a/core/modules/block/src/BlockInterface.php +++ b/core/modules/block/src/BlockInterface.php @@ -16,6 +16,8 @@ /** * Denotes that a block is not enabled in any region and should not be shown. + * + * @deprecated Scheduled for removal in Drupal 9.0.0. */ const BLOCK_REGION_NONE = -1; diff --git a/core/modules/block/src/BlockListBuilder.php b/core/modules/block/src/BlockListBuilder.php index 3365415..7858d6b 100644 --- a/core/modules/block/src/BlockListBuilder.php +++ b/core/modules/block/src/BlockListBuilder.php @@ -156,6 +156,7 @@ protected function buildBlocksForm() { 'weight' => $entity->getWeight(), 'entity' => $entity, 'category' => $definition['category'], + 'status' => $entity->status(), ); } @@ -186,8 +187,7 @@ protected function buildBlocksForm() { // Loop over each region and build blocks. $regions = $this->systemRegionList($this->getThemeName(), REGIONS_VISIBLE); - $block_regions_with_disabled = $regions + array(BlockInterface::BLOCK_REGION_NONE => $this->t('Disabled', array(), array('context' => 'Plural'))); - foreach ($block_regions_with_disabled as $region => $title) { + foreach ($regions as $region => $title) { $form['#tabledrag'][] = array( 'action' => 'match', 'relationship' => 'sibling', @@ -214,9 +214,9 @@ protected function buildBlocksForm() { '#attributes' => array('class' => 'region-title__action'), ) ), - '#prefix' => $region != BlockInterface::BLOCK_REGION_NONE ? $title : $block_regions_with_disabled[$region], + '#prefix' => $title, '#type' => 'link', - '#title' => $this->t('Place block in the %region region', ['%region' => $block_regions_with_disabled[$region]]), + '#title' => $this->t('Place block in the %region region', ['%region' => $title]), '#url' => Url::fromRoute('block.admin_library', ['theme' => $this->getThemeName()], ['query' => ['region' => $region]]), '#wrapper_attributes' => array( 'colspan' => 5, @@ -255,12 +255,13 @@ protected function buildBlocksForm() { 'class' => array('draggable'), ), ); + $form[$entity_id]['#attributes']['class'][] = $info['status'] ? 'block-enabled' : 'block-disabled'; if ($placement && $placement == Html::getClass($entity_id)) { $form[$entity_id]['#attributes']['class'][] = 'color-success'; $form[$entity_id]['#attributes']['class'][] = 'js-block-placed'; } $form[$entity_id]['info'] = array( - '#plain_text' => $info['label'], + '#plain_text' => $info['status'] ? $info['label'] : $this->t('@label (disabled)', ['@label' => $info['label']]), '#wrapper_attributes' => array( 'class' => array('block'), ), @@ -271,7 +272,7 @@ protected function buildBlocksForm() { $form[$entity_id]['region-theme']['region'] = array( '#type' => 'select', '#default_value' => $region, - '#empty_value' => BlockInterface::BLOCK_REGION_NONE, + '#required' => TRUE, '#title' => $this->t('Region for @block block', array('@block' => $info['label'])), '#title_display' => 'invisible', '#options' => $regions, @@ -361,12 +362,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $entity_values = $form_state->getValue(array('blocks', $entity_id)); $entity->setWeight($entity_values['weight']); $entity->setRegion($entity_values['region']); - if ($entity->getRegion() == BlockInterface::BLOCK_REGION_NONE) { - $entity->disable(); - } - else { - $entity->enable(); - } $entity->save(); } drupal_set_message(t('The block settings have been updated.')); diff --git a/core/modules/block/src/Controller/BlockController.php b/core/modules/block/src/Controller/BlockController.php index d8f4226..58417d5 100644 --- a/core/modules/block/src/Controller/BlockController.php +++ b/core/modules/block/src/Controller/BlockController.php @@ -3,6 +3,7 @@ namespace Drupal\block\Controller; use Drupal\Component\Utility\Html; +use Drupal\block\BlockInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Extension\ThemeHandlerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -40,6 +41,23 @@ public static function create(ContainerInterface $container) { } /** + * Calls a method on a block and reloads the listing page. + * + * @param \Drupal\block\BlockInterface $block + * The block being acted upon. + * @param string $op + * The operation to perform, e.g., 'enable' or 'disable'. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * A redirect back to the listing page. + */ + public function performOperation(BlockInterface $block, $op) { + $block->$op()->save(); + drupal_set_message($this->t('The block settings have been updated.')); + return $this->redirect('block.admin_display'); + } + + /** * Returns a block theme demo page. * * @param string $theme diff --git a/core/modules/block/src/Entity/Block.php b/core/modules/block/src/Entity/Block.php index 9cbd30b..63b5205 100644 --- a/core/modules/block/src/Entity/Block.php +++ b/core/modules/block/src/Entity/Block.php @@ -28,11 +28,14 @@ * }, * admin_permission = "administer blocks", * entity_keys = { - * "id" = "id" + * "id" = "id", + * "status" = "status" * }, * links = { * "delete-form" = "/admin/structure/block/manage/{block}/delete", - * "edit-form" = "/admin/structure/block/manage/{block}" + * "edit-form" = "/admin/structure/block/manage/{block}", + * "enable" = "/admin/structure/block/manage/{block}/enable", + * "disable" = "/admin/structure/block/manage/{block}/disable", * }, * config_export = { * "id", @@ -70,7 +73,7 @@ class Block extends ConfigEntityBase implements BlockInterface, EntityWithPlugin * * @var string */ - protected $region = self::BLOCK_REGION_NONE; + protected $region; /** * The block weight. @@ -209,13 +212,13 @@ public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) if ($status !== 0) { return $status; } - // Sort by weight, unless disabled. - if ($a->getRegion() != static::BLOCK_REGION_NONE) { - $weight = $a->getWeight() - $b->getWeight(); - if ($weight) { - return $weight; - } + + // Sort by weight. + $weight = $a->getWeight() - $b->getWeight(); + if ($weight) { + return $weight; } + // Sort by label. return strcmp($a->label(), $b->label()); } @@ -327,4 +330,21 @@ public function createDuplicateBlock($new_id = NULL, $new_theme = NULL) { return $duplicate; } + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // Ensure the region is valid to mirror the behavior of block_rebuild(). + // This is done primarily for backwards compatibility support of + // \Drupal\block\BlockInterface::BLOCK_REGION_NONE. + $regions = system_region_list($this->theme); + if (!isset($regions[$this->region]) && $this->status()) { + $this + ->setRegion(system_default_region($this->theme)) + ->disable(); + } + } + } diff --git a/core/modules/block/src/Tests/BlockInvalidRegionTest.php b/core/modules/block/src/Tests/BlockInvalidRegionTest.php index 438a0c1..99ef06d 100644 --- a/core/modules/block/src/Tests/BlockInvalidRegionTest.php +++ b/core/modules/block/src/Tests/BlockInvalidRegionTest.php @@ -37,8 +37,8 @@ protected function setUp() { function testBlockInInvalidRegion() { // Enable a test block and place it in an invalid region. $block = $this->drupalPlaceBlock('test_html'); - $block->setRegion('invalid_region'); - $block->save(); + \Drupal::configFactory()->getEditable('block.block.' . $block->id())->set('region', 'invalid_region')->save(); + $block = Block::load($block->id()); $warning_message = t('The block %info was assigned to the invalid region %region and has been disabled.', array('%info' => $block->id(), '%region' => 'invalid_region')); @@ -51,9 +51,8 @@ function testBlockInInvalidRegion() { $this->assertNoRaw($warning_message, 'Disabled block in the invalid region will not trigger the warning.'); // Place disabled test block in the invalid region of the default theme. + \Drupal::configFactory()->getEditable('block.block.' . $block->id())->set('region', 'invalid_region')->save(); $block = Block::load($block->id()); - $block->setRegion('invalid_region'); - $block->save(); // Clear the cache to check if the warning message is not triggered. $this->drupalPostForm('admin/config/development/performance', array(), 'Clear all caches'); diff --git a/core/modules/block/src/Tests/BlockTest.php b/core/modules/block/src/Tests/BlockTest.php index 039aa64..41d376a 100644 --- a/core/modules/block/src/Tests/BlockTest.php +++ b/core/modules/block/src/Tests/BlockTest.php @@ -178,6 +178,10 @@ function testBlock() { // Place page title block to test error messages. $this->drupalPlaceBlock('page_title_block'); + // Disable the block. + $this->drupalGet('admin/structure/block'); + $this->clickLink('Disable'); + // Select the 'Powered by Drupal' block to be configured and moved. $block = array(); $block['id'] = 'system_powered_by_block'; @@ -199,13 +203,12 @@ function testBlock() { $this->moveBlockToRegion($block, $region); } - // Set the block to the disabled region. - $edit = array(); - $edit['blocks[' . $block['id'] . '][region]'] = -1; - $this->drupalPostForm('admin/structure/block', $edit, t('Save blocks')); + // Disable the block. + $this->drupalGet('admin/structure/block'); + $this->clickLink('Disable'); // Confirm that the block is now listed as disabled. - $this->assertText(t('The block settings have been updated.'), 'Block successfully move to disabled region.'); + $this->assertText(t('The block settings have been updated.'), 'Block successfully moved to disabled region.'); // Confirm that the block instance title and markup are not displayed. $this->drupalGet('node'); @@ -218,7 +221,7 @@ function testBlock() { // Test deleting the block from the edit form. $this->drupalGet('admin/structure/block/manage/' . $block['id']); $this->clickLink(t('Delete')); - $this->assertRaw(t('Are you sure you want to delete the block %name?', array('%name' => $block['settings[label]']))); + $this->assertRaw(t('Are you sure you want to delete the block @name?', array('@name' => $block['settings[label]']))); $this->drupalPostForm(NULL, array(), t('Delete')); $this->assertRaw(t('The block %name has been deleted.', array('%name' => $block['settings[label]']))); @@ -226,7 +229,7 @@ function testBlock() { $block = $this->drupalPlaceBlock('system_powered_by_block'); $this->drupalGet('admin/structure/block/manage/' . $block->id(), array('query' => array('destination' => 'admin'))); $this->clickLink(t('Delete')); - $this->assertRaw(t('Are you sure you want to delete the block %name?', array('%name' => $block->label()))); + $this->assertRaw(t('Are you sure you want to delete the block @name?', array('@name' => $block->label()))); $this->drupalPostForm(NULL, array(), t('Delete')); $this->assertRaw(t('The block %name has been deleted.', array('%name' => $block->label()))); $this->assertUrl('admin'); diff --git a/core/modules/block/src/Tests/BlockUiTest.php b/core/modules/block/src/Tests/BlockUiTest.php index ec720e9..35c3b5f 100644 --- a/core/modules/block/src/Tests/BlockUiTest.php +++ b/core/modules/block/src/Tests/BlockUiTest.php @@ -257,12 +257,15 @@ public function testMachineNameSuggestion() { $url = 'admin/structure/block/add/test_block_instantiation/classy'; $this->drupalGet($url); $this->assertFieldByName('id', 'displaymessage', 'Block form uses raw machine name suggestion when no instance already exists.'); - $this->drupalPostForm($url, array(), 'Save block'); + $edit = ['region' => 'content']; + $this->drupalPostForm($url, $edit, 'Save block'); + $this->assertText('The block configuration has been saved.'); // Now, check to make sure the form starts by autoincrementing correctly. $this->drupalGet($url); $this->assertFieldByName('id', 'displaymessage_2', 'Block form appends _2 to plugin-suggested machine name when an instance already exists.'); - $this->drupalPostForm($url, array(), 'Save block'); + $this->drupalPostForm($url, $edit, 'Save block'); + $this->assertText('The block configuration has been saved.'); // And verify that it continues working beyond just the first two. $this->drupalGet($url); @@ -292,7 +295,7 @@ public function testBlockPlacementIndicator() { * Tests if validation errors are passed plugin form to the parent form. */ public function testBlockValidateErrors() { - $this->drupalPostForm('admin/structure/block/add/test_settings_validation/classy', ['settings[digits]' => 'abc'], t('Save block')); + $this->drupalPostForm('admin/structure/block/add/test_settings_validation/classy', ['region' => 'content', 'settings[digits]' => 'abc'], t('Save block')); $arguments = [':message' => 'Only digits are allowed']; $pattern = '//div[contains(@class,"messages messages--error")]/div[contains(text()[2],:message)]'; diff --git a/core/modules/block/src/Tests/Update/BlockRemoveDisabledRegionUpdateTest.php b/core/modules/block/src/Tests/Update/BlockRemoveDisabledRegionUpdateTest.php new file mode 100644 index 0000000..60ba24c --- /dev/null +++ b/core/modules/block/src/Tests/Update/BlockRemoveDisabledRegionUpdateTest.php @@ -0,0 +1,57 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.update-test-block-disabled-2513534.php', + ]; + } + + /** + * Tests that block context mapping is updated properly. + */ + public function testUpdateHookN() { + $this->runUpdates(); + + // Disable maintenance mode. + \Drupal::state()->set('system.maintenance_mode', FALSE); + + // We finished updating so we can login the user now. + $this->drupalLogin($this->rootUser); + + // Verify that a disabled block is in the default region. + $this->drupalGet('admin/structure/block'); + $element = $this->xpath("//tr[contains(@data-drupal-selector, :block) and contains(@class, :status)]//select/option[@selected and @value=:region]", + [':block' => 'edit-blocks-pagetitle-1', ':status' => 'block-disabled', ':region' => 'header']); + $this->assertTrue(!empty($element)); + + // Verify that an enabled block is now disabled and in the default region. + $this->drupalGet('admin/structure/block'); + $element = $this->xpath("//tr[contains(@data-drupal-selector, :block) and contains(@class, :status)]//select/option[@selected and @value=:region]", + [':block' => 'edit-blocks-pagetitle-2', ':status' => 'block-disabled', ':region' => 'header']); + $this->assertTrue(!empty($element)); + + } + +} diff --git a/core/modules/block/src/Tests/Views/DisplayBlockTest.php b/core/modules/block/src/Tests/Views/DisplayBlockTest.php index faf68c4..368af68 100644 --- a/core/modules/block/src/Tests/Views/DisplayBlockTest.php +++ b/core/modules/block/src/Tests/Views/DisplayBlockTest.php @@ -185,8 +185,10 @@ public function testViewsBlockForm() { // Test that that machine name field is hidden from display and has been // saved as expected from the default value. $this->assertNoFieldById('edit-machine-name', 'views_block__test_view_block_1', 'The machine name is hidden on the views block form.'); + // Save the block. - $this->drupalPostForm(NULL, array(), t('Save block')); + $edit = ['region' => 'content']; + $this->drupalPostForm(NULL, $edit, t('Save block')); $storage = $this->container->get('entity_type.manager')->getStorage('block'); $block = $storage->load('views_block__test_view_block_block_1'); // This will only return a result if our new block has been created with the @@ -195,7 +197,7 @@ public function testViewsBlockForm() { for ($i = 2; $i <= 3; $i++) { // Place the same block again and make sure we have a new ID. - $this->drupalPostForm('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme, array(), t('Save block')); + $this->drupalPostForm('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme, $edit, t('Save block')); $block = $storage->load('views_block__test_view_block_block_1_' . $i); // This will only return a result if our new block has been created with the // expected machine name. @@ -204,7 +206,7 @@ public function testViewsBlockForm() { // Tests the override capability of items per page. $this->drupalGet('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme); - $edit = array(); + $edit = ['region' => 'content']; $edit['settings[override][items_per_page]'] = 10; $this->drupalPostForm('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme, $edit, t('Save block')); @@ -222,7 +224,7 @@ public function testViewsBlockForm() { $this->assertEqual(5, $config['items_per_page'], "'Items per page' is properly saved."); // Tests the override of the label capability. - $edit = array(); + $edit = ['region' => 'content']; $edit['settings[views_label_checkbox]'] = 1; $edit['settings[views_label]'] = 'Custom title'; $this->drupalPostForm('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme, $edit, t('Save block')); diff --git a/core/modules/block/tests/src/Kernel/BlockRebuildTest.php b/core/modules/block/tests/src/Kernel/BlockRebuildTest.php new file mode 100644 index 0000000..33de12c --- /dev/null +++ b/core/modules/block/tests/src/Kernel/BlockRebuildTest.php @@ -0,0 +1,103 @@ +container->get('theme_installer')->install(['stable', 'classy']); + $this->container->get('config.factory')->getEditable('system.theme')->set('default', 'classy')->save(); + } + + /** + * {@inheritdoc} + */ + public static function setUpBeforeClass() { + parent::setUpBeforeClass(); + + // @todo Once block_rebuild() is refactored to auto-loadable code, remove + // this require statement. + require_once static::getDrupalRoot() . '/core/modules/block/block.module'; + } + + /** + * @covers ::block_rebuild + */ + public function testRebuildNoBlocks() { + block_rebuild(); + $messages = drupal_get_messages(); + $this->assertEquals([], $messages); + } + + /** + * @covers ::block_rebuild + */ + public function testRebuildNoInvalidBlocks() { + $this->placeBlock('system_powered_by_block', ['region' => 'content']); + + block_rebuild(); + $messages = drupal_get_messages(); + $this->assertEquals([], $messages); + } + + /** + * @covers ::block_rebuild + */ + public function testRebuildInvalidBlocks() { + $this->placeBlock('system_powered_by_block', ['region' => 'content']); + $block1 = $this->placeBlock('system_powered_by_block'); + $block2 = $this->placeBlock('system_powered_by_block'); + $block2->disable()->save(); + // Use the config API directly to bypass Block::preSave(). + \Drupal::configFactory()->getEditable('block.block.' . $block1->id())->set('region', 'INVALID')->save(); + \Drupal::configFactory()->getEditable('block.block.' . $block2->id())->set('region', 'INVALID')->save(); + + // Reload block entities. + $block1 = Block::load($block1->id()); + $block2 = Block::load($block2->id()); + + $this->assertSame('INVALID', $block1->getRegion()); + $this->assertTrue($block1->status()); + $this->assertSame('INVALID', $block2->getRegion()); + $this->assertFalse($block2->status()); + + block_rebuild(); + + // Reload block entities. + $block1 = Block::load($block1->id()); + $block2 = Block::load($block2->id()); + + $messages = drupal_get_messages(); + $expected = ['warning' => [new TranslatableMarkup('The block %info was assigned to the invalid region %region and has been disabled.', ['%info' => $block1->id(), '%region' => 'INVALID'])]]; + $this->assertEquals($expected, $messages); + + $default_region = system_default_region('classy'); + $this->assertSame($default_region, $block1->getRegion()); + $this->assertFalse($block1->status()); + $this->assertSame($default_region, $block2->getRegion()); + $this->assertFalse($block2->status()); + } + +} diff --git a/core/modules/block/tests/src/Kernel/BlockStorageUnitTest.php b/core/modules/block/tests/src/Kernel/BlockStorageUnitTest.php index 6dfc66c..5704476 100644 --- a/core/modules/block/tests/src/Kernel/BlockStorageUnitTest.php +++ b/core/modules/block/tests/src/Kernel/BlockStorageUnitTest.php @@ -34,6 +34,8 @@ protected function setUp() { parent::setUp(); $this->controller = $this->container->get('entity_type.manager')->getStorage('block'); + + $this->container->get('theme_installer')->install(['stark']); } /** @@ -66,6 +68,7 @@ protected function createTests() { $entity = $this->controller->create(array( 'id' => 'test_block', 'theme' => 'stark', + 'region' => 'content', 'plugin' => 'test_html', )); $entity->save(); @@ -84,7 +87,7 @@ protected function createTests() { 'dependencies' => array('module' => array('block_test'), 'theme' => array('stark')), 'id' => 'test_block', 'theme' => 'stark', - 'region' => '-1', + 'region' => 'content', 'weight' => NULL, 'provider' => NULL, 'plugin' => 'test_html', @@ -111,7 +114,7 @@ protected function loadTests() { $this->assertTrue($entity instanceof Block, 'The loaded entity is a Block.'); // Verify several properties of the block. - $this->assertEqual($entity->getRegion(), '-1'); + $this->assertSame('content', $entity->getRegion()); $this->assertTrue($entity->status()); $this->assertEqual($entity->getTheme(), 'stark'); $this->assertTrue($entity->uuid()); diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php index 1ceadd1..bc59f71 100644 --- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php +++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php @@ -28,6 +28,10 @@ class MigrateBlockTest extends MigrateDrupal6TestBase { */ protected function setUp() { parent::setUp(); + + // Install the themes used for this test. + $this->container->get('theme_installer')->install(['bartik', 'seven', 'test_theme']); + $this->installConfig(['block_content']); $this->installEntitySchema('block_content'); @@ -37,9 +41,6 @@ protected function setUp() { $config->set('admin', 'seven'); $config->save(); - // Install one of D8's test themes. - \Drupal::service('theme_handler')->install(['test_theme']); - $this->executeMigrations([ 'd6_filter_format', 'block_content_type', @@ -69,13 +70,14 @@ protected function setUp() { * @param string $label_display * The block label display setting. */ - public function assertEntity($id, $visibility, $region, $theme, $weight, $label, $label_display) { + public function assertEntity($id, $visibility, $region, $theme, $weight, $label, $label_display, $status = TRUE) { $block = Block::load($id); $this->assertTrue($block instanceof Block); $this->assertIdentical($visibility, $block->getVisibility()); $this->assertIdentical($region, $block->getRegion()); $this->assertIdentical($theme, $block->getTheme()); $this->assertIdentical($weight, $block->getWeight()); + $this->assertIdentical($status, $block->status()); $config = $this->config('block.block.' . $id); $this->assertIdentical($label, $config->get('settings.label')); @@ -122,7 +124,9 @@ public function testBlockMigration() { $visibility['request_path']['id'] = 'request_path'; $visibility['request_path']['negate'] = TRUE; $visibility['request_path']['pages'] = '/node/1'; - $this->assertEntity('system', $visibility, 'footer', 'bartik', -5, '', '0'); + // @todo https://www.drupal.org/node/2753939 This block is the footer region + // but Bartik in D8 does not have this region. + $this->assertEntity('system', $visibility, 'header', 'bartik', -5, '', '0', FALSE); // Check menu blocks $visibility = []; @@ -137,7 +141,9 @@ public function testBlockMigration() { $visibility['request_path']['id'] = 'request_path'; $visibility['request_path']['negate'] = FALSE; $visibility['request_path']['pages'] = '/node'; - $this->assertEntity('block_1', $visibility, 'sidebar_second', 'bluemarine', -4, 'Another Static Block', 'visible'); + // @todo https://www.drupal.org/node/2753939 The bluemarine theme does not + // exist. + $this->assertEntity('block_1', $visibility, '', 'bluemarine', -4, 'Another Static Block', 'visible', FALSE); $visibility = []; $this->assertEntity('block_2', $visibility, 'right', 'test_theme', -7, '', '0'); diff --git a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php index 571c210..49be0ee 100644 --- a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php +++ b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php @@ -33,6 +33,10 @@ class MigrateBlockTest extends MigrateDrupal7TestBase { */ protected function setUp() { parent::setUp(); + + // Install the themes used for this test. + $this->container->get('theme_installer')->install(['bartik', 'seven']); + $this->installConfig(static::$modules); $this->installEntitySchema('block_content'); @@ -42,9 +46,6 @@ protected function setUp() { $config->set('admin', 'seven'); $config->save(); - // Install one of D8's test themes. - \Drupal::service('theme_handler')->install(['bartik']); - $this->executeMigrations([ 'd7_filter_format', 'd7_user_role', @@ -77,7 +78,7 @@ protected function setUp() { * @param string $label_display * The block label display setting. */ - public function assertEntity($id, $plugin_id, array $roles, $pages, $region, $theme, $weight, $label, $label_display) { + public function assertEntity($id, $plugin_id, array $roles, $pages, $region, $theme, $weight, $label, $label_display, $status = TRUE) { $block = Block::load($id); $this->assertTrue($block instanceof Block); /** @var \Drupal\block\BlockInterface $block */ @@ -95,6 +96,7 @@ public function assertEntity($id, $plugin_id, array $roles, $pages, $region, $th $this->assertIdentical($region, $block->getRegion()); $this->assertIdentical($theme, $block->getTheme()); $this->assertIdentical($weight, $block->getWeight()); + $this->assertIdentical($status, $block->status()); $config = $this->config('block.block.' . $id); $this->assertIdentical($label, $config->get('settings.label')); @@ -108,7 +110,9 @@ public function testBlockMigration() { $this->assertEntity('bartik_system_main', 'system_main_block', [], '', 'content', 'bartik', 0, '', '0'); $this->assertEntity('bartik_search_form', 'search_form_block', [], '', 'sidebar_first', 'bartik', -1, '', '0'); $this->assertEntity('bartik_user_login', 'user_login_block', [], '', 'sidebar_first', 'bartik', 0, '', '0'); - $this->assertEntity('bartik_system_powered_by', 'system_powered_by_block', [], '', 'footer', 'bartik', 10, '', '0'); + // @todo https://www.drupal.org/node/2753939 This block is the footer region + // but Bartik in D8 does not have this region. + $this->assertEntity('bartik_system_powered_by', 'system_powered_by_block', [], '', 'header', 'bartik', 10, '', '0', FALSE); $this->assertEntity('seven_system_main', 'system_main_block', [], '', 'content', 'seven', 0, '', '0'); $this->assertEntity('seven_user_login', 'user_login_block', [], '', 'content', 'seven', 10, '', '0'); diff --git a/core/modules/block_content/src/Tests/BlockContentCreationTest.php b/core/modules/block_content/src/Tests/BlockContentCreationTest.php index 0cca85e..e45892c 100644 --- a/core/modules/block_content/src/Tests/BlockContentCreationTest.php +++ b/core/modules/block_content/src/Tests/BlockContentCreationTest.php @@ -107,7 +107,7 @@ public function testBlockContentCreationMultipleViewModes() { )), 'Basic block created.'); // Save our block permanently - $this->drupalPostForm(NULL, NULL, t('Save block')); + $this->drupalPostForm(NULL, ['region' => 'content'], t('Save block')); // Set test_view_mode as a custom display to be available on the list. $this->drupalGet('admin/structure/block/block-content'); @@ -134,6 +134,7 @@ public function testBlockContentCreationMultipleViewModes() { $this->assertFieldByXPath('//select[@name="settings[view_mode]"]', NULL, 'View mode setting shown because multiple exist'); // Change the view mode. + $view_mode['region'] = 'content'; $view_mode['settings[view_mode]'] = 'test_view_mode'; $this->drupalPostForm(NULL, $view_mode, t('Save block')); diff --git a/core/modules/block_content/src/Tests/BlockContentTypeTest.php b/core/modules/block_content/src/Tests/BlockContentTypeTest.php index e73aa2a..8f6ffe1 100644 --- a/core/modules/block_content/src/Tests/BlockContentTypeTest.php +++ b/core/modules/block_content/src/Tests/BlockContentTypeTest.php @@ -211,7 +211,7 @@ public function testsBlockContentAddTypes() { if (!empty($blocks)) { $block = reset($blocks); $this->assertUrl(\Drupal::url('block.admin_add', array('plugin_id' => 'block_content:' . $block->uuid(), 'theme' => $theme), array('absolute' => TRUE))); - $this->drupalPostForm(NULL, array(), t('Save block')); + $this->drupalPostForm(NULL, ['region' => 'content'], t('Save block')); $this->assertUrl(\Drupal::url('block.admin_display_theme', array('theme' => $theme), array('absolute' => TRUE, 'query' => array('block-placement' => Html::getClass($edit['info[0][value]']))))); } else { diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml index e37cb48..b9f8fb8 100644 --- a/core/modules/locale/locale.services.yml +++ b/core/modules/locale/locale.services.yml @@ -1,7 +1,7 @@ services: locale.default.config.storage: class: Drupal\locale\LocaleDefaultConfigStorage - arguments: ['@config.storage', '@language_manager'] + arguments: ['@config.storage', '@language_manager', '%install_profile%'] public: false locale.config_manager: class: Drupal\locale\LocaleConfigManager diff --git a/core/modules/locale/src/LocaleDefaultConfigStorage.php b/core/modules/locale/src/LocaleDefaultConfigStorage.php index 29697a1..bcb242b 100644 --- a/core/modules/locale/src/LocaleDefaultConfigStorage.php +++ b/core/modules/locale/src/LocaleDefaultConfigStorage.php @@ -57,12 +57,12 @@ class LocaleDefaultConfigStorage { * @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager * The language manager. */ - public function __construct(StorageInterface $config_storage, ConfigurableLanguageManagerInterface $language_manager) { + public function __construct(StorageInterface $config_storage, ConfigurableLanguageManagerInterface $language_manager, $install_profile) { $this->configStorage = $config_storage; $this->languageManager = $language_manager; - $this->requiredInstallStorage = new ExtensionInstallStorage($this->configStorage); - $this->optionalInstallStorage = new ExtensionInstallStorage($this->configStorage, ExtensionInstallStorage::CONFIG_OPTIONAL_DIRECTORY); + $this->requiredInstallStorage = new ExtensionInstallStorage($this->configStorage, $install_profile); + $this->optionalInstallStorage = new ExtensionInstallStorage($this->configStorage, $install_profile, ExtensionInstallStorage::CONFIG_OPTIONAL_DIRECTORY); } /** diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 5cf42dd..dfb0272 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -36,6 +36,9 @@ */ class EntityResource extends ResourceBase implements DependentPluginInterface { + use EntityResourceValidationTrait; + use EntityResourceAccessTrait; + /** * The entity type targeted by this resource. * @@ -156,14 +159,7 @@ public function post(EntityInterface $entity = NULL) { throw new BadRequestHttpException('Only new entities can be created'); } - // Only check 'edit' permissions for fields that were actually - // submitted by the user. Field access makes no difference between 'create' - // and 'update', so the 'edit' operation is used here. - foreach ($entity->_restSubmittedFields as $key => $field_name) { - if (!$entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on creating field '$field_name'"); - } - } + $this->checkEditFieldAccess($entity); // Validate the received data before saving. $this->validate($entity); @@ -175,8 +171,7 @@ public function post(EntityInterface $entity = NULL) { // body. These responses are not cacheable, so we add no cacheability // metadata here. $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE); - $response = new ModifiedResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]); - return $response; + return new ModifiedResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]); } catch (EntityStorageException $e) { throw new HttpException(500, 'Internal Server Error', $e); @@ -277,39 +272,6 @@ public function delete(EntityInterface $entity) { } /** - * Verifies that the whole entity does not violate any validation constraints. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object. - * - * @throws \Symfony\Component\HttpKernel\Exception\HttpException - * If validation errors are found. - */ - protected function validate(EntityInterface $entity) { - // @todo Remove when https://www.drupal.org/node/2164373 is committed. - if (!$entity instanceof FieldableEntityInterface) { - return; - } - $violations = $entity->validate(); - - // Remove violations of inaccessible fields as they cannot stem from our - // changes. - $violations->filterByFieldAccess(); - - if (count($violations) > 0) { - $message = "Unprocessable Entity: validation failed.\n"; - foreach ($violations as $violation) { - $message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n"; - } - // Instead of returning a generic 400 response we use the more specific - // 422 Unprocessable Entity code from RFC 4918. That way clients can - // distinguish between general syntax errors in bad serializations (code - // 400) and semantic errors in well-formed requests (code 422). - throw new HttpException(422, $message); - } - } - - /** * {@inheritdoc} */ public function permissions() { diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php new file mode 100644 index 0000000..7bf8e82 --- /dev/null +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php @@ -0,0 +1,35 @@ +_restSubmittedFields as $key => $field_name) { + if (!$entity->get($field_name)->access('edit')) { + throw new AccessDeniedHttpException("Access denied on creating field '$field_name'."); + } + } + } + +} diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php new file mode 100644 index 0000000..a2ff40a --- /dev/null +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php @@ -0,0 +1,44 @@ +validate(); + + // Remove violations of inaccessible fields as they cannot stem from our + // changes. + $violations->filterByFieldAccess(); + + if ($violations->count() > 0) { + $message = "Unprocessable Entity: validation failed.\n"; + foreach ($violations as $violation) { + $message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n"; + } + throw new UnprocessableEntityHttpException($message); + } + } + +} diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php index 584c003..1a52474 100644 --- a/core/modules/rest/src/Tests/RESTTestBase.php +++ b/core/modules/rest/src/Tests/RESTTestBase.php @@ -270,6 +270,15 @@ protected function entityValues($entity_type_id) { 'vid' => 'tags', 'name' => $this->randomMachineName(), ]; + case 'block': + // Block placements depend on themes, ensure Bartik is installed. + $this->container->get('theme_installer')->install(['bartik']); + return [ + 'id' => strtolower($this->randomMachineName(8)), + 'plugin' => 'system_powered_by_block', + 'theme' => 'bartik', + 'region' => 'header', + ]; default: if ($this->isConfigEntity($entity_type_id)) { return $this->configEntityValues($entity_type_id); diff --git a/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php b/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php new file mode 100644 index 0000000..20a6175 --- /dev/null +++ b/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php @@ -0,0 +1,73 @@ +getMockForTrait('Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait'); + + $method = new \ReflectionMethod($trait, 'validate'); + $method->setAccessible(TRUE); + + $entity = $this->prophesize(Node::class); + + $violations = $this->prophesize(EntityConstraintViolationList::class); + $violations->filterByFieldAccess()->willReturn([]); + $violations->count()->willReturn(0); + + $entity->validate()->willReturn($violations->reveal()); + + $method->invoke($trait, $entity->reveal()); + } + + /** + * @covers ::validate + */ + public function testFailedValidate() { + $violation1 = $this->prophesize(ConstraintViolationInterface::class); + $violation1->getPropertyPath()->willReturn('property_path'); + $violation1->getMessage()->willReturn('message'); + + $violation2 = $this->prophesize(ConstraintViolationInterface::class); + $violation2->getPropertyPath()->willReturn('property_path'); + $violation2->getMessage()->willReturn('message'); + + $entity = $this->prophesize(User::class); + + $violations = $this->getMockBuilder(EntityConstraintViolationList::class) + ->setConstructorArgs([$entity->reveal(), [$violation1->reveal(), $violation2->reveal()]]) + ->setMethods(['filterByFieldAccess']) + ->getMock(); + + $violations->expects($this->once()) + ->method('filterByFieldAccess') + ->will($this->returnValue([])); + + $entity->validate()->willReturn($violations); + + $trait = $this->getMockForTrait('Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait'); + + $method = new \ReflectionMethod($trait, 'validate'); + $method->setAccessible(TRUE); + + $this->setExpectedException(UnprocessableEntityHttpException::class); + + $method->invoke($trait, $entity->reveal()); + } + +} diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php index 15ecf1f..5876eb7 100644 --- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php +++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php @@ -323,13 +323,13 @@ function testNoThemeByDefault() { } /** - * Tests that drupal_get_profile() returns NULL. + * Tests that \Drupal::installProfile() returns FALSE. * * As the currently active installation profile is used when installing * configuration, for example, this is essential to ensure test isolation. */ public function testDrupalGetProfile() { - $this->assertNull(drupal_get_profile()); + $this->assertFalse(\Drupal::installProfile()); } /** diff --git a/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php b/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php new file mode 100644 index 0000000..32d2872 --- /dev/null +++ b/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php @@ -0,0 +1,148 @@ +connection = $connection; + $this->state = $state; + $this->requestStack = $request_stack; + } + + /** + * {@inheritdoc} + */ + public function recordView($id) { + return (bool) $this->connection + ->merge('node_counter') + ->key('nid', $id) + ->fields([ + 'daycount' => 1, + 'totalcount' => 1, + 'timestamp' => $this->getRequestTime(), + ]) + ->expression('daycount', 'daycount + 1') + ->expression('totalcount', 'totalcount + 1') + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function fetchViews($ids) { + $views = $this->connection + ->select('node_counter', 'nc') + ->fields('nc', ['totalcount', 'daycount', 'timestamp']) + ->condition('nid', $ids, 'IN') + ->execute() + ->fetchAll(); + foreach ($views as $id => $view) { + $views[$id] = new StatisticsViewsResult($view->totalcount, $view->daycount, $view->timestamp); + } + return $views; + } + + /** + * {@inheritdoc} + */ + public function fetchView($id) { + $views = $this->fetchViews(array($id)); + return reset($views); + } + + /** + * {@inheritdoc} + */ + public function fetchAll($order = 'totalcount', $limit = 5) { + assert(in_array($order, ['totalcount', 'daycount', 'timestamp']), "Invalid order argument."); + + return $this->connection + ->select('node_counter', 'nc') + ->fields('nc', ['nid']) + ->orderBy($order, 'DESC') + ->range(0, $limit) + ->execute() + ->fetchCol(); + } + + /** + * {@inheritdoc} + */ + public function deleteViews($id) { + return (bool) $this->connection + ->delete('node_counter') + ->condition('nid', $id) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function resetDayCount() { + $statistics_timestamp = $this->state->get('statistics.day_timestamp') ?: 0; + if (($this->getRequestTime() - $statistics_timestamp) >= 86400) { + $this->state->set('statistics.day_timestamp', $this->getRequestTime()); + $this->connection->update('node_counter') + ->fields(['daycount' => 0]) + ->execute(); + } + } + + /** + * {@inheritdoc} + */ + public function maxTotalCount() { + $query = $this->connection->select('node_counter', 'nc'); + $query->addExpression('MAX(totalcount)'); + $max_total_count = (int)$query->execute()->fetchField(); + return $max_total_count; + } + + /** + * Get current request time. + * + * @return int + * Unix timestamp for current server request time. + */ + protected function getRequestTime() { + return $this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME'); + } + +} diff --git a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php index 8f4384e..8bd84b2 100644 --- a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php @@ -4,8 +4,14 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\statistics\StatisticsStorageInterface; /** * Provides a 'Popular content' block. @@ -15,7 +21,72 @@ * admin_label = @Translation("Popular content") * ) */ -class StatisticsPopularBlock extends BlockBase { +class StatisticsPopularBlock extends BlockBase implements ContainerFactoryPluginInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity repository service. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + + /** + * The storage for statistics. + * + * @var \Drupal\statistics\StatisticsStorageInterface + */ + protected $statisticsStorage; + + /** + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Constructs an StatisticsPopularBlock object. + * + * @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 \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository service + * @param \Drupal\statistics\StatisticsStorageInterface $statistics_storage + * The storage for statistics. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, StatisticsStorageInterface $statistics_storage, RendererInterface $renderer) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->entityTypeManager = $entity_type_manager; + $this->entityRepository = $entity_repository; + $this->statisticsStorage = $statistics_storage; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity.repository'), + $container->get('statistics.storage.node'), + $container->get('renderer') + ); + } /** * {@inheritdoc} @@ -82,28 +153,64 @@ public function build() { $content = array(); if ($this->configuration['top_day_num'] > 0) { - $result = statistics_title_list('daycount', $this->configuration['top_day_num']); - if ($result) { - $content['top_day'] = node_title_list($result, $this->t("Today's:")); + $nids = $this->statisticsStorage->fetchAll('daycount', $this->configuration['top_day_num']); + if ($nids) { + $content['top_day'] = $this->nodeTitleList($nids, $this->t("Today's:")); $content['top_day']['#suffix'] = '
'; } } if ($this->configuration['top_all_num'] > 0) { - $result = statistics_title_list('totalcount', $this->configuration['top_all_num']); - if ($result) { - $content['top_all'] = node_title_list($result, $this->t('All time:')); + $nids = $this->statisticsStorage->fetchAll('totalcount', $this->configuration['top_all_num']); + if ($nids) { + $content['top_all'] = $this->nodeTitleList($nids, $this->t('All time:')); $content['top_all']['#suffix'] = '
'; } } if ($this->configuration['top_last_num'] > 0) { - $result = statistics_title_list('timestamp', $this->configuration['top_last_num']); - $content['top_last'] = node_title_list($result, $this->t('Last viewed:')); + $nids = $this->statisticsStorage->fetchAll('timestamp', $this->configuration['top_last_num']); + $content['top_last'] = $this->nodeTitleList($nids, $this->t('Last viewed:')); $content['top_last']['#suffix'] = '
'; } return $content; } + /** + * Generates the ordered array of node links for build(). + * + * @param int[] $nids + * An ordered array of node ids. + * @param string $title + * The title for the list. + * + * @return array + * A render array for the list. + */ + protected function nodeTitleList(array $nids, $title) { + $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids); + + $items = []; + foreach ($nids as $nid) { + $node = $this->entityRepository->getTranslationFromContext($nodes[$nid]); + $item = [ + '#type' => 'link', + '#title' => $node->getTitle(), + '#url' => $node->urlInfo('canonical'), + ]; + $this->renderer->addCacheableDependency($item, $node); + $items[] = $item; + } + + return [ + '#theme' => 'item_list__node', + '#items' => $items, + '#title' => $title, + '#cache' => [ + 'tags' => $this->entityTypeManager->getDefinition('node')->getListCacheTags(), + ], + ]; + } + } diff --git a/core/modules/statistics/src/StatisticsStorageInterface.php b/core/modules/statistics/src/StatisticsStorageInterface.php new file mode 100644 index 0000000..ccb51e4 --- /dev/null +++ b/core/modules/statistics/src/StatisticsStorageInterface.php @@ -0,0 +1,85 @@ +totalCount = $total_count; + $this->dayCount = $day_count; + $this->timestamp = $timestamp; + } + + /** + * Total number of times the entity has been viewed. + * + * @return int + */ + public function getTotalCount() { + return $this->totalCount; + } + + + /** + * Total number of times the entity has been viewed "today". + * + * @return int + */ + public function getDayCount() { + return $this->dayCount; + } + + + /** + * Timestamp of when the entity was last viewed. + * + * @return int + */ + public function getTimestamp() { + return $this->timestamp; + } + +} diff --git a/core/modules/statistics/src/Tests/StatisticsReportsTest.php b/core/modules/statistics/src/Tests/StatisticsReportsTest.php index 9c0d26c..0fe2e28 100644 --- a/core/modules/statistics/src/Tests/StatisticsReportsTest.php +++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php @@ -2,6 +2,9 @@ namespace Drupal\statistics\Tests; +use Drupal\Core\Cache\Cache; +use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait; + /** * Tests display of statistics report blocks. * @@ -9,6 +12,8 @@ */ class StatisticsReportsTest extends StatisticsTestBase { + use AssertPageCacheContextsAndTagsTrait; + /** * Tests the "popular content" block. */ @@ -30,7 +35,7 @@ function testPopularContentBlock() { $client->post($stats_path, array('headers' => $headers, 'body' => $post)); // Configure and save the block. - $this->drupalPlaceBlock('statistics_popular_block', array( + $block = $this->drupalPlaceBlock('statistics_popular_block', array( 'label' => 'Popular content', 'top_day_num' => 3, 'top_all_num' => 3, @@ -44,9 +49,16 @@ function testPopularContentBlock() { $this->assertText('All time', 'Found the all time popular content.'); $this->assertText('Last viewed', 'Found the last viewed popular content.'); - // statistics.module doesn't use node entities, prevent the node language - // from being added to the options. - $this->assertRaw(\Drupal::l($node->label(), $node->urlInfo('canonical', ['language' => NULL])), 'Found link to visited node.'); + $tags = Cache::mergeTags($node->getCacheTags(), $block->getCacheTags()); + $tags = Cache::mergeTags($tags, $this->blockingUser->getCacheTags()); + $tags = Cache::mergeTags($tags, ['block_view', 'config:block_list', 'node_list', 'rendered', 'user_view']); + $this->assertCacheTags($tags); + $contexts = Cache::mergeContexts($node->getCacheContexts(), $block->getCacheContexts()); + $contexts = Cache::mergeContexts($contexts, ['url.query_args:_wrapper_format']); + $this->assertCacheContexts($contexts); + + // Check if the node link is displayed. + $this->assertRaw(\Drupal::l($node->label(), $node->urlInfo('canonical')), 'Found link to visited node.'); } } diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module index 5079e43..5419645 100644 --- a/core/modules/statistics/statistics.module +++ b/core/modules/statistics/statistics.module @@ -52,9 +52,9 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array if ($context['view_mode'] != 'rss') { $links['#cache']['contexts'][] = 'user.permissions'; if (\Drupal::currentUser()->hasPermission('view post access counter')) { - $statistics = statistics_get($entity->id()); + $statistics = \Drupal::service('statistics.storage.node')->fetchView($entity->id()); if ($statistics) { - $statistics_links['statistics_counter']['title'] = \Drupal::translation()->formatPlural($statistics['totalcount'], '1 view', '@count views'); + $statistics_links['statistics_counter']['title'] = \Drupal::translation()->formatPlural($statistics->getTotalCount(), '1 view', '@count views'); $links['statistics'] = array( '#theme' => 'links__node__statistics', '#links' => $statistics_links, @@ -70,18 +70,10 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array * Implements hook_cron(). */ function statistics_cron() { - $statistics_timestamp = \Drupal::state()->get('statistics.day_timestamp') ?: 0; - - if ((REQUEST_TIME - $statistics_timestamp) >= 86400) { - // Reset day counts. - db_update('node_counter') - ->fields(array('daycount' => 0)) - ->execute(); - \Drupal::state()->set('statistics.day_timestamp', REQUEST_TIME); - } - - // Calculate the maximum of node views, for node search ranking. - \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, db_query('SELECT MAX(totalcount) FROM {node_counter}')->fetchField())); + $storage = \Drupal::service('statistics.storage.node'); + $storage->resetDayCount(); + $max_total_count = $storage->maxTotalCount(); + \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, $max_total_count)); } /** @@ -123,26 +115,21 @@ function statistics_title_list($dbfield, $dbrows) { return FALSE; } - /** * Retrieves a node's "view statistics". * - * @param int $nid - * The node ID. - * - * @return array - * An associative array containing: - * - totalcount: Integer for the total number of times the node has been - * viewed. - * - daycount: Integer for the total number of times the node has been viewed - * "today". For the daycount to be reset, cron must be enabled. - * - timestamp: Integer for the timestamp of when the node was last viewed. + * @deprecated in Drupal 8.2.x, will be removed before Drupal 9.0.0. + * Use \Drupal::service('statistics.storage.node')->fetchView($id). */ -function statistics_get($nid) { - - if ($nid > 0) { - // Retrieve an array with both totalcount and daycount. - return db_query('SELECT totalcount, daycount, timestamp FROM {node_counter} WHERE nid = :nid', array(':nid' => $nid), array('target' => 'replica'))->fetchAssoc(); +function statistics_get($id) { + if ($id > 0) { + /** @var \Drupal\statistics\StatisticsViewsResult $statistics */ + $statistics = \Drupal::service('statistics.storage.node')->fetchView($id); + return [ + 'totalcount' => $statistics->getTotalCount(), + 'daycount' => $statistics->getDayCount(), + 'timestamp' => $statistics->getTimestamp(), + ]; } } @@ -151,9 +138,8 @@ function statistics_get($nid) { */ function statistics_node_predelete(EntityInterface $node) { // Clean up statistics table when node is deleted. - db_delete('node_counter') - ->condition('nid', $node->id()) - ->execute(); + $id = $node->id(); + return \Drupal::service('statistics.storage.node')->deleteViews($id); } /** diff --git a/core/modules/statistics/statistics.php b/core/modules/statistics/statistics.php index a79af5f..a43509e 100644 --- a/core/modules/statistics/statistics.php +++ b/core/modules/statistics/statistics.php @@ -14,8 +14,9 @@ $kernel = DrupalKernel::createFromRequest(Request::createFromGlobals(), $autoloader, 'prod'); $kernel->boot(); +$container = $kernel->getContainer(); -$views = $kernel->getContainer() +$views = $container ->get('config.factory') ->get('statistics.settings') ->get('count_content_views'); @@ -23,15 +24,7 @@ if ($views) { $nid = filter_input(INPUT_POST, 'nid', FILTER_VALIDATE_INT); if ($nid) { - \Drupal::database()->merge('node_counter') - ->key('nid', $nid) - ->fields(array( - 'daycount' => 1, - 'totalcount' => 1, - 'timestamp' => REQUEST_TIME, - )) - ->expression('daycount', 'daycount + 1') - ->expression('totalcount', 'totalcount + 1') - ->execute(); + $container->get('request_stack')->push(Request::createFromGlobals()); + $container->get('statistics.storage.node')->recordView($nid); } } diff --git a/core/modules/statistics/statistics.services.yml b/core/modules/statistics/statistics.services.yml new file mode 100644 index 0000000..cf15573 --- /dev/null +++ b/core/modules/statistics/statistics.services.yml @@ -0,0 +1,6 @@ +services: + statistics.storage.node: + class: Drupal\statistics\NodeStatisticsDatabaseStorage + arguments: ['@database', '@state', '@request_stack'] + tags: + - { name: backend_overridable } diff --git a/core/modules/system/src/Tests/Installer/DistributionProfileExistingSettingsTest.php b/core/modules/system/src/Tests/Installer/DistributionProfileExistingSettingsTest.php new file mode 100644 index 0000000..bf3205c --- /dev/null +++ b/core/modules/system/src/Tests/Installer/DistributionProfileExistingSettingsTest.php @@ -0,0 +1,162 @@ +info = [ + 'type' => 'profile', + 'core' => \Drupal::CORE_COMPATIBILITY, + 'name' => 'Distribution profile', + 'distribution' => [ + 'name' => 'My Distribution', + 'install' => [ + 'theme' => 'bartik', + ], + ], + ]; + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/mydistro'; + mkdir($path, 0777, TRUE); + file_put_contents("$path/mydistro.info.yml", Yaml::encode($this->info)); + + // Pre-configure hash salt. + // Any string is valid, so simply use the class name of this test. + $this->settings['settings']['hash_salt'] = (object) [ + 'value' => __CLASS__, + 'required' => TRUE, + ]; + + // Pre-configure database credentials. + $connection_info = Database::getConnectionInfo(); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + + $this->settings['databases']['default'] = (object) [ + 'value' => $connection_info, + 'required' => TRUE, + ]; + + // Use the kernel to find the site path because the site.path service should + // not be available at this point in the install process. + $site_path = DrupalKernel::findSitePath(Request::createFromGlobals()); + // Pre-configure config directories. + $this->settings['config_directories'] = [ + CONFIG_SYNC_DIRECTORY => (object) [ + 'value' => $site_path . '/files/config_staging', + 'required' => TRUE, + ], + ]; + mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE); + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function setUpLanguage() { + // Make settings file not writable. + $filename = $this->siteDirectory . '/settings.php'; + // Make the settings file read-only. + // Not using File API; a potential error must trigger a PHP warning. + chmod($filename, 0444); + + // Verify that the distribution name appears. + $this->assertRaw($this->info['distribution']['name']); + // Verify that the requested theme is used. + $this->assertRaw($this->info['distribution']['install']['theme']); + // Verify that the "Choose profile" step does not appear. + $this->assertNoText('profile'); + + parent::setUpLanguage(); + } + + /** + * {@inheritdoc} + */ + protected function setUpProfile() { + // This step is skipped, because there is a distribution profile. + } + + /** + * {@inheritdoc} + */ + protected function setUpSettings() { + // This step should not appear, since settings.php is fully configured + // already. + } + + /** + * Confirms that the installation succeeded. + */ + public function testInstalled() { + $this->assertUrl('user/1'); + $this->assertResponse(200); + // Confirm that we are logged-in after installation. + $this->assertText($this->rootUser->getUsername()); + + // Confirm that Drupal recognizes this distribution as the current profile. + $this->assertEqual(\Drupal::installProfile(), 'mydistro'); + $this->assertNull(Settings::get('install_profile'), 'The install profile has not been written to settings.php.'); + + $this->rebuildContainer(); + $this->pass('Container can be rebuilt even though distribution is not written to settings.php.'); + + // Create another installation profile which is a distrubtion. + $info = [ + 'type' => 'profile', + 'core' => \Drupal::CORE_COMPATIBILITY, + 'name' => 'Distribution profile 2', + 'distribution' => [ + 'name' => 'My Distribution 2', + 'install' => [ + 'theme' => 'bartik', + ], + ], + ]; + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/mydistro2'; + mkdir($path, 0777, TRUE); + file_put_contents("$path/mydistro2.info.yml", Yaml::encode($info)); + + // Test that a site will multiple distributions will get an exception when + // rebuilding the container. In order to do this we need to reset the + // discovered files in ExtensionDiscovery. + try { + ExtensionDiscovery::reset(); + $this->rebuildContainer(); + $this->fail('TooManyDistributionsException exception thrown.'); + } + catch (TooManyDistributionsException $e) { + $this->pass('TooManyDistributionsException exception thrown.'); + $this->assertEqual('A site can only have one distribution, multiple installation profiles discovered: mydistro, mydistro2', $e->getMessage()); + } + } + +} diff --git a/core/modules/system/src/Tests/Installer/DistributionProfileTest.php b/core/modules/system/src/Tests/Installer/DistributionProfileTest.php index 0bb47ac..01e25e6 100644 --- a/core/modules/system/src/Tests/Installer/DistributionProfileTest.php +++ b/core/modules/system/src/Tests/Installer/DistributionProfileTest.php @@ -3,6 +3,7 @@ namespace Drupal\system\Tests\Installer; use Drupal\Core\Serialization\Yaml; +use Drupal\Core\Site\Settings; use Drupal\simpletest\InstallerTestBase; /** @@ -68,6 +69,10 @@ public function testInstalled() { $this->assertResponse(200); // Confirm that we are logged-in after installation. $this->assertText($this->rootUser->getUsername()); + + // Confirm that Drupal recognizes this distribution as the current profile. + $this->assertEqual(\Drupal::installProfile(), 'mydistro'); + $this->assertEqual(Settings::get('install_profile'), 'mydistro', 'The install profile has been written to settings.php.'); } } diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileBrokenTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileBrokenTest.php new file mode 100644 index 0000000..69a81c6 --- /dev/null +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileBrokenTest.php @@ -0,0 +1,131 @@ +settings['settings']['hash_salt'] = (object) [ + 'value' => __CLASS__, + 'required' => TRUE, + ]; + + // Pre-configure database credentials. + $connection_info = Database::getConnectionInfo(); + unset($connection_info['default']['pdo']); + unset($connection_info['default']['init_commands']); + + $this->settings['databases']['default'] = (object) [ + 'value' => $connection_info, + 'required' => TRUE, + ]; + + // During interactive install we'll change this to a different profile and + // this test will ensure that the new value is written to settings.php. + $this->settings['settings']['install_profile'] = (object) [ + 'value' => 'minimal', + 'required' => TRUE, + ]; + + // Pre-configure config directories. + $site_path = DrupalKernel::findSitePath(Request::createFromGlobals()); + $this->settings['config_directories'] = [ + CONFIG_SYNC_DIRECTORY => (object) [ + 'value' => $site_path . '/files/config_staging', + 'required' => TRUE, + ], + ]; + mkdir($this->settings['config_directories'][CONFIG_SYNC_DIRECTORY]->value, 0777, TRUE); + + // @todo Remove HTML once https://www.drupal.org/node/2514044 is fixed. + $this->exceptionMessage = (string) new FormattableMarkup('Failed to modify %path. Verify the file permissions.', ['%path' => $this->siteDirectory . '/settings.php']); + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function visitInstaller() { + // Make settings file not writable. This will break the installer. + $filename = $this->siteDirectory . '/settings.php'; + // Make the settings file read-only. + // Not using File API; a potential error must trigger a PHP warning. + chmod($filename, 0444); + + $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=testing'); + } + + /** + * {@inheritdoc} + */ + protected function setUpLanguage() { + // This step is skipped, because there is a lagcode as a query param. + } + + /** + * {@inheritdoc} + */ + protected function setUpProfile() { + // This step is skipped, because there is a profile as a query param. + } + + /** + * {@inheritdoc} + */ + protected function setUpSettings() { + // This step should not appear, since settings.php is fully configured + // already. + } + + protected function setUpSite() { + // This step should not appear, since settings.php could not be written. + } + + /** + * Verifies that installation did not succeed. + */ + public function testBrokenInstaller() { + $this->assertRaw(Html::escape($this->exceptionMessage)); + // The exceptions are expected. Do not interpret them as a test failure. + // Not using File API; a potential error must trigger a PHP warning. + unlink(\Drupal::root() . '/' . $this->siteDirectory . '/error.log'); + } + + /** + * {@inheritdoc} + */ + protected function error($message = '', $group = 'Other', array $caller = NULL) { + if ($group == 'Exception' && $message == $this->exceptionMessage) { + // Ignore the expected exception. + return FALSE; + } + return parent::error($message, $group, $caller); + } + +} diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileTest.php similarity index 64% copy from core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php copy to core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileTest.php index 84f861a..a179039 100644 --- a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsMismatchProfileTest.php @@ -13,7 +13,7 @@ * * @group Installer */ -class InstallerExistingSettingsNoProfileTest extends InstallerTestBase { +class InstallerExistingSettingsMismatchProfileTest extends InstallerTestBase { /** * {@inheritdoc} @@ -39,6 +39,13 @@ protected function setUp() { 'required' => TRUE, ); + // During interactive install we'll change this to a different profile and + // this test will ensure that the new value is written to settings.php. + $this->settings['settings']['install_profile'] = (object) [ + 'value' => 'minimal', + 'required' => TRUE, + ]; + // Pre-configure config directories. $this->settings['config_directories'] = array( CONFIG_SYNC_DIRECTORY => (object) array( @@ -54,6 +61,28 @@ protected function setUp() { /** * {@inheritdoc} */ + protected function visitInstaller() { + // Provide profile and language in query string to skip these pages. + $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=testing'); + } + + /** + * {@inheritdoc} + */ + protected function setUpLanguage() { + // This step is skipped, because there is a lagcode as a query param. + } + + /** + * {@inheritdoc} + */ + protected function setUpProfile() { + // This step is skipped, because there is a profile as a query param. + } + + /** + * {@inheritdoc} + */ protected function setUpSettings() { // This step should not appear, since settings.php is fully configured // already. @@ -65,7 +94,8 @@ protected function setUpSettings() { public function testInstaller() { $this->assertUrl('user/1'); $this->assertResponse(200); - $this->assertEqual('testing', Settings::get('install_profile')); + $this->assertEqual('testing', \Drupal::installProfile()); + $this->assertEqual('testing', Settings::get('install_profile'), 'Profile was correctly changed to testing in Settings.php'); } } diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php index 84f861a..9517308 100644 --- a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsNoProfileTest.php @@ -3,7 +3,6 @@ namespace Drupal\system\Tests\Installer; use Drupal\Core\DrupalKernel; -use Drupal\Core\Site\Settings; use Drupal\simpletest\InstallerTestBase; use Drupal\Core\Database\Database; use Symfony\Component\HttpFoundation\Request; @@ -65,7 +64,7 @@ protected function setUpSettings() { public function testInstaller() { $this->assertUrl('user/1'); $this->assertResponse(200); - $this->assertEqual('testing', Settings::get('install_profile')); + $this->assertEqual('testing', \Drupal::installProfile()); } } diff --git a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php index 6ecd9fd..eaca8fa 100644 --- a/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php +++ b/core/modules/system/src/Tests/Installer/InstallerExistingSettingsTest.php @@ -2,6 +2,7 @@ namespace Drupal\system\Tests\Installer; +use Drupal\Core\Site\Settings; use Drupal\simpletest\InstallerTestBase; use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; @@ -74,7 +75,8 @@ protected function setUpSettings() { public function testInstaller() { $this->assertUrl('user/1'); $this->assertResponse(200); - $this->assertEqual('testing', drupal_get_profile(), 'Profile was changed from minimal to testing during interactive install.'); + $this->assertEqual('testing', \Drupal::installProfile(), 'Profile was changed from minimal to testing during interactive install.'); + $this->assertEqual('testing', Settings::get('install_profile'), 'Profile was correctly changed to testing in Settings.php'); } } diff --git a/core/modules/system/src/Tests/Installer/MultipleDistributionsProfileTest.php b/core/modules/system/src/Tests/Installer/MultipleDistributionsProfileTest.php new file mode 100644 index 0000000..4ac889e --- /dev/null +++ b/core/modules/system/src/Tests/Installer/MultipleDistributionsProfileTest.php @@ -0,0 +1,109 @@ + 'profile', + 'core' => \Drupal::CORE_COMPATIBILITY, + 'name' => $name . ' profile', + 'distribution' => [ + 'name' => $name, + 'install' => [ + 'theme' => 'bartik', + ], + ], + ]; + // File API functions are not available yet. + $path = $this->siteDirectory . '/profiles/' . $name; + mkdir($path, 0777, TRUE); + file_put_contents("$path/$name.info.yml", Yaml::encode($info)); + } + // Install the first distribution. + $this->profile = 'distribution_one'; + + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function setUpLanguage() { + $this->assertNoRaw('distribution_one'); + $this->assertNoRaw('distribution_two'); + // Verify that the "Choose profile" step appears. + $this->assertText('Choose profile'); + + parent::setUpLanguage(); + } + + /** + * {@inheritdoc} + */ + protected function setUpProfile() { + $this->assertText('distribution_one'); + $this->assertText('distribution_two'); + parent::setUpProfile(); + } + + /** + * Confirms that the installation succeeded. + */ + public function testInstalled() { + $this->assertUrl('user/1'); + $this->assertResponse(200); + // Confirm that we are logged-in after installation. + $this->assertText($this->rootUser->getUsername()); + + // Confirm that Drupal recognizes this distribution as the current profile. + $this->assertEqual(\Drupal::installProfile(), 'distribution_one'); + $this->assertEqual(Settings::get('install_profile'), 'distribution_one', 'The install profile has been written to settings.php.'); + + // Test that a site will multiple distributions will get an exception when + // calling \Drupal\Core\DrupalKernel::getDistribution(). + try { + $kernel = $this->container->get('kernel'); + // getDistribution() is protected in the DrupalKernel class. + // Call setAccessible(TRUE) so that we can call it to test its behavior. + $getDistributionMethod = new \ReflectionMethod($kernel, 'getDistribution'); + $getDistributionMethod->setAccessible(TRUE); + $getDistributionMethod->invokeArgs($kernel, array()); + $this->fail('TooManyDistributionsException exception thrown.'); + } + catch (TooManyDistributionsException $e) { + $this->pass('TooManyDistributionsException exception thrown.'); + } + // To mirror the test in DistributionProfileExistingSettingsTest we reset + // the discovered files in ExtensionDiscovery. + // @see \Drupal\system\Tests\Installer\DistributionProfileExistingSettingsTest::testInstalled() + ExtensionDiscovery::reset(); + $this->rebuildContainer(); + $this->pass('Container can be rebuilt as distribution is written to settings.php.'); + } + +} diff --git a/core/modules/system/src/Tests/System/AccessDeniedTest.php b/core/modules/system/src/Tests/System/AccessDeniedTest.php index 81f9fad..e7f17d7 100644 --- a/core/modules/system/src/Tests/System/AccessDeniedTest.php +++ b/core/modules/system/src/Tests/System/AccessDeniedTest.php @@ -65,7 +65,7 @@ function testAccessDenied() { $this->drupalPostForm('admin/config/system/site-information', $edit, t('Save configuration')); // Enable the user login block. - $this->drupalPlaceBlock('user_login_block', array('id' => 'login')); + $block = $this->drupalPlaceBlock('user_login_block', array('id' => 'login')); // Log out and check that the user login block is shown on custom 403 pages. $this->drupalLogout(); @@ -90,10 +90,7 @@ function testAccessDenied() { // Log back in, set the custom 403 page to /user/login and remove the block $this->drupalLogin($this->adminUser); $this->config('system.site')->set('page.403', '/user/login')->save(); - $edit = [ - 'region' => -1, - ]; - $this->drupalPostForm('admin/structure/block/manage/login', $edit, t('Save block')); + $block->disable()->save(); // Check that we can log in from the 403 page. $this->drupalLogout(); diff --git a/core/modules/system/tests/fixtures/update/block.block.secondtestfor2513534.yml b/core/modules/system/tests/fixtures/update/block.block.secondtestfor2513534.yml new file mode 100644 index 0000000..2999cab --- /dev/null +++ b/core/modules/system/tests/fixtures/update/block.block.secondtestfor2513534.yml @@ -0,0 +1,18 @@ +uuid: 3c4e92c3-5fb1-408d-993c-6066559230be +langcode: en +status: true +dependencies: + theme: + - bartik +id: pagetitle_2 +theme: bartik +region: '-1' +weight: null +provider: null +plugin: page_title_block +settings: + id: page_title_block + label: 'Page title' + provider: core + label_display: '0' +visibility: { } diff --git a/core/modules/system/tests/fixtures/update/block.block.testfor2513534.yml b/core/modules/system/tests/fixtures/update/block.block.testfor2513534.yml new file mode 100644 index 0000000..b8a55fa --- /dev/null +++ b/core/modules/system/tests/fixtures/update/block.block.testfor2513534.yml @@ -0,0 +1,18 @@ +uuid: 87097da9-29d1-441f-8b00-b93852c760d6 +langcode: en +status: false +dependencies: + theme: + - bartik +id: pagetitle_1 +theme: bartik +region: '-1' +weight: -8 +provider: null +plugin: page_title_block +settings: + id: page_title_block + label: 'Page title' + provider: core + label_display: '0' +visibility: { } diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-block-disabled-2513534.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-block-disabled-2513534.php new file mode 100644 index 0000000..fc8e1c5 --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-block-disabled-2513534.php @@ -0,0 +1,50 @@ +insert('config') + ->fields(array( + 'collection', + 'name', + 'data', + )) + ->values(array( + 'collection' => '', + 'name' => 'block.block.' . $block_config['id'], + 'data' => serialize($block_config), + )) + ->execute(); +} + +// Update the config entity query "index". +$existing_blocks = $connection->select('key_value') + ->fields('key_value', ['value']) + ->condition('collection', 'config.entity.key_store.block') + ->condition('name', 'theme:bartik') + ->execute() + ->fetchField(); +$existing_blocks = unserialize($existing_blocks); + +$connection->update('key_value') + ->fields([ + 'value' => serialize(array_merge($existing_blocks, ['block.block.testfor2513534', 'block.block.secondtestfor2513534'])) + ]) + ->condition('collection', 'config.entity.key_store.block') + ->condition('name', 'theme:bartik') + ->execute(); diff --git a/core/modules/taxonomy/src/Tests/VocabularyUiTest.php b/core/modules/taxonomy/src/Tests/VocabularyUiTest.php index 3066ef0..76fcac2 100644 --- a/core/modules/taxonomy/src/Tests/VocabularyUiTest.php +++ b/core/modules/taxonomy/src/Tests/VocabularyUiTest.php @@ -47,14 +47,17 @@ function testVocabularyInterface() { // Edit the vocabulary. $this->drupalGet('admin/structure/taxonomy'); - $this->assertText($edit['name'], 'Vocabulary found in the vocabulary overview listing.'); + $this->assertText($edit['name'], 'Vocabulary name found in the vocabulary overview listing.'); + $this->assertText($edit['description'], 'Vocabulary description found in the vocabulary overview listing.'); $this->assertLinkByHref(Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $edit['vid']])->toString()); $this->clickLink(t('Edit vocabulary')); $edit = array(); $edit['name'] = $this->randomMachineName(); + $edit['description'] = $this->randomMachineName(); $this->drupalPostForm(NULL, $edit, t('Save')); $this->drupalGet('admin/structure/taxonomy'); - $this->assertText($edit['name'], 'Vocabulary found in the vocabulary overview listing.'); + $this->assertText($edit['name'], 'Vocabulary name found in the vocabulary overview listing.'); + $this->assertText($edit['description'], 'Vocabulary description found in the vocabulary overview listing.'); // Try to submit a vocabulary with a duplicate machine name. $edit['vid'] = $vid; diff --git a/core/modules/taxonomy/src/VocabularyListBuilder.php b/core/modules/taxonomy/src/VocabularyListBuilder.php index b5597bb..9c24311 100644 --- a/core/modules/taxonomy/src/VocabularyListBuilder.php +++ b/core/modules/taxonomy/src/VocabularyListBuilder.php @@ -56,6 +56,7 @@ public function getDefaultOperations(EntityInterface $entity) { */ public function buildHeader() { $header['label'] = t('Vocabulary name'); + $header['description'] = t('Description'); return $header + parent::buildHeader(); } @@ -64,6 +65,7 @@ public function buildHeader() { */ public function buildRow(EntityInterface $entity) { $row['label'] = $entity->label(); + $row['description']['data'] = ['#markup' => $entity->getDescription()]; return $row + parent::buildRow($entity); } diff --git a/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php b/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php new file mode 100644 index 0000000..6a243c3 --- /dev/null +++ b/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php @@ -0,0 +1,190 @@ +userSettings = $user_settings; + $this->currentUser = $current_user; + } + + /** + * {@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('config.factory')->get('user.settings'), + $container->get('current_user') + ); + } + + /** + * Responds to user registration POST request. + * + * @param \Drupal\user\UserInterface $account + * The user account entity. + * + * @return \Drupal\rest\ModifiedResourceResponse + * The HTTP response object. + * + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function post(UserInterface $account = NULL) { + $this->ensureAccountCanRegister($account); + + // Only activate new users if visitors are allowed to register and no email + // verification required. + if ($this->userSettings->get('register') == USER_REGISTER_VISITORS && !$this->userSettings->get('verify_mail')) { + $account->activate(); + } + else { + $account->block(); + } + + $this->checkEditFieldAccess($account); + + // Make sure that the user entity is valid (email and name are valid). + $this->validate($account); + + // Create the account. + $account->save(); + + $this->sendEmailNotifications($account); + + return new ModifiedResourceResponse($account, 200); + } + + /** + * Ensure the account can be registered in this request. + * + * @param \Drupal\user\UserInterface $account + * The user account to register. + */ + protected function ensureAccountCanRegister(UserInterface $account = NULL) { + if ($account === NULL) { + throw new BadRequestHttpException('No user account data for registration received.'); + } + + // POSTed user accounts must not have an ID set, because we always want to + // create new entities here. + if (!$account->isNew()) { + throw new BadRequestHttpException('An ID has been set and only new user accounts can be registered.'); + } + + // Only allow anonymous users to register, authenticated users with the + // necessary permissions can POST a new user to the "user" REST resource. + // @see \Drupal\rest\Plugin\rest\resource\EntityResource + if (!$this->currentUser->isAnonymous()) { + throw new AccessDeniedHttpException('Only anonymous users can register a user.'); + } + + // Verify that the current user can register a user account. + if ($this->userSettings->get('register') == USER_REGISTER_ADMINISTRATORS_ONLY) { + throw new AccessDeniedHttpException('You cannot register a new user account.'); + } + + if (!$this->userSettings->get('verify_mail')) { + if (empty($account->getPassword())) { + // If no e-mail verification then the user must provide a password. + throw new UnprocessableEntityHttpException('No password provided.'); + } + } + else { + if (!empty($account->getPassword())) { + // If e-mail verification required then a password cannot provided. + // The password will be set when the user logs in. + throw new UnprocessableEntityHttpException('A Password cannot be specified. It will be generated on login.'); + } + } + } + + /** + * Sends email notifications if necessary for user that was registered. + * + * @param \Drupal\user\UserInterface $account + * The user account. + */ + protected function sendEmailNotifications(UserInterface $account) { + $approval_settings = $this->userSettings->get('register'); + // No e-mail verification is required. Activating the user. + if ($approval_settings == USER_REGISTER_VISITORS) { + if ($this->userSettings->get('verify_mail')) { + // No administrator approval required. + _user_mail_notify('register_no_approval_required', $account); + } + } + // Administrator approval required. + elseif ($approval_settings == USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) { + _user_mail_notify('register_pending_approval', $account); + } + } + +} diff --git a/core/modules/user/src/Tests/RestRegisterUserTest.php b/core/modules/user/src/Tests/RestRegisterUserTest.php new file mode 100644 index 0000000..c10d028 --- /dev/null +++ b/core/modules/user/src/Tests/RestRegisterUserTest.php @@ -0,0 +1,187 @@ +enableService('user_registration', 'POST', 'hal_json'); + + Role::load(RoleInterface::ANONYMOUS_ID) + ->grantPermission('restful post user_registration') + ->save(); + + Role::load(RoleInterface::AUTHENTICATED_ID) + ->grantPermission('restful post user_registration') + ->save(); + } + + /** + * Tests that only anonymous users can register users. + */ + public function testRegisterUser() { + // Verify that an authenticated user cannot register a new user, despite + // being granted permission to do so because only anonymous users can + // register themselves, authenticated users with the necessary permissions + // can POST a new user to the "user" REST resource. + $user = $this->createUser(); + $this->drupalLogin($user); + $this->registerRequest('palmer.eldritch'); + $this->assertResponse('403', 'Only anonymous users can register users.'); + $this->drupalLogout(); + + $user_settings = $this->config('user.settings'); + + // Test out different setting User Registration and Email Verification. + // Allow visitors to register with no email verification. + $user_settings->set('register', USER_REGISTER_VISITORS); + $user_settings->set('verify_mail', 0); + $user_settings->save(); + $user = $this->registerUser('Palmer.Eldritch'); + $this->assertFalse($user->isBlocked()); + $this->assertFalse(empty($user->getPassword())); + $email_count = count($this->drupalGetMails()); + $this->assertEqual(0, $email_count); + + // Attempt to register without sending a password. + $this->registerRequest('Rick.Deckard', FALSE); + $this->assertResponse('422', 'No password provided'); + + // Allow visitors to register with email verification. + $user_settings->set('register', USER_REGISTER_VISITORS); + $user_settings->set('verify_mail', 1); + $user_settings->save(); + $user = $this->registerUser('Jason.Taverner', FALSE); + $this->assertTrue(empty($user->getPassword())); + $this->assertTrue($user->isBlocked()); + $this->assertMailString('body', 'You may now log in by clicking this link', 1); + + // Attempt to register with a password when e-mail verification is on. + $this->registerRequest('Estraven', TRUE); + $this->assertResponse('422', 'A Password cannot be specified. It will be generated on login.'); + + // Allow visitors to register with Admin approval and e-mail verification. + $user_settings->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL); + $user_settings->set('verify_mail', 1); + $user_settings->save(); + $user = $this->registerUser('Bob.Arctor', FALSE); + $this->assertTrue(empty($user->getPassword())); + $this->assertTrue($user->isBlocked()); + $this->assertMailString('body', 'Your application for an account is', 2); + $this->assertMailString('body', 'Bob.Arctor has applied for an account', 2); + + // Attempt to register with a password when e-mail verification is on. + $this->registerRequest('Ursula', TRUE); + $this->assertResponse('422', 'A Password cannot be specified. It will be generated on login.'); + + // Allow visitors to register with Admin approval and no email verification. + $user_settings->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL); + $user_settings->set('verify_mail', 0); + $user_settings->save(); + $user = $this->registerUser('Argaven'); + $this->assertFalse(empty($user->getPassword())); + $this->assertTrue($user->isBlocked()); + $this->assertMailString('body', 'Your application for an account is', 2); + $this->assertMailString('body', 'Argaven has applied for an account', 2); + + // Attempt to register without sending a password. + $this->registerRequest('Tibe', FALSE); + $this->assertResponse('422', 'No password provided'); + } + + /** + * Creates serialize user values. + * + * @param string $name + * The name of the user. Use only valid values for emails. + * + * @param bool $include_password + * Whether to include a password in the user values. + * + * @return string Serialized user values. + * Serialized user values. + */ + protected function createSerializedUser($name, $include_password = TRUE) { + global $base_url; + // New user info to be serialized. + $data = [ + "_links" => + [ + "type" => ["href" => $base_url . "/rest/type/user/user"], + ], + "langcode" => [ + [ + "value" => "en", + ], + ], + "name" => [ + [ + "value" => $name, + ], + ], + "mail" => [ + [ + "value" => "$name@example.com", + ], + ], + ]; + if ($include_password) { + $data['pass']['value'] = 'SuperSecretPassword'; + } + + // Create a HAL+JSON version for the user entity we want to create. + $serialized = $this->container->get('serializer') + ->serialize($data, 'hal_json'); + return $serialized; + } + + /** + * Registers a user via REST resource. + * + * @param $name + * User name. + * + * @param bool $include_password + * + * @return bool|\Drupal\user\Entity\User + */ + protected function registerUser($name, $include_password = TRUE) { + // Verify that an anonymous user can register. + $this->registerRequest($name, $include_password); + $this->assertResponse('200', 'HTTP response code is correct.'); + $user = user_load_by_name($name); + $this->assertFalse(empty($user), 'User was create as expected'); + return $user; + } + + /** + * Make a REST user registration request. + * + * @param $name + * @param $include_password + */ + protected function registerRequest($name, $include_password = TRUE) { + $serialized = $this->createSerializedUser($name, $include_password); + $this->httpRequest('/user/register', 'POST', $serialized, 'application/hal+json'); + } + +} diff --git a/core/modules/user/tests/src/Unit/UserRegistrationResourceTest.php b/core/modules/user/tests/src/Unit/UserRegistrationResourceTest.php new file mode 100644 index 0000000..142685c --- /dev/null +++ b/core/modules/user/tests/src/Unit/UserRegistrationResourceTest.php @@ -0,0 +1,151 @@ +logger = $this->prophesize(LoggerInterface::class)->reveal(); + + $this->userSettings = $this->prophesize(ImmutableConfig::class); + + $this->currentUser = $this->prophesize(AccountInterface::class); + + $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->userSettings->reveal(), $this->currentUser->reveal()); + $this->reflection = new \ReflectionClass($this->testClass); + } + + /** + * Tests that an exception is thrown when no data provided for the account. + */ + public function testEmptyPost() { + $this->setExpectedException(BadRequestHttpException::class); + $this->testClass->post(NULL); + } + + /** + * Tests that only new user accounts can be registered. + */ + public function testExistedEntityPost() { + $entity = $this->prophesize(User::class); + $entity->isNew()->willReturn(FALSE); + $this->setExpectedException(BadRequestHttpException::class); + + $this->testClass->post($entity->reveal()); + } + + /** + * Tests that admin permissions are required to register a user account. + */ + public function testRegistrationAdminOnlyPost() { + + $this->userSettings->get('register')->willReturn(USER_REGISTER_ADMINISTRATORS_ONLY); + + $this->currentUser->isAnonymous()->willReturn(TRUE); + + $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->userSettings->reveal(), $this->currentUser->reveal()); + + $entity = $this->prophesize(User::class); + $entity->isNew()->willReturn(TRUE); + + $this->setExpectedException(AccessDeniedHttpException::class); + + $this->testClass->post($entity->reveal()); + } + + /** + * Tests that only anonymous users can register users. + */ + public function testRegistrationAnonymousOnlyPost() { + $this->currentUser->isAnonymous()->willReturn(FALSE); + + $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->userSettings->reveal(), $this->currentUser->reveal()); + + $entity = $this->prophesize(User::class); + $entity->isNew()->willReturn(TRUE); + + $this->setExpectedException(AccessDeniedHttpException::class); + + $this->testClass->post($entity->reveal()); + } + +} diff --git a/core/modules/views/tests/src/Kernel/Handler/AreaEntityTest.php b/core/modules/views/tests/src/Kernel/Handler/AreaEntityTest.php index af3d658..ebf9e05 100644 --- a/core/modules/views/tests/src/Kernel/Handler/AreaEntityTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/AreaEntityTest.php @@ -2,9 +2,9 @@ namespace Drupal\Tests\views\Kernel\Handler; -use Drupal\block\Entity\Block; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormState; +use Drupal\simpletest\BlockCreationTrait; use Drupal\Tests\views\Kernel\ViewsKernelTestBase; use Drupal\views\Entity\View; use Drupal\views\Views; @@ -17,6 +17,8 @@ */ class AreaEntityTest extends ViewsKernelTestBase { + use BlockCreationTrait; + /** * Modules to enable. * @@ -42,14 +44,15 @@ protected function setUp($import_test_views = TRUE) { * {@inheritdoc} */ protected function setUpFixtures() { + // Install the themes used for this test. + $this->container->get('theme_installer')->install(['bartik']); + $this->container->get('config.factory')->getEditable('system.theme')->set('default', 'bartik')->save(); + $this->installEntitySchema('user'); $this->installEntitySchema('entity_test'); $this->installConfig(['entity_test']); - Block::create([ - 'id' => 'test_block', - 'plugin' => 'system_main_block', - ])->save(); + $this->placeBlock('system_main_block', ['id' => 'test_block']); parent::setUpFixtures(); } diff --git a/core/modules/views/tests/src/Kernel/Handler/AreaOrderTest.php b/core/modules/views/tests/src/Kernel/Handler/AreaOrderTest.php index 457c635..9ade514 100644 --- a/core/modules/views/tests/src/Kernel/Handler/AreaOrderTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/AreaOrderTest.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\views\Kernel\Handler; -use Drupal\block\Entity\Block; +use Drupal\simpletest\BlockCreationTrait; use Drupal\Tests\views\Kernel\ViewsKernelTestBase; use Drupal\views\Views; @@ -14,6 +14,8 @@ */ class AreaOrderTest extends ViewsKernelTestBase { + use BlockCreationTrait; + /** * Modules to enable. * @@ -32,23 +34,21 @@ class AreaOrderTest extends ViewsKernelTestBase { * {@inheritdoc} */ protected function setUpFixtures() { - Block::create( - [ - 'id' => 'bartik_branding', - 'theme' => 'bartik', - 'plugin' => 'system_branding_block', - 'weight' => 1, - ] - )->save(); + // Install the themes used for this test. + $this->container->get('theme_installer')->install(['bartik']); + + $this->placeBlock('system_branding_block', [ + 'id' => 'bartik_branding', + 'theme' => 'bartik', + 'plugin' => 'system_branding_block', + 'weight' => 1, + ]); - Block::create( - [ - 'id' => 'bartik_powered', - 'theme' => 'bartik', - 'plugin' => 'system_powered_by_block', - 'weight' => 2, - ] - )->save(); + $this->placeBlock('system_powered_by_block', [ + 'id' => 'bartik_powered', + 'theme' => 'bartik', + 'weight' => 2, + ]); parent::setUpFixtures(); } diff --git a/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php b/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php index 6cdc4b3..e2108b3 100644 --- a/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php +++ b/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php @@ -2,6 +2,7 @@ namespace Drupal\KernelTests\Core\Bootstrap; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\KernelTests\KernelTestBase; /** @@ -17,14 +18,18 @@ class GetFilenameTest extends KernelTestBase { public static $modules = ['system']; /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + parent::register($container); + // Use the testing install profile. + $container->setParameter('install_profile', 'testing'); + } + + /** * Tests that drupal_get_filename() works when the file is not in database. */ function testDrupalGetFilename() { - // drupal_get_profile() is using obtaining the profile from state if the - // install_state global is not set. - global $install_state; - $install_state['parameters']['profile'] = 'testing'; - // Rebuild system.module.files state data. // @todo Remove as part of https://www.drupal.org/node/2186491 drupal_static_reset('system_rebuild_module_data'); diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 0da98e8..94a1e04 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -144,6 +144,11 @@ * @code * 'prefix' => 'main_', * @endcode + * + * Per-table prefixes are deprecated as of Drupal 8.2, and will be removed in + * Drupal 9.0. After that, only a single prefix for all tables will be + * supported. + * * To provide prefixes for specific tables, set 'prefix' as an array. * The array's keys are the table names and the values are the prefixes. * The 'default' element is mandatory and holds the prefix for any tables