Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains \Drupal\system\Access\CronAccessCheck.
*/
namespace Drupal\system\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
/**
* Access check for cron routes.
*/
class CronAccessCheck implements AccessInterface {
/**
* Checks access.
*
* @param string $key
* The cron key.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access($key) {
if ($key != \Drupal::state()->get('system.cron_key')) {
\Drupal::logger('cron')->notice('Cron could not run because an invalid key was used.');
return AccessResult::forbidden()->setCacheMaxAge(0);
}
elseif (\Drupal::state()->get('system.maintenance_mode')) {
\Drupal::logger('cron')->notice('Cron could not run because the site is in maintenance mode.');
return AccessResult::forbidden()->setCacheMaxAge(0);
}
return AccessResult::allowed()->setCacheMaxAge(0);
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\system\Access\DbUpdateAccessCheck.
*/
namespace Drupal\system\Access;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
/**
* Access check for database update routes.
*/
class DbUpdateAccessCheck implements AccessInterface {
/**
* Checks access for update routes.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(AccountInterface $account) {
// Allow the global variable in settings.php to override the access check.
if (Settings::get('update_free_access')) {
return AccessResult::allowed()->setCacheMaxAge(0);
}
if ($account->hasPermission('administer software updates')) {
return AccessResult::allowed()->cachePerPermissions();
}
else {
return AccessResult::forbidden()->cachePerPermissions();
}
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains \Drupal\system\ActionConfigEntityInterface.
*/
namespace Drupal\system;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a action entity.
*/
interface ActionConfigEntityInterface extends ConfigEntityInterface {
/**
* Returns whether or not this action is configurable.
*
* @return bool
*/
public function isConfigurable();
/**
* Returns the operation type.
*
* @return string
*/
public function getType();
/**
* Returns the operation plugin.
*
* @return \Drupal\Core\Action\ActionInterface
*/
public function getPlugin();
}

View file

@ -0,0 +1,58 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\AdminController.
*/
namespace Drupal\system\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller for admin section.
*/
class AdminController extends ControllerBase {
/**
* Prints a listing of admin tasks, organized by module.
*
* @return array
* A render array containing the listing.
*/
public function index() {
$module_info = system_get_info('module');
foreach ($module_info as $module => $info) {
$module_info[$module] = new \stdClass();
$module_info[$module]->info = $info;
}
uasort($module_info, 'system_sort_modules_by_info_name');
$menu_items = array();
foreach ($module_info as $module => $info) {
// Only display a section if there are any available tasks.
if ($admin_tasks = system_get_module_admin_tasks($module, $info->info)) {
// Sort links by title.
uasort($admin_tasks, array('\Drupal\Component\Utility\SortArray', 'sortByTitleElement'));
// Move 'Configure permissions' links to the bottom of each section.
$permission_key = "user.admin_permissions.$module";
if (isset($admin_tasks[$permission_key])) {
$permission_task = $admin_tasks[$permission_key];
unset($admin_tasks[$permission_key]);
$admin_tasks[$permission_key] = $permission_task;
}
$menu_items[$info->info['name']] = array($info->info['description'], $admin_tasks);
}
}
$output = array(
'#theme' => 'system_admin_index',
'#menu_items' => $menu_items,
);
return $output;
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\BatchController.
*/
namespace Drupal\system\Controller;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Controller routines for batch routes.
*/
class BatchController implements ContainerInjectionInterface {
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new BatchController.
*
* @param string $root
* The app root.
*/
public function __construct($root) {
$this->root = $root;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('app.root')
);
}
/**
* Returns a system batch page.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return \Symfony\Component\HttpFoundation\Response|array
* A \Symfony\Component\HttpFoundation\Response object or render array.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function batchPage(Request $request) {
require_once $this->root . '/core/includes/batch.inc';
$output = _batch_page($request);
if ($output === FALSE) {
throw new AccessDeniedHttpException();
}
elseif ($output instanceof Response) {
return $output;
}
elseif (isset($output)) {
$page = [
'#type' => 'page',
'#show_messages' => FALSE,
'content' => $output,
];
return $page;
}
}
/**
* The _title_callback for the system.batch_page.normal route.
*
* @return string
* The page title.
*/
public function batchPageTitle() {
$current_set = _batch_current_set();
return !empty($current_set['title']) ? $current_set['title'] : '';
}
}

View file

@ -0,0 +1,674 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\DbUpdateController.
*/
namespace Drupal\system\Controller;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\Core\Render\BareHtmlPageRendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller routines for database update routes.
*/
class DbUpdateController extends ControllerBase {
/**
* The keyvalue expirable factory.
*
* @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface
*/
protected $keyValueExpirableFactory;
/**
* A cache backend interface.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The entity definition update manager.
*
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
*/
protected $entityDefinitionUpdateManager;
/**
* The bare HTML page renderer.
*
* @var \Drupal\Core\Render\BareHtmlPageRendererInterface
*/
protected $bareHtmlPageRenderer;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new UpdateController.
*
* @param string $root
* The app root.
* @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
* The keyvalue expirable factory.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* A cache backend interface.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_update_manager
* The entity definition update manager.
* @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
* The bare HTML page renderer.
*/
public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, BareHtmlPageRendererInterface $bare_html_page_renderer) {
$this->root = $root;
$this->keyValueExpirableFactory = $key_value_expirable_factory;
$this->cache = $cache;
$this->state = $state;
$this->moduleHandler = $module_handler;
$this->account = $account;
$this->entityDefinitionUpdateManager = $entity_definition_update_manager;
$this->bareHtmlPageRenderer = $bare_html_page_renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('app.root'),
$container->get('keyvalue.expirable'),
$container->get('cache.default'),
$container->get('state'),
$container->get('module_handler'),
$container->get('current_user'),
$container->get('entity.definition_update_manager'),
$container->get('bare_html_page_renderer')
);
}
/**
* Returns a database update page.
*
* @param string $op
* The update operation to perform. Can be any of the below:
* - info
* - selection
* - run
* - results
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return \Symfony\Component\HttpFoundation\Response
* A response object object.
*/
public function handle($op, Request $request) {
require_once $this->root . '/core/includes/install.inc';
require_once $this->root . '/core/includes/update.inc';
drupal_load_updates();
update_fix_compatibility();
if ($request->query->get('continue')) {
$_SESSION['update_ignore_warnings'] = TRUE;
}
$regions = array();
$requirements = update_check_requirements();
$severity = drupal_requirements_severity($requirements);
if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_WARNING && empty($_SESSION['update_ignore_warnings']))) {
$regions['sidebar_first'] = $this->updateTasksList('requirements');
$output = $this->requirements($severity, $requirements);
}
else {
switch ($op) {
case 'selection':
$regions['sidebar_first'] = $this->updateTasksList('selection');
$output = $this->selection();
break;
case 'run':
$regions['sidebar_first'] = $this->updateTasksList('run');
$output = $this->triggerBatch($request);
break;
case 'info':
$regions['sidebar_first'] = $this->updateTasksList('info');
$output = $this->info();
break;
case 'results':
$regions['sidebar_first'] = $this->updateTasksList('results');
$output = $this->results();
break;
// Regular batch ops : defer to batch processing API.
default:
require_once $this->root . '/core/includes/batch.inc';
$regions['sidebar_first'] = $this->updateTasksList('run');
$output = _batch_page($request);
break;
}
}
if ($output instanceof Response) {
return $output;
}
$title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update');
return $this->bareHtmlPageRenderer->renderBarePage($output, $title, 'maintenance_page', $regions);
}
/**
* Returns the info database update page.
*
* @return array
* A render array.
*/
protected function info() {
// Change query-strings on css/js files to enforce reload for all users.
_drupal_flush_css_js();
// Flush the cache of all data for the update status module.
$this->keyValueExpirableFactory->get('update')->deleteAll();
$this->keyValueExpirableFactory->get('update_available_release')->deleteAll();
$build['info_header'] = array(
'#markup' => '<p>' . $this->t('Use this utility to update your database whenever a new release of Drupal or a module is installed.') . '</p><p>' . $this->t('For more detailed information, see the <a href="https://www.drupal.org/upgrade">upgrading handbook</a>. If you are unsure what these terms mean you should probably contact your hosting provider.') . '</p>',
);
$info[] = $this->t("<strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.");
$info[] = $this->t('Put your site into <a href="@url">maintenance mode</a>.', array(
'@url' => $this->url('system.site_maintenance_mode'),
));
$info[] = $this->t('<strong>Back up your database</strong>. This process will change your database values and in case of emergency you may need to revert to a backup.');
$info[] = $this->t('Install your new files in the appropriate location, as described in the handbook.');
$build['info'] = array(
'#theme' => 'item_list',
'#list_type' => 'ol',
'#items' => $info,
);
$build['info_footer'] = array(
'#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>',
);
$url = new Url('system.db_update', array('op' => 'selection'));
$build['link'] = array(
'#type' => 'link',
'#title' => $this->t('Continue'),
'#attributes' => array('class' => array('button', 'button--primary')),
'#url' => $url,
);
return $build;
}
/**
* Renders a list of available database updates.
*
* @return array
* A render array.
*/
protected function selection() {
// Make sure there is no stale theme registry.
$this->cache->deleteAll();
$count = 0;
$incompatible_count = 0;
$build['start'] = array(
'#tree' => TRUE,
'#type' => 'details',
);
// Ensure system.module's updates appear first.
$build['start']['system'] = array();
$updates = update_get_update_list();
$starting_updates = array();
$incompatible_updates_exist = FALSE;
foreach ($updates as $module => $update) {
if (!isset($update['start'])) {
$build['start'][$module] = array(
'#type' => 'item',
'#title' => $module . ' module',
'#markup' => $update['warning'],
'#prefix' => '<div class="messages messages--warning">',
'#suffix' => '</div>',
);
$incompatible_updates_exist = TRUE;
continue;
}
if (!empty($update['pending'])) {
$starting_updates[$module] = $update['start'];
$build['start'][$module] = array(
'#type' => 'hidden',
'#value' => $update['start'],
);
$build['start'][$module . '_updates'] = array(
'#theme' => 'item_list',
'#items' => $update['pending'],
'#title' => $module . ' module',
);
}
if (isset($update['pending'])) {
$count = $count + count($update['pending']);
}
}
// Find and label any incompatible updates.
foreach (update_resolve_dependencies($starting_updates) as $data) {
if (!$data['allowed']) {
$incompatible_updates_exist = TRUE;
$incompatible_count++;
$module_update_key = $data['module'] . '_updates';
if (isset($build['start'][$module_update_key]['#items'][$data['number']])) {
if ($data['missing_dependencies']) {
$text = $this->t('This update will been skipped due to the following missing dependencies:') . '<em>' . implode(', ', $data['missing_dependencies']) . '</em>';
}
else {
$text = $this->t("This update will be skipped due to an error in the module's code.");
}
$build['start'][$module_update_key]['#items'][$data['number']] .= '<div class="warning">' . $text . '</div>';
}
// Move the module containing this update to the top of the list.
$build['start'] = array($module_update_key => $build['start'][$module_update_key]) + $build['start'];
}
}
// Warn the user if any updates were incompatible.
if ($incompatible_updates_exist) {
drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning');
}
// If there are entity definition updates, display their summary.
if ($this->entityDefinitionUpdateManager->needsUpdates()) {
$entity_build = array();
$summary = $this->entityDefinitionUpdateManager->getChangeSummary();
foreach ($summary as $entity_type_id => $items) {
$entity_update_key = 'entity_type_updates_' . $entity_type_id;
$entity_build[$entity_update_key] = array(
'#theme' => 'item_list',
'#items' => $items,
'#title' => $entity_type_id . ' entity type',
);
$count++;
}
// Display these above the module updates, since they will be run first.
$build['start'] = $entity_build + $build['start'];
}
if (empty($count)) {
drupal_set_message($this->t('No pending updates.'));
unset($build);
$build['links'] = array(
'#theme' => 'links',
'#links' => $this->helpfulLinks(),
);
// No updates to run, so caches won't get flushed later. Clear them now.
drupal_flush_all_caches();
}
else {
$build['help'] = array(
'#markup' => '<p>' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '</p>',
'#weight' => -5,
);
if ($incompatible_count) {
$build['start']['#title'] = $this->formatPlural(
$count,
'1 pending update (@number_applied to be applied, @number_incompatible skipped)',
'@count pending updates (@number_applied to be applied, @number_incompatible skipped)',
array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count)
);
}
else {
$build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates');
}
$url = new Url('system.db_update', array('op' => 'run'));
$build['link'] = array(
'#type' => 'link',
'#title' => $this->t('Apply pending updates'),
'#attributes' => array('class' => array('button', 'button--primary')),
'#weight' => 5,
'#url' => $url,
);
}
return $build;
}
/**
* Displays results of the update script with any accompanying errors.
*
* @return array
* A render array.
*/
protected function results() {
// Report end result.
$dblog_exists = $this->moduleHandler->moduleExists('dblog');
if ($dblog_exists && $this->account->hasPermission('access site reports')) {
$log_message = $this->t('All errors have been <a href="@url">logged</a>.', array(
'@url' => $this->url('dblog.overview'),
));
}
else {
$log_message = $this->t('All errors have been logged.');
}
if (!empty($_SESSION['update_success'])) {
$message = '<p>' . $this->t('Updates were attempted. If you see no failures below, you may proceed happily back to your <a href="@url">site</a>. Otherwise, you may need to update your database manually.', array('@url' => $this->url('<front>'))) . ' ' . $log_message . '</p>';
}
else {
$last = reset($_SESSION['updates_remaining']);
list($module, $version) = array_pop($last);
$message = '<p class="error">' . $this->t('The update process was aborted prematurely while running <strong>update #@version in @module.module</strong>.', array(
'@version' => $version,
'@module' => $module,
)) . ' ' . $log_message;
if ($dblog_exists) {
$message .= ' ' . $this->t('You may need to check the <code>watchdog</code> database table manually.');
}
$message .= '</p>';
}
if (Settings::get('update_free_access')) {
$message .= '<p>' . $this->t("<strong>Reminder: don't forget to set the <code>\$settings['update_free_access']</code> value in your <code>settings.php</code> file back to <code>FALSE</code>.</strong>") . '</p>';
}
$build['message'] = array(
'#markup' => $message,
);
$build['links'] = array(
'#theme' => 'links',
'#links' => $this->helpfulLinks(),
);
// Output a list of info messages.
if (!empty($_SESSION['update_results'])) {
$all_messages = array();
foreach ($_SESSION['update_results'] as $module => $updates) {
if ($module != '#abort') {
$module_has_message = FALSE;
$info_messages = array();
foreach ($updates as $number => $queries) {
$messages = array();
foreach ($queries as $query) {
// If there is no message for this update, don't show anything.
if (empty($query['query'])) {
continue;
}
if ($query['success']) {
$messages[] = array(
'#wrapper_attributes' => array('class' => array('success')),
'#markup' => $query['query'],
);
}
else {
$messages[] = array(
'#wrapper_attributes' => array('class' => array('failure')),
'#markup' => '<strong>' . $this->t('Failed:') . '</strong> ' . $query['query'],
);
}
}
if ($messages) {
$module_has_message = TRUE;
$info_messages[] = array(
'#theme' => 'item_list',
'#items' => $messages,
'#title' => $this->t('Update #@count', array('@count' => $number)),
);
}
}
// If there were any messages then prefix them with the module name
// and add it to the global message list.
if ($module_has_message) {
$all_messages[] = array(
'#type' => 'container',
'#prefix' => '<h3>' . $this->t('@module module', array('@module' => $module)) . '</h3>',
'#children' => $info_messages,
);
}
}
}
if ($all_messages) {
$build['query_messages'] = array(
'#type' => 'container',
'#children' => $all_messages,
'#attributes' => array('class' => array('update-results')),
'#prefix' => '<h2>' . $this->t('The following updates returned messages:') . '</h2>',
);
}
}
unset($_SESSION['update_results']);
unset($_SESSION['update_success']);
unset($_SESSION['update_ignore_warnings']);
return $build;
}
/**
* Renders a list of requirement errors or warnings.
*
* @return array
* A render array.
*/
public function requirements($severity, array $requirements) {
$options = $severity == REQUIREMENT_WARNING ? array('continue' => 1) : array();
$try_again_url = $this->url('system.db_update', $options);
$build['status_report'] = array(
'#theme' => 'status_report',
'#requirements' => $requirements,
'#suffix' => $this->t('Check the messages and <a href="@url">try again</a>.', array('@url' => $try_again_url))
);
$build['#title'] = $this->t('Requirements problem');
return $build;
}
/**
* Provides the update task list render array.
*
* @param string $active
* The active task.
* Can be one of 'requirements', 'info', 'selection', 'run', 'results'.
*
* @return array
* A render array.
*/
protected function updateTasksList($active = NULL) {
// Default list of tasks.
$tasks = array(
'requirements' => $this->t('Verify requirements'),
'info' => $this->t('Overview'),
'selection' => $this->t('Review updates'),
'run' => $this->t('Run updates'),
'results' => $this->t('Review log'),
);
$task_list = array(
'#theme' => 'maintenance_task_list',
'#items' => $tasks,
'#active' => $active,
);
return $task_list;
}
/**
* Starts the database update batch process.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*/
protected function triggerBatch(Request $request) {
// During the update, bring the site offline so that schema changes do not
// affect visiting users.
$maintenance_mode = $this->config('system.maintenance')->get('enabled');
if (isset($maintenance_mode)) {
$_SESSION['maintenance_mode'] = $maintenance_mode;
}
if (empty($_SESSION['maintenance_mode'])) {
$this->state->set('system.maintenance_mode', TRUE);
}
$operations = array();
// First of all perform entity definition updates, which will update
// storage schema if needed, so that module update functions work with
// the correct entity schema.
if ($this->entityDefinitionUpdateManager->needsUpdates()) {
$operations[] = array('update_entity_definitions', array('system', '0 - Update entity definitions'));
}
// Resolve any update dependencies to determine the actual updates that will
// be run and the order they will be run in.
$start = $this->getModuleUpdates();
$updates = update_resolve_dependencies($start);
// Store the dependencies for each update function in an array which the
// batch API can pass in to the batch operation each time it is called. (We
// do not store the entire update dependency array here because it is
// potentially very large.)
$dependency_map = array();
foreach ($updates as $function => $update) {
$dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array();
}
// Determine updates to be performed.
foreach ($updates as $update) {
if ($update['allowed']) {
// Set the installed version of each module so updates will start at the
// correct place. (The updates are already sorted, so we can simply base
// this on the first one we come across in the above foreach loop.)
if (isset($start[$update['module']])) {
drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
unset($start[$update['module']]);
}
// Add this update function to the batch.
$function = $update['module'] . '_update_' . $update['number'];
$operations[] = array('update_do_one', array($update['module'], $update['number'], $dependency_map[$function]));
}
}
$batch['operations'] = $operations;
$batch += array(
'title' => $this->t('Updating'),
'init_message' => $this->t('Starting updates'),
'error_message' => $this->t('An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.'),
'finished' => array('\Drupal\system\Controller\DbUpdateController', 'batchFinished'),
);
batch_set($batch);
return batch_process('update.php/results', Url::fromRoute('system.db_update', array('op' => 'start')));
}
/**
* Finishes the update process and stores the results for eventual display.
*
* After the updates run, all caches are flushed. The update results are
* stored into the session (for example, to be displayed on the update results
* page in update.php). Additionally, if the site was off-line, now that the
* update process is completed, the site is set back online.
*
* @param $success
* Indicate that the batch API tasks were all completed successfully.
* @param array $results
* An array of all the results that were updated in update_do_one().
* @param array $operations
* A list of all the operations that had not been completed by the batch API.
*/
public static function batchFinished($success, $results, $operations) {
// No updates to run, so caches won't get flushed later. Clear them now.
drupal_flush_all_caches();
$_SESSION['update_results'] = $results;
$_SESSION['update_success'] = $success;
$_SESSION['updates_remaining'] = $operations;
// Now that the update is done, we can put the site back online if it was
// previously in maintenance mode.
if (isset($_SESSION['maintenance_mode'])) {
\Drupal::state()->set('system.maintenance_mode', FALSE);
unset($_SESSION['maintenance_mode']);
}
}
/**
* Provides links to the homepage and administration pages.
*
* @return array
* An array of links.
*/
protected function helpfulLinks() {
$links['front'] = array(
'title' => $this->t('Front page'),
'url' => Url::fromRoute('<front>'),
);
if ($this->account->hasPermission('access administration pages')) {
$links['admin-pages'] = array(
'title' => $this->t('Administration pages'),
'url' => Url::fromRoute('system.admin'),
);
}
return $links;
}
/**
* Retrieves module updates.
*
* @return array
* The module updates that can be performed.
*/
protected function getModuleUpdates() {
$return = array();
$updates = update_get_update_list();
foreach ($updates as $module => $update) {
$return[$module] = $update['start'];
}
return $return;
}
}

View file

@ -0,0 +1,114 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\EntityAutocompleteController.
*/
namespace Drupal\system\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Tags;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityAutocompleteMatcher;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Defines a route controller for entity autocomplete form elements.
*/
class EntityAutocompleteController extends ControllerBase {
/**
* The autocomplete matcher for entity references.
*
* @var \Drupal\Core\Entity\EntityAutocompleteMatcher
*/
protected $matcher;
/**
* The key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValue;
/**
* Constructs a EntityAutocompleteController object.
*
* @param \Drupal\Core\Entity\EntityAutocompleteMatcher $matcher
* The autocomplete matcher for entity references.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value
* The key value factory.
*/
public function __construct(EntityAutocompleteMatcher $matcher, KeyValueStoreInterface $key_value) {
$this->matcher = $matcher;
$this->keyValue = $key_value;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.autocomplete_matcher'),
$container->get('keyvalue')->get('entity_autocomplete')
);
}
/**
* Autocomplete the label of an entity.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object that contains the typed tags.
* @param string $target_type
* The ID of the target entity type.
* @param string $selection_handler
* The plugin ID of the entity reference selection handler.
* @param string $selection_settings_key
* The hashed key of the key/value entry that holds the selection handler
* settings.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The matched entity labels as a JSON response.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Thrown if the selection settings key is not found in the key/value store
* or if it does not match the stored data.
*/
public function handleAutocomplete(Request $request, $target_type, $selection_handler, $selection_settings_key) {
$matches = array();
// Get the typed string from the URL, if it exists.
if ($input = $request->query->get('q')) {
$typed_string = Tags::explode($input);
$typed_string = Unicode::strtolower(array_pop($typed_string));
// Selection settings are passed in as a hashed key of a serialized array
// stored in the key/value store.
$selection_settings = $this->keyValue->get($selection_settings_key, FALSE);
if ($selection_settings !== FALSE) {
$selection_settings_hash = Crypt::hmacBase64(serialize($selection_settings) . $target_type . $selection_handler, Settings::getHashSalt());
if ($selection_settings_hash !== $selection_settings_key) {
// Disallow access when the selection settings hash does not match the
// passed-in key.
throw new AccessDeniedHttpException('Invalid selection settings key.');
}
}
else {
// Disallow access when the selection settings key is not found in the
// key/value store.
throw new AccessDeniedHttpException();
}
$matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string);
}
return new JsonResponse($matches);
}
}

View file

@ -0,0 +1,186 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\FormAjaxController.
*/
namespace Drupal\system\Controller;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormAjaxResponseBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Render\MainContent\MainContentRendererInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\system\FileAjaxForm;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Defines a controller to respond to form Ajax requests.
*/
class FormAjaxController implements ContainerInjectionInterface {
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface|\Drupal\Core\Form\FormCacheInterface
*/
protected $formBuilder;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The main content to AJAX Response renderer.
*
* @var \Drupal\Core\Render\MainContent\MainContentRendererInterface
*/
protected $ajaxRenderer;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The form AJAX response builder.
*
* @var \Drupal\Core\Form\FormAjaxResponseBuilderInterface
*/
protected $formAjaxResponseBuilder;
/**
* Constructs a FormAjaxController object.
*
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
* The main content to AJAX Response renderer.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Form\FormAjaxResponseBuilderInterface $form_ajax_response_builder
* The form AJAX response builder.
*/
public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match, FormAjaxResponseBuilderInterface $form_ajax_response_builder) {
$this->logger = $logger;
$this->formBuilder = $form_builder;
$this->renderer = $renderer;
$this->ajaxRenderer = $ajax_renderer;
$this->routeMatch = $route_match;
$this->formAjaxResponseBuilder = $form_ajax_response_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('logger.factory')->get('ajax'),
$container->get('form_builder'),
$container->get('renderer'),
$container->get('main_content_renderer.ajax'),
$container->get('current_route_match'),
$container->get('form_ajax_response_builder')
);
}
/**
* Processes an Ajax form submission.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return mixed
* Whatever is returned by the triggering element's #ajax['callback']
* function. One of:
* - A render array containing the new or updated content to return to the
* browser. This is commonly an element within the rebuilt form.
* - A \Drupal\Core\Ajax\AjaxResponse object containing commands for the
* browser to process.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
*/
public function content(Request $request) {
$ajax_form = $this->getForm($request);
$form = $ajax_form->getForm();
$form_state = $ajax_form->getFormState();
$commands = $ajax_form->getCommands();
$this->formBuilder->processForm($form['#form_id'], $form, $form_state);
return $this->formAjaxResponseBuilder->buildResponse($request, $form, $form_state, $commands);
}
/**
* Gets a form submitted via #ajax during an Ajax callback.
*
* This will load a form from the form cache used during Ajax operations. It
* pulls the form info from the request body.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return \Drupal\system\FileAjaxForm
* A wrapper object containing the $form, $form_state, $form_id,
* $form_build_id and an initial list of Ajax $commands.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
*/
protected function getForm(Request $request) {
$form_state = new FormState();
$form_build_id = $request->request->get('form_build_id');
// Get the form from the cache.
$form = $this->formBuilder->getCache($form_build_id, $form_state);
if (!$form) {
// If $form cannot be loaded from the cache, the form_build_id must be
// invalid, which means that someone performed a POST request onto
// system/ajax without actually viewing the concerned form in the browser.
// This is likely a hacking attempt as it never happens under normal
// circumstances.
$this->logger->warning('Invalid form POST data.');
throw new BadRequestHttpException();
}
// Since some of the submit handlers are run, redirects need to be disabled.
$form_state->disableRedirect();
// When a form is rebuilt after Ajax processing, its #build_id and #action
// should not change.
// @see \Drupal\Core\Form\FormBuilderInterface::rebuildForm()
$form_state->addRebuildInfo('copy', [
'#build_id' => TRUE,
'#action' => TRUE,
]);
// The form needs to be processed; prepare for that by setting a few
// internal variables.
$form_state->setUserInput($request->request->all());
$form_id = $form['#form_id'];
return new FileAjaxForm($form, $form_state, $form_id, $form['#build_id'], []);
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\Http4xxController.
*/
namespace Drupal\system\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller for default HTTP 4xx responses.
*/
class Http4xxController extends ControllerBase {
/**
* The default 401 content.
*
* @return array
* A render array containing the message to display for 401 pages.
*/
public function on401() {
return [
'#markup' => $this->t('Please log in to access this page.'),
];
}
/**
* The default 403 content.
*
* @return array
* A render array containing the message to display for 404 pages.
*/
public function on403() {
return [
'#markup' => $this->t('You are not authorized to access this page.'),
];
}
/**
* The default 404 content.
*
* @return array
* A render array containing the message to display for 404 pages.
*/
public function on404() {
return [
'#markup' => $this->t('The requested page could not be found.'),
];
}
}

View file

@ -0,0 +1,344 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\SystemController.
*/
namespace Drupal\system\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Theme\ThemeAccessCheck;
use Drupal\Core\Url;
use Drupal\system\SystemManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for System routes.
*/
class SystemController extends ControllerBase {
/**
* The entity query factory object.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $queryFactory;
/**
* System Manager Service.
*
* @var \Drupal\system\SystemManager
*/
protected $systemManager;
/**
* The theme access checker service.
*
* @var \Drupal\Core\Theme\ThemeAccessCheck
*/
protected $themeAccess;
/**
* The form builder service.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The theme handler service.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuLinkTree;
/**
* Constructs a new SystemController.
*
* @param \Drupal\system\SystemManager $systemManager
* System manager service.
* @param \Drupal\Core\Entity\Query\QueryFactory $queryFactory
* The entity query object.
* @param \Drupal\Core\Theme\ThemeAccessCheck $theme_access
* The theme access checker service.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface
* The menu link tree service.
*/
public function __construct(SystemManager $systemManager, QueryFactory $queryFactory, ThemeAccessCheck $theme_access, FormBuilderInterface $form_builder, ThemeHandlerInterface $theme_handler, MenuLinkTreeInterface $menu_link_tree) {
$this->systemManager = $systemManager;
$this->queryFactory = $queryFactory;
$this->themeAccess = $theme_access;
$this->formBuilder = $form_builder;
$this->themeHandler = $theme_handler;
$this->menuLinkTree = $menu_link_tree;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('system.manager'),
$container->get('entity.query'),
$container->get('access_check.theme'),
$container->get('form_builder'),
$container->get('theme_handler'),
$container->get('menu.link_tree')
);
}
/**
* Provide the administration overview page.
*
* @param string $link_id
* The ID of the administrative path link for which to display child links.
*
* @return array
* A renderable array of the administration overview page.
*/
public function overview($link_id) {
// Check for status report errors.
if ($this->systemManager->checkRequirements() && $this->currentUser()->hasPermission('administer site configuration')) {
drupal_set_message($this->t('One or more problems were detected with your Drupal installation. Check the <a href="@status">status report</a> for more information.', array('@status' => $this->url('system.status'))), 'error');
}
// Load all menu links below it.
$parameters = new MenuTreeParameters();
$parameters->setRoot($link_id)->excludeRoot()->setTopLevelOnly()->onlyEnabledLinks();
$tree = $this->menuLinkTree->load(NULL, $parameters);
$manipulators = array(
array('callable' => 'menu.default_tree_manipulators:checkAccess'),
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
);
$tree = $this->menuLinkTree->transform($tree, $manipulators);
$tree_access_cacheability = new CacheableMetadata();
$blocks = array();
foreach ($tree as $key => $element) {
$tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
// Only render accessible links.
if (!$element->access->isAllowed()) {
continue;
}
$link = $element->link;
$block['title'] = $link->getTitle();
$block['description'] = $link->getDescription();
$block['content'] = array(
'#theme' => 'admin_block_content',
'#content' => $this->systemManager->getAdminBlock($link),
);
if (!empty($block['content']['#content'])) {
$blocks[$key] = $block;
}
}
if ($blocks) {
ksort($blocks);
$build = [
'#theme' => 'admin_page',
'#blocks' => $blocks,
];
$tree_access_cacheability->applyTo($build);
return $build;
}
else {
$build = [
'#markup' => $this->t('You do not have any administrative items.'),
];
$tree_access_cacheability->applyTo($build);
return $build;
}
}
/**
* Sets whether the admin menu is in compact mode or not.
*
* @param string $mode
* Valid values are 'on' and 'off'.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function compactPage($mode) {
user_cookie_save(array('admin_compact_mode' => ($mode == 'on')));
return $this->redirect('<front>');
}
/**
* Provides a single block from the administration menu as a page.
*/
public function systemAdminMenuBlockPage() {
return $this->systemManager->getBlockContents();
}
/**
* Returns a theme listing.
*
* @return string
* An HTML string of the theme listing page.
*
* @todo Move into ThemeController.
*/
public function themesPage() {
$config = $this->config('system.theme');
// Get all available themes.
$themes = $this->themeHandler->rebuildThemeData();
uasort($themes, 'system_sort_modules_by_info_name');
$theme_default = $config->get('default');
$theme_groups = array('installed' => array(), 'uninstalled' => array());
$admin_theme = $config->get('admin');
$admin_theme_options = array();
foreach ($themes as &$theme) {
if (!empty($theme->info['hidden'])) {
continue;
}
$theme->is_default = ($theme->getName() == $theme_default);
$theme->is_admin = ($theme->getName() == $admin_theme || ($theme->is_default && $admin_theme == '0'));
// Identify theme screenshot.
$theme->screenshot = NULL;
// Create a list which includes the current theme and all its base themes.
if (isset($themes[$theme->getName()]->base_themes)) {
$theme_keys = array_keys($themes[$theme->getName()]->base_themes);
$theme_keys[] = $theme->getName();
}
else {
$theme_keys = array($theme->getName());
}
// Look for a screenshot in the current theme or in its closest ancestor.
foreach (array_reverse($theme_keys) as $theme_key) {
if (isset($themes[$theme_key]) && file_exists($themes[$theme_key]->info['screenshot'])) {
$theme->screenshot = array(
'uri' => $themes[$theme_key]->info['screenshot'],
'alt' => $this->t('Screenshot for !theme theme', array('!theme' => $theme->info['name'])),
'title' => $this->t('Screenshot for !theme theme', array('!theme' => $theme->info['name'])),
'attributes' => array('class' => array('screenshot')),
);
break;
}
}
if (empty($theme->status)) {
// Ensure this theme is compatible with this version of core.
// Require the 'content' region to make sure the main page
// content has a common place in all themes.
$theme->incompatible_core = !isset($theme->info['core']) || ($theme->info['core'] != \DRUPAL::CORE_COMPATIBILITY) || !isset($theme->info['regions']['content']);
$theme->incompatible_php = version_compare(phpversion(), $theme->info['php']) < 0;
// Confirmed that the base theme is available.
$theme->incompatible_base = isset($theme->info['base theme']) && !isset($themes[$theme->info['base theme']]);
// Confirm that the theme engine is available.
$theme->incompatible_engine = isset($theme->info['engine']) && !isset($theme->owner);
}
$theme->operations = array();
if (!empty($theme->status) || !$theme->incompatible_core && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine) {
// Create the operations links.
$query['theme'] = $theme->getName();
if ($this->themeAccess->checkAccess($theme->getName())) {
$theme->operations[] = array(
'title' => $this->t('Settings'),
'url' => Url::fromRoute('system.theme_settings_theme', ['theme' => $theme->getName()]),
'attributes' => array('title' => $this->t('Settings for !theme theme', array('!theme' => $theme->info['name']))),
);
}
if (!empty($theme->status)) {
if (!$theme->is_default) {
$theme_uninstallable = TRUE;
if ($theme->getName() == $admin_theme) {
$theme_uninstallable = FALSE;
}
// Check it isn't the base of theme of an installed theme.
foreach ($theme->required_by as $themename => $dependency) {
if (!empty($themes[$themename]->status)) {
$theme_uninstallable = FALSE;
}
}
if ($theme_uninstallable) {
$theme->operations[] = array(
'title' => $this->t('Uninstall'),
'url' => Url::fromRoute('system.theme_uninstall'),
'query' => $query,
'attributes' => array('title' => $this->t('Uninstall !theme theme', array('!theme' => $theme->info['name']))),
);
}
$theme->operations[] = array(
'title' => $this->t('Set as default'),
'url' => Url::fromRoute('system.theme_set_default'),
'query' => $query,
'attributes' => array('title' => $this->t('Set !theme as default theme', array('!theme' => $theme->info['name']))),
);
}
$admin_theme_options[$theme->getName()] = $theme->info['name'];
}
else {
$theme->operations[] = array(
'title' => $this->t('Install'),
'url' => Url::fromRoute('system.theme_install'),
'query' => $query,
'attributes' => array('title' => $this->t('Install !theme theme', array('!theme' => $theme->info['name']))),
);
$theme->operations[] = array(
'title' => $this->t('Install and set as default'),
'url' => Url::fromRoute('system.theme_set_default'),
'query' => $query,
'attributes' => array('title' => $this->t('Install !theme as default theme', array('!theme' => $theme->info['name']))),
);
}
}
// Add notes to default and administration theme.
$theme->notes = array();
if ($theme->is_default) {
$theme->notes[] = $this->t('default theme');
}
if ($theme->is_admin) {
$theme->notes[] = $this->t('admin theme');
}
// Sort installed and uninstalled themes into their own groups.
$theme_groups[$theme->status ? 'installed' : 'uninstalled'][] = $theme;
}
// There are two possible theme groups.
$theme_group_titles = array(
'installed' => $this->formatPlural(count($theme_groups['installed']), 'Installed theme', 'Installed themes'),
);
if (!empty($theme_groups['uninstalled'])) {
$theme_group_titles['uninstalled'] = $this->formatPlural(count($theme_groups['uninstalled']), 'Uninstalled theme', 'Uninstalled themes');
}
uasort($theme_groups['installed'], 'system_sort_themes');
$this->moduleHandler()->alter('system_themes_page', $theme_groups);
$build = array();
$build[] = array(
'#theme' => 'system_themes_page',
'#theme_groups' => $theme_groups,
'#theme_group_titles' => $theme_group_titles,
);
$build[] = $this->formBuilder->getForm('Drupal\system\Form\ThemeAdminForm', $admin_theme_options);
return $build;
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\SystemInfoController.
*/
namespace Drupal\system\Controller;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\system\SystemManager;
/**
* Returns responses for System Info routes.
*/
class SystemInfoController implements ContainerInjectionInterface {
/**
* System Manager Service.
*
* @var \Drupal\system\SystemManager
*/
protected $systemManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('system.manager')
);
}
/**
* Constructs a SystemInfoController object.
*
* @param \Drupal\system\SystemManager $systemManager
* System manager service.
*/
public function __construct(SystemManager $systemManager) {
$this->systemManager = $systemManager;
}
/**
* Displays the site status report.
*
* @return string
* The current status of the Drupal installation.
*/
public function status() {
$requirements = $this->systemManager->listRequirements();
return array('#theme' => 'status_report', '#requirements' => $requirements);
}
/**
* Returns the contents of phpinfo().
*
* @return \Symfony\Component\HttpFoundation\Response
* A response object to be sent to the client.
*/
public function php() {
if (function_exists('phpinfo')) {
ob_start();
phpinfo();
$output = ob_get_clean();
}
else {
$output = t('The phpinfo() function has been disabled for security reasons. For more information, visit <a href="@phpinfo">Enabling and disabling phpinfo()</a> handbook page.', array('@phpinfo' => 'https://www.drupal.org/node/243993'));
}
return new Response($output);
}
}

View file

@ -0,0 +1,212 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\ThemeController.
*/
namespace Drupal\system\Controller;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Controller for theme handling.
*/
class ThemeController extends ControllerBase {
/**
* The theme handler service.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The route builder service.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routeBuilder;
/**
* Constructs a new ThemeController.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* The route builder.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(ThemeHandlerInterface $theme_handler, RouteBuilderInterface $route_builder, ConfigFactoryInterface $config_factory) {
$this->themeHandler = $theme_handler;
$this->configFactory = $config_factory;
$this->routeBuilder = $route_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('theme_handler'),
$container->get('router.builder'),
$container->get('config.factory')
);
}
/**
* Uninstalls a theme.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object containing a theme name and a valid token.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirects back to the appearance admin page.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Throws access denied when no theme or token is set in the request or when
* the token is invalid.
*/
public function uninstall(Request $request) {
$theme = $request->get('theme');
$config = $this->config('system.theme');
if (isset($theme)) {
// Get current list of themes.
$themes = $this->themeHandler->listInfo();
// Check if the specified theme is one recognized by the system.
if (!empty($themes[$theme])) {
// Do not uninstall the default or admin theme.
if ($theme === $config->get('default') || $theme === $config->get('admin')) {
drupal_set_message($this->t('%theme is the default theme and cannot be uninstalled.', array('%theme' => $themes[$theme]->info['name'])), 'error');
}
else {
$this->themeHandler->uninstall(array($theme));
drupal_set_message($this->t('The %theme theme has been uninstalled.', array('%theme' => $themes[$theme]->info['name'])));
}
}
else {
drupal_set_message($this->t('The %theme theme was not found.', array('%theme' => $theme)), 'error');
}
return $this->redirect('system.themes_page');
}
throw new AccessDeniedHttpException();
}
/**
* Installs a theme.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object containing a theme name and a valid token.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirects back to the appearance admin page.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Throws access denied when no theme or token is set in the request or when
* the token is invalid.
*/
public function install(Request $request) {
$theme = $request->get('theme');
if (isset($theme)) {
try {
if ($this->themeHandler->install(array($theme))) {
$themes = $this->themeHandler->listInfo();
drupal_set_message($this->t('The %theme theme has been installed.', array('%theme' => $themes[$theme]->info['name'])));
}
else {
drupal_set_message($this->t('The %theme theme was not found.', array('%theme' => $theme)), 'error');
}
}
catch (PreExistingConfigException $e) {
$config_objects = $e->flattenConfigObjects($e->getConfigObjects());
drupal_set_message(
$this->formatPlural(
count($config_objects),
'Unable to install @extension, %config_names already exists in active configuration.',
'Unable to install @extension, %config_names already exist in active configuration.',
array(
'%config_names' => implode(', ', $config_objects),
'@extension' => $theme,
)),
'error'
);
}
catch (UnmetDependenciesException $e) {
drupal_set_message($e->getTranslatedMessage($this->getStringTranslation(), $theme), 'error');
}
return $this->redirect('system.themes_page');
}
throw new AccessDeniedHttpException();
}
/**
* Set the default theme.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object containing a theme name.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirects back to the appearance admin page.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Throws access denied when no theme is set in the request.
*/
public function setDefaultTheme(Request $request) {
$config = $this->configFactory->getEditable('system.theme');
$theme = $request->query->get('theme');
if (isset($theme)) {
// Get current list of themes.
$themes = $this->themeHandler->listInfo();
// Check if the specified theme is one recognized by the system.
// Or try to install the theme.
if (isset($themes[$theme]) || $this->themeHandler->install(array($theme))) {
$themes = $this->themeHandler->listInfo();
// Set the default theme.
$config->set('default', $theme)->save();
$this->routeBuilder->setRebuildNeeded();
// The status message depends on whether an admin theme is currently in
// use: a value of 0 means the admin theme is set to be the default
// theme.
$admin_theme = $config->get('admin');
if ($admin_theme != 0 && $admin_theme != $theme) {
drupal_set_message($this->t('Please note that the administration theme is still set to the %admin_theme theme; consequently, the theme on this page remains unchanged. All non-administrative sections of the site, however, will show the selected %selected_theme theme by default.', array(
'%admin_theme' => $themes[$admin_theme]->info['name'],
'%selected_theme' => $themes[$theme]->info['name'],
)));
}
else {
drupal_set_message($this->t('%theme is now the default theme.', array('%theme' => $themes[$theme]->info['name'])));
}
}
else {
drupal_set_message($this->t('The %theme theme was not found.', array('%theme' => $theme)), 'error');
}
return $this->redirect('system.themes_page');
}
throw new AccessDeniedHttpException();
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\system\Controller\TimezoneController.
*/
namespace Drupal\system\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Provides a callback for finding out a timezone name.
*/
class TimezoneController {
/**
* Retrieve a JSON object containing a time zone name given a timezone
* abbreviation.
*
* @param string $abbreviation
* Time zone abbreviation.
* @param int $offset
* Offset from GMT in seconds. Defaults to -1 which means that first found
* time zone corresponding to abbr is returned. Otherwise exact offset is
* searched and only if not found then the first time zone with any offset
* is returned.
* @param null|bool $is_daylight_saving_time
* Daylight saving time indicator. If abbr does not exist then the time
* zone is searched solely by offset and isdst.
*
* @return JsonResponse
* The timezone name in JsonResponse object.
*/
public function getTimezone($abbreviation = '', $offset = -1, $is_daylight_saving_time = NULL) {
// An abbreviation of "0" passed in the callback arguments should be
// interpreted as the empty string.
$abbreviation = $abbreviation ? $abbreviation : '';
$timezone = timezone_name_from_abbr($abbreviation, intval($offset), $is_daylight_saving_time);
return new JsonResponse($timezone);
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains \Drupal\system\CronController.
*/
namespace Drupal\system;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\CronInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Controller for Cron handling.
*/
class CronController extends ControllerBase {
/**
* The cron service.
*
* @var \Drupal\Core\CronInterface
*/
protected $cron;
/**
* Constructs a CronController object.
*
* @param \Drupal\Core\CronInterface $cron
* The cron service.
*/
public function __construct(CronInterface $cron) {
$this->cron = $cron;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('cron'));
}
/**
* Run Cron once.
*
* @return \Symfony\Component\HttpFoundation\Response
* A Symfony response object.
*/
public function run() {
$this->cron->run();
// HTTP 204 is "No content", meaning "I did what you asked and we're done."
return new Response('', 204);
}
/**
* Run cron manually.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A Symfony direct response object.
*/
public function runManually() {
if ($this->cron->run()) {
drupal_set_message($this->t('Cron ran successfully.'));
}
else {
drupal_set_message($this->t('Cron run failed.'), 'error');
}
return $this->redirect('system.status');
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\system\DateFormatAccessControlHandler.
*/
namespace Drupal\system;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the date format entity type.
*
* @see \Drupal\system\Entity\DateFormat
*/
class DateFormatAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
// There are no restrictions on viewing a date format.
if ($operation == 'view') {
return AccessResult::allowed();
}
// Locked date formats cannot be updated or deleted.
elseif (in_array($operation, array('update', 'delete'))) {
if ($entity->isLocked()) {
return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
}
else {
return parent::checkAccess($entity, $operation, $langcode, $account)->cacheUntilEntityChanges($entity);
}
}
return parent::checkAccess($entity, $operation, $langcode, $account);
}
}

View file

@ -0,0 +1,83 @@
<?php
/**
* @file
* Contains \Drupal\system\DateFormatListBuilder.
*/
namespace Drupal\system;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of date format entities.
*
* @see \Drupal\system\Entity\DateFormat
*/
class DateFormatListBuilder extends ConfigEntityListBuilder {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* Constructs a new DateFormatListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatter $date_formatter) {
parent::__construct($entity_type, $storage);
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager')->getStorage($entity_type->id()),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['id'] = t('Machine name');
$header['label'] = t('Name');
$header['pattern'] = t('Pattern');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
if ($entity->isLocked()) {
$row['id'] = $this->t('@entity_id (locked)', array('@entity_id' => $entity->id()));
}
else {
$row['id'] = $entity->id();
}
$row['label'] = $this->getLabel($entity);
$row['pattern'] = $this->dateFormatter->format(REQUEST_TIME, $entity->id());
return $row + parent::buildRow($entity);
}
}

View file

@ -0,0 +1,158 @@
<?php
/**
* @file
* Contains \Drupal\system\Entity\Action.
*/
namespace Drupal\system\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\system\ActionConfigEntityInterface;
use Drupal\Core\Action\ActionPluginCollection;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
/**
* Defines the configured action entity.
*
* @ConfigEntityType(
* id = "action",
* label = @Translation("Action"),
* admin_permission = "administer actions",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "type",
* "plugin",
* "configuration",
* }
* )
*/
class Action extends ConfigEntityBase implements ActionConfigEntityInterface, EntityWithPluginCollectionInterface {
/**
* The name (plugin ID) of the action.
*
* @var string
*/
protected $id;
/**
* The label of the action.
*
* @var string
*/
protected $label;
/**
* The action type.
*
* @var string
*/
protected $type;
/**
* The configuration of the action.
*
* @var array
*/
protected $configuration = array();
/**
* The plugin ID of the action.
*
* @var string
*/
protected $plugin;
/**
* The plugin collection that stores action plugins.
*
* @var \Drupal\Core\Action\ActionPluginCollection
*/
protected $pluginCollection;
/**
* Encapsulates the creation of the action's LazyPluginCollection.
*
* @return \Drupal\Component\Plugin\LazyPluginCollection
* The action's plugin collection.
*/
protected function getPluginCollection() {
if (!$this->pluginCollection) {
$this->pluginCollection = new ActionPluginCollection(\Drupal::service('plugin.manager.action'), $this->plugin, $this->configuration);
}
return $this->pluginCollection;
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
return array('configuration' => $this->getPluginCollection());
}
/**
* {@inheritdoc}
*/
public function getPlugin() {
return $this->getPluginCollection()->get($this->plugin);
}
/**
* {@inheritdoc}
*/
public function setPlugin($plugin_id) {
$this->plugin = $plugin_id;
$this->getPluginCollection()->addInstanceId($plugin_id);
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition() {
return $this->getPlugin()->getPluginDefinition();
}
/**
* {@inheritdoc}
*/
public function execute(array $entities) {
return $this->getPlugin()->executeMultiple($entities);
}
/**
* {@inheritdoc}
*/
public function isConfigurable() {
return $this->getPlugin() instanceof ConfigurablePluginInterface;
}
/**
* {@inheritdoc}
*/
public function getType() {
return $this->type;
}
/**
* {@inheritdoc}
*/
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
/** @var \Drupal\system\ActionConfigEntityInterface $a */
/** @var \Drupal\system\ActionConfigEntityInterface $b */
$a_type = $a->getType();
$b_type = $b->getType();
if ($a_type != $b_type) {
return strnatcasecmp($a_type, $b_type);
}
return parent::sort($a, $b);
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\system\Entity\Menu.
*/
namespace Drupal\system\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\system\MenuInterface;
/**
* Defines the Menu configuration entity class.
*
* @ConfigEntityType(
* id = "menu",
* label = @Translation("Menu"),
* handlers = {
* "access" = "Drupal\system\MenuAccessControlHandler"
* },
* admin_permission = "administer menu",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "description",
* "locked",
* }
* )
*/
class Menu extends ConfigEntityBase implements MenuInterface {
/**
* The menu machine name.
*
* @var string
*/
protected $id;
/**
* The human-readable name of the menu entity.
*
* @var string
*/
protected $label;
/**
* The menu description.
*
* @var string
*/
protected $description;
/**
* The locked status of this menu.
*
* @var bool
*/
protected $locked = FALSE;
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function isLocked() {
return (bool) $this->locked;
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\system\EventSubscriber\AdminRouteSubscriber.
*/
namespace Drupal\system\EventSubscriber;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\RouteCollection;
/**
* Adds the _admin_route option to each admin route.
*/
class AdminRouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($collection->all() as $route) {
if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route')) {
$route->setOption('_admin_route', TRUE);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = parent::getSubscribedEvents();
// Use a higher priority than \Drupal\field_ui\Routing\RouteSubscriber or
// \Drupal\views\EventSubscriber\RouteSubscriber to ensure we add the
// option to their routes.
// @todo https://www.drupal.org/node/2158571
$events[RoutingEvents::ALTER] = array('onAlterRoutes', -200);
return $events;
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* @file
* Contains \Drupal\system\EventSubscriber\AutomaticCron.
*/
namespace Drupal\system\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\CronInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* A subscriber running cron when a request terminates.
*/
class AutomaticCron implements EventSubscriberInterface {
/**
* The cron service.
*
* @var \Drupal\Core\CronInterface
*/
protected $cron;
/**
* The cron configuration.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The state key value store.
*
* Drupal\Core\State\StateInterface;
*/
protected $state;
/**
* Construct a new automatic cron runner.
*
* @param \Drupal\Core\CronInterface $cron
* The cron service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\State\StateInterface $state
* The state key value store.
*/
public function __construct(CronInterface $cron, ConfigFactoryInterface $config_factory, StateInterface $state) {
$this->cron = $cron;
$this->config = $config_factory->get('system.cron');
$this->state = $state;
}
/**
* Run the automated cron if enabled.
*
* @param Symfony\Component\HttpKernel\Event\PostResponseEvent $event
* The Event to process.
*/
public function onTerminate(PostResponseEvent $event) {
// If the site is not fully installed, suppress the automated cron run.
// Otherwise it could be triggered prematurely by Ajax requests during
// installation.
if ($this->state->get('install_task') == 'done') {
$threshold = $this->config->get('threshold.autorun');
if ($threshold > 0) {
$cron_next = $this->state->get('system.cron_last', 0) + $threshold;
if (REQUEST_TIME > $cron_next) {
$this->cron->run();
}
}
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
public static function getSubscribedEvents() {
$events[KernelEvents::TERMINATE][] = array('onTerminate', 100);
return $events;
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* @file
* Contains \Drupal\system\EventSubscriber\ConfigCacheTag.
*/
namespace Drupal\system\EventSubscriber;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A subscriber invalidating cache tags when system config objects are saved.
*/
class ConfigCacheTag implements EventSubscriberInterface {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The cache tags invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagsInvalidator;
/**
* Constructs a ConfigCacheTag object.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* The cache tags invalidator.
*/
public function __construct(ThemeHandlerInterface $theme_handler, CacheTagsInvalidatorInterface $cache_tags_invalidator) {
$this->themeHandler = $theme_handler;
$this->cacheTagsInvalidator = $cache_tags_invalidator;
}
/**
* Invalidate cache tags when particular system config objects are saved.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The Event to process.
*/
public function onSave(ConfigCrudEvent $event) {
// Changing the site settings may mean a different route is selected for the
// front page. Additionally a change to the site name or similar must
// invalidate the render cache since this could be used anywhere.
if ($event->getConfig()->getName() === 'system.site') {
$this->cacheTagsInvalidator->invalidateTags(['route_match', 'rendered']);
}
// Global theme settings.
if ($event->getConfig()->getName() === 'system.theme.global') {
$this->cacheTagsInvalidator->invalidateTags(['rendered']);
}
// Theme-specific settings, check if this matches a theme settings
// configuration object, in that case, clear the rendered cache tag.
foreach (array_keys($this->themeHandler->listInfo()) as $theme_name) {
if ($theme_name == $event->getConfig()->getName()) {
$this->cacheTagsInvalidator->invalidateTags(['rendered']);
break;
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = ['onSave'];
return $events;
}
}

View file

@ -0,0 +1,122 @@
<?php
/**
* @file
* Contains \Drupal\system\FileAjaxForm.
*/
namespace Drupal\system;
use Drupal\Core\Form\FormStateInterface;
/**
* Wrapper for Ajax forms data and commands, avoiding a multi-return-value tuple.
*
* @ingroup ajax
*/
class FileAjaxForm {
/**
* The form to cache.
*
* @var array
*/
protected $form;
/**
* The form state.
*
* @var \Drupal\Core\Form\FormStateInterface
*/
protected $formState;
/**
* The unique form ID.
*
* @var string
*/
protected $formId;
/**
* The unique form build ID.
*
* @var string
*/
protected $formBuildId;
/**
* The array of ajax commands.
*
* @var \Drupal\Core\Ajax\CommandInterface[]
*/
protected $commands;
/**
* Constructs a FileAjaxForm object.
*
* @param array $form
* The form definition.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Ajax\string|string $form_id
* The unique form ID.
* @param \Drupal\Core\Ajax\string|string $form_build_id
* The unique form build ID.
* @param \Drupal\Core\Ajax\CommandInterface[] $commands
* The ajax commands.
*/
public function __construct(array $form, FormStateInterface $form_state, $form_id, $form_build_id, array $commands) {
$this->form = $form;
$this->formState = $form_state;
$this->formId = $form_id;
$this->formBuildId = $form_build_id;
$this->commands = $commands;
}
/**
* Gets all AJAX commands.
*
* @return \Drupal\Core\Ajax\CommandInterface[]
* Returns all previously added AJAX commands.
*/
public function getCommands() {
return $this->commands;
}
/**
* Gets the form definition.
*
* @return array
*/
public function getForm() {
return $this->form;
}
/**
* Gets the unique form build ID.
*
* @return string
*/
public function getFormBuildId() {
return $this->formBuildId;
}
/**
* Gets the unique form ID.
*
* @return string
*/
public function getFormId() {
return $this->formId;
}
/**
* Gets the form state.
*
* @return \Drupal\Core\Form\FormStateInterface
*/
public function getFormState() {
return $this->formState;
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* @file
* Contains \Drupal\system\FileDownloadController.
*/
namespace Drupal\system;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* System file controller.
*/
class FileDownloadController extends ControllerBase {
/**
* Handles private file transfers.
*
* Call modules that implement hook_file_download() to find out if a file is
* accessible and what headers it should be transferred with. If one or more
* modules returned headers the download will start with the returned headers.
* If a module returns -1 an AccessDeniedHttpException will be thrown. If the
* file exists but no modules responded an AccessDeniedHttpException will be
* thrown. If the file does not exist a NotFoundHttpException will be thrown.
*
* @see hook_file_download()
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param string $scheme
* The file scheme, defaults to 'private'.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown when the requested file does not exist.
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Thrown when the user does not have access to the file.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* The transferred file as response.
*/
public function download(Request $request, $scheme = 'private') {
$target = $request->query->get('file');
// Merge remaining path arguments into relative file path.
$uri = $scheme . '://' . $target;
if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
// Let other modules provide headers and controls access to the file.
$headers = $this->moduleHandler()->invokeAll('file_download', array($uri));
foreach ($headers as $result) {
if ($result == -1) {
throw new AccessDeniedHttpException();
}
}
if (count($headers)) {
return new BinaryFileResponse($uri, 200, $headers);
}
throw new AccessDeniedHttpException();
}
throw new NotFoundHttpException();
}
}

View file

@ -0,0 +1,156 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\CronForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\CronInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Form\ConfigFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Configure cron settings for this site.
*/
class CronForm extends ConfigFormBase {
/**
* Stores the state storage service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The cron service.
*
* @var \Drupal\Core\CronInterface
*/
protected $cron;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* Constructs a CronForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\State\StateInterface $state
* The state key value store.
* @param \Drupal\Core\CronInterface $cron
* The cron service.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
*/
public function __construct(ConfigFactoryInterface $config_factory, StateInterface $state, CronInterface $cron, DateFormatter $date_formatter) {
parent::__construct($config_factory);
$this->state = $state;
$this->cron = $cron;
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('state'),
$container->get('cron'),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_cron_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.cron'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('system.cron');
$form['description'] = array(
'#markup' => '<p>' . t('Cron takes care of running periodic tasks like checking for updates and indexing content for search.') . '</p>',
);
$form['run'] = array(
'#type' => 'submit',
'#value' => t('Run cron'),
'#submit' => array('::submitCron'),
);
$status = '<p>' . $this->t('Last run: %time ago.', array('%time' => $this->dateFormatter->formatTimeDiffSince($this->state->get('system.cron_last')))) . '</p>';
$form['status'] = array(
'#markup' => $status,
);
$form['cron_url'] = array(
'#markup' => '<p>' . t('To run cron from outside the site, go to <a href="!cron">!cron</a>', array('!cron' => $this->url('system.cron', array('key' => $this->state->get('system.cron_key')), array('absolute' => TRUE)))) . '</p>',
);
$form['cron'] = array(
'#title' => t('Cron settings'),
'#type' => 'details',
'#open' => TRUE,
);
$options = array(3600, 10800, 21600, 43200, 86400, 604800);
$form['cron']['cron_safe_threshold'] = array(
'#type' => 'select',
'#title' => t('Run cron every'),
'#description' => t('More information about setting up scheduled tasks can be found by <a href="@url">reading the cron tutorial on drupal.org</a>.', array('@url' => 'https://www.drupal.org/cron')),
'#default_value' => $config->get('threshold.autorun'),
'#options' => array(0 => t('Never')) + array_map(array($this->dateFormatter, 'formatInterval'), array_combine($options, $options)),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.cron')
->set('threshold.autorun', $form_state->getValue('cron_safe_threshold'))
->save();
parent::submitForm($form, $form_state);
}
/**
* Runs cron and reloads the page.
*/
public function submitCron(array &$form, FormStateInterface $form_state) {
// Run cron manually from Cron form.
if ($this->cron->run()) {
drupal_set_message(t('Cron run successfully.'));
}
else {
drupal_set_message(t('Cron run failed.'), 'error');
}
return new RedirectResponse($this->url('system.cron_settings', array(), array('absolute' => TRUE)));
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\DateFormatAddForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form for adding a date format.
*/
class DateFormatAddForm extends DateFormatFormBase {
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = t('Add format');
return $actions;
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\DateFormatDeleteForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityDeleteForm;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Builds a form to delete a date format.
*/
class DateFormatDeleteForm extends EntityDeleteForm {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* Constructs an DateFormatDeleteForm object.
*
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
*/
public function __construct(DateFormatter $date_formatter) {
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to delete the format %name : %format?', array(
'%name' => $this->entity->label(),
'%format' => $this->dateFormatter->format(REQUEST_TIME, $this->entity->id()))
);
}
}

View file

@ -0,0 +1,40 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\DateFormatEditForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form for editing a date format.
*/
class DateFormatEditForm extends DateFormatFormBase {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$now = t('Displayed as %date', array('%date' => $this->dateFormatter->format(REQUEST_TIME, $this->entity->id())));
$form['date_format_pattern']['#field_suffix'] = ' <small data-drupal-date-formatter="preview">' . $now . '</small>';
$form['date_format_pattern']['#default_value'] = $this->entity->getPattern();
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = t('Save format');
unset($actions['delete']);
return $actions;
}
}

View file

@ -0,0 +1,166 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\DateFormatFormBase.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityForm;
/**
* Provides a base form for date formats.
*/
abstract class DateFormatFormBase extends EntityForm {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* The date format storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $dateFormatStorage;
/**
* Constructs a new date format form.
*
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date service.
* @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $date_format_storage
* The date format storage.
*/
public function __construct(DateFormatter $date_formatter, ConfigEntityStorageInterface $date_format_storage) {
$date = new DrupalDateTime();
$this->dateFormatter = $date_formatter;
$this->dateFormatStorage = $date_format_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter'),
$container->get('entity.manager')->getStorage('date_format')
);
}
/**
* Checks for an existing date format.
*
* @param string|int $entity_id
* The entity ID.
* @param array $element
* The form element.
*
* @return bool
* TRUE if this format already exists, FALSE otherwise.
*/
public function exists($entity_id, array $element) {
return (bool) $this->dateFormatStorage
->getQuery()
->condition('id', $element['#field_prefix'] . $entity_id)
->execute();
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form['label'] = array(
'#type' => 'textfield',
'#title' => 'Name',
'#maxlength' => 100,
'#description' => t('Name of the date format'),
'#default_value' => $this->entity->label(),
);
$form['id'] = array(
'#type' => 'machine_name',
'#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
'#disabled' => !$this->entity->isNew(),
'#default_value' => $this->entity->id(),
'#machine_name' => array(
'exists' => array($this, 'exists'),
'replace_pattern' =>'([^a-z0-9_]+)|(^custom$)',
'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".'),
),
);
$form['date_format_pattern'] = array(
'#type' => 'textfield',
'#title' => t('Format string'),
'#maxlength' => 100,
'#description' => $this->t('A user-defined date format. See the <a href="@url">PHP manual</a> for available options.', array('@url' => 'http://php.net/manual/function.date.php')),
'#required' => TRUE,
'#attributes' => [
'data-drupal-date-formatter' => 'source',
],
'#field_suffix' => ' <small class="js-hide" data-drupal-date-formatter="preview">' . $this->t('Displayed as %date_format', ['%date_format' => '']) . '</small>',
);
$form['langcode'] = array(
'#type' => 'language_select',
'#title' => t('Language'),
'#languages' => LanguageInterface::STATE_ALL,
'#default_value' => $this->entity->language()->getId(),
);
$form['#attached']['drupalSettings']['dateFormats'] = $this->dateFormatter->getSampleDateFormats();
$form['#attached']['library'][] = 'system/drupal.system.date';
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validate(array $form, FormStateInterface $form_state) {
parent::validate($form, $form_state);
// The machine name field should already check to see if the requested
// machine name is available. Regardless of machine_name or human readable
// name, check to see if the provided pattern exists.
$pattern = trim($form_state->getValue('date_format_pattern'));
foreach ($this->dateFormatStorage->loadMultiple() as $format) {
if ($format->getPattern() == $pattern && ($this->entity->isNew() || $format->id() != $this->entity->id())) {
$form_state->setErrorByName('date_format_pattern', $this->t('This format already exists. Enter a unique format string.'));
continue;
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$form_state->setValue('pattern', trim($form_state->getValue('date_format_pattern')));
parent::submitForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$status = $this->entity->save();
if ($status == SAVED_UPDATED) {
drupal_set_message(t('Custom date format updated.'));
}
else {
drupal_set_message(t('Custom date format added.'));
}
$form_state->setRedirectUrl($this->entity->urlInfo('collection'));
}
}

View file

@ -0,0 +1,151 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\FileSystemForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StreamWrapper\PrivateStream;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure file system settings for this site.
*/
class FileSystemForm extends ConfigFormBase {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* The stream wrapper manager.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* Constructs a FileSystemForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
* The stream wrapper manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, DateFormatter $date_formatter, StreamWrapperManagerInterface $stream_wrapper_manager) {
parent::__construct($config_factory);
$this->dateFormatter = $date_formatter;
$this->streamWrapperManager = $stream_wrapper_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static (
$container->get('config.factory'),
$container->get('date.formatter'),
$container->get('stream_wrapper_manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_file_system_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.file'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('system.file');
$form['file_public_path'] = array(
'#type' => 'item',
'#title' => t('Public file system path'),
'#markup' => PublicStream::basePath(),
'#description' => t('A local file system path where public files will be stored. This directory must exist and be writable by Drupal. This directory must be relative to the Drupal installation directory and be accessible over the web. This must be changed in settings.php'),
);
$form['file_private_path'] = array(
'#type' => 'item',
'#title' => t('Private file system path'),
'#markup' => (PrivateStream::basePath() ? PrivateStream::basePath() : t('Not set')),
'#description' => t('An existing local file system path for storing private files. It should be writable by Drupal and not accessible over the web. This must be changed in settings.php'),
);
$form['file_temporary_path'] = array(
'#type' => 'textfield',
'#title' => t('Temporary directory'),
'#default_value' => $config->get('path.temporary'),
'#maxlength' => 255,
'#description' => t('A local file system path where temporary files will be stored. This directory should not be accessible over the web.'),
'#after_build' => array('system_check_directory'),
);
// Any visible, writeable wrapper can potentially be used for the files
// directory, including a remote file system that integrates with a CDN.
$options = $this->streamWrapperManager->getDescriptions(StreamWrapperInterface::WRITE_VISIBLE);
if (!empty($options)) {
$form['file_default_scheme'] = array(
'#type' => 'radios',
'#title' => t('Default download method'),
'#default_value' => $config->get('default_scheme'),
'#options' => $options,
'#description' => t('This setting is used as the preferred download method. The use of public files is more efficient, but does not provide any access control.'),
);
}
$intervals = array(0, 21600, 43200, 86400, 604800, 2419200, 7776000);
$period = array_combine($intervals, array_map(array($this->dateFormatter, 'formatInterval'), $intervals));
$period[0] = t('Never');
$form['temporary_maximum_age'] = array(
'#type' => 'select',
'#title' => t('Delete orphaned files after'),
'#default_value' => $config->get('temporary_maximum_age'),
'#options' => $period,
'#description' => t('Orphaned files are not referenced from any content but remain in the file system and may appear in administrative listings. <strong>Warning:</strong> If enabled, orphaned files will be permanently deleted and may not be recoverable.'),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('system.file')
->set('path.temporary', $form_state->getValue('file_temporary_path'))
->set('temporary_maximum_age', $form_state->getValue('temporary_maximum_age'));
if ($form_state->hasValue('file_default_scheme')) {
$config->set('default_scheme', $form_state->getValue('file_default_scheme'));
}
$config->save();
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,131 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\ImageToolkitForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\ImageToolkit\ImageToolkitManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configures image toolkit settings for this site.
*/
class ImageToolkitForm extends ConfigFormBase {
/**
* An array containing currently available toolkits.
*
* @var \Drupal\Core\ImageToolkit\ImageToolkitInterface[]
*/
protected $availableToolkits = array();
/**
* Constructs a ImageToolkitForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\ImageToolkit\ImageToolkitManager $manager
* The image toolkit plugin manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, ImageToolkitManager $manager) {
parent::__construct($config_factory);
foreach ($manager->getAvailableToolkits() as $id => $definition) {
$this->availableToolkits[$id] = $manager->createInstance($id);
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('image.toolkit.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_image_toolkit_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.image'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$current_toolkit = $this->config('system.image')->get('toolkit');
$form['image_toolkit'] = array(
'#type' => 'radios',
'#title' => $this->t('Select an image processing toolkit'),
'#default_value' => $current_toolkit,
'#options' => array(),
);
// If we have more than one image toolkit, allow the user to select the one
// to use, and load each of the toolkits' settings form.
foreach ($this->availableToolkits as $id => $toolkit) {
$definition = $toolkit->getPluginDefinition();
$form['image_toolkit']['#options'][$id] = $definition['title'];
$form['image_toolkit_settings'][$id] = array(
'#type' => 'details',
'#title' => $this->t('@toolkit settings', array('@toolkit' => $definition['title'])),
'#open' => TRUE,
'#tree' => TRUE,
'#states' => array(
'visible' => array(
':radio[name="image_toolkit"]' => array('value' => $id),
),
),
);
$form['image_toolkit_settings'][$id] += $toolkit->buildConfigurationForm(array(), $form_state);
}
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
// Call the form validation handler for each of the toolkits.
foreach ($this->availableToolkits as $toolkit) {
$toolkit->validateConfigurationForm($form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.image')
->set('toolkit', $form_state->getValue('image_toolkit'))
->save();
// Call the form submit handler for each of the toolkits.
foreach ($this->availableToolkits as $toolkit) {
$toolkit->submitConfigurationForm($form, $form_state);
}
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\LoggingForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure logging settings for this site.
*/
class LoggingForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_logging_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.logging'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('system.logging');
$form['error_level'] = array(
'#type' => 'radios',
'#title' => t('Error messages to display'),
'#default_value' => $config->get('error_level'),
'#options' => array(
ERROR_REPORTING_HIDE => t('None'),
ERROR_REPORTING_DISPLAY_SOME => t('Errors and warnings'),
ERROR_REPORTING_DISPLAY_ALL => t('All messages'),
ERROR_REPORTING_DISPLAY_VERBOSE => t('All messages, with backtrace information'),
),
'#description' => t('It is recommended that sites running on production environments do not display any errors.'),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.logging')
->set('error_level', $form_state->getValue('error_level'))
->save();
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,198 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\ModulesListConfirmForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Builds a confirmation form for enabling modules with dependencies.
*/
class ModulesListConfirmForm extends ConfirmFormBase {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The expirable key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
*/
protected $keyValueExpirable;
/**
* An associative list of modules to enable or disable.
*
* @var array
*/
protected $modules = array();
/**
* The module installer.
*
* @var \Drupal\Core\Extension\ModuleInstallerInterface
*/
protected $moduleInstaller;
/**
* Constructs a ModulesListConfirmForm object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
* The module installer.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable
* The key value expirable factory.
*/
public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable) {
$this->moduleHandler = $module_handler;
$this->moduleInstaller = $module_installer;
$this->keyValueExpirable = $key_value_expirable;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('module_installer'),
$container->get('keyvalue.expirable')->get('module_list')
);
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Some required modules must be enabled');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('system.modules_list');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Continue');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Would you like to continue with the above?');
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_modules_confirm_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$account = $this->currentUser()->id();
$this->modules = $this->keyValueExpirable->get($account);
// Redirect to the modules list page if the key value store is empty.
if (!$this->modules) {
return $this->redirect('system.modules_list');
}
$items = array();
// Display a list of required modules that have to be installed as well but
// were not manually selected.
foreach ($this->modules['dependencies'] as $module => $dependencies) {
$items[] = $this->formatPlural(count($dependencies), 'You must enable the @required module to install @module.', 'You must enable the @required modules to install @module.', array(
'@module' => $this->modules['install'][$module],
'@required' => implode(', ', $dependencies),
));
}
$form['message'] = array(
'#theme' => 'item_list',
'#items' => $items,
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Remove the key value store entry.
$account = $this->currentUser()->id();
$this->keyValueExpirable->delete($account);
// Gets list of modules prior to install process.
$before = $this->moduleHandler->getModuleList();
// Install the given modules.
if (!empty($this->modules['install'])) {
// Don't catch the exception that this can throw for missing dependencies:
// the form doesn't allow modules with unmet dependencies, so the only way
// this can happen is if the filesystem changed between form display and
// submit, in which case the user has bigger problems.
try {
$this->moduleInstaller->install(array_keys($this->modules['install']));
}
catch (PreExistingConfigException $e) {
$config_objects = $e->flattenConfigObjects($e->getConfigObjects());
drupal_set_message(
$this->formatPlural(
count($config_objects),
'Unable to install @extension, %config_names already exists in active configuration.',
'Unable to install @extension, %config_names already exist in active configuration.',
array(
'%config_names' => implode(', ', $config_objects),
'@extension' => $this->modules['install'][$e->getExtension()]
)),
'error'
);
return;
}
catch (UnmetDependenciesException $e) {
drupal_set_message(
$e->getTranslatedMessage($this->getStringTranslation(), $this->modules['install'][$e->getExtension()]),
'error'
);
return;
}
}
// Gets module list after install process, flushes caches and displays a
// message if there are changes.
if ($before != $this->moduleHandler->getModuleList()) {
drupal_set_message($this->t('The configuration options have been saved.'));
}
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View file

@ -0,0 +1,545 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\ModulesListForm.
*/
namespace Drupal\system\Form;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides module installation interface.
*
* The list of modules gets populated by module.info.yml files, which contain
* each module's name, description, and information about which modules it
* requires. See \Drupal\Core\Extension\InfoParser for info on module.info.yml
* descriptors.
*/
class ModulesListForm extends FormBase {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The expirable key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
*/
protected $keyValueExpirable;
/**
* The title resolver.
*
* @var \Drupal\Core\Controller\TitleResolverInterface
*/
protected $titleResolver;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The module installer.
*
* @var \Drupal\Core\Extension\ModuleInstallerInterface
*/
protected $moduleInstaller;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('module_installer'),
$container->get('keyvalue.expirable')->get('module_list'),
$container->get('access_manager'),
$container->get('current_user'),
$container->get('current_route_match'),
$container->get('title_resolver'),
$container->get('router.route_provider'),
$container->get('plugin.manager.menu.link')
);
}
/**
* Constructs a ModulesListForm object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
* The module installer.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable
* The key value expirable factory.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* Access manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
* The title resolver.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
*/
public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, AccessManagerInterface $access_manager, AccountInterface $current_user, RouteMatchInterface $route_match, TitleResolverInterface $title_resolver, RouteProviderInterface $route_provider, MenuLinkManagerInterface $menu_link_manager) {
$this->moduleHandler = $module_handler;
$this->moduleInstaller = $module_installer;
$this->keyValueExpirable = $key_value_expirable;
$this->accessManager = $access_manager;
$this->currentUser = $current_user;
$this->routeMatch = $route_match;
$this->titleResolver = $title_resolver;
$this->routeProvider = $route_provider;
$this->menuLinkManager = $menu_link_manager;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_modules';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
require_once DRUPAL_ROOT . '/core/includes/install.inc';
$distribution = SafeMarkup::checkPlain(drupal_install_profile_distribution_name());
// Include system.admin.inc so we can use the sort callbacks.
$this->moduleHandler->loadInclude('system', 'inc', 'system.admin');
$form['filters'] = array(
'#type' => 'container',
'#attributes' => array(
'class' => array('table-filter', 'js-show'),
),
);
$form['filters']['text'] = array(
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter module name'),
'#attributes' => array(
'class' => array('table-filter-text'),
'data-table' => '#system-modules',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the module name or description to filter by.'),
),
);
// Sort all modules by their names.
$modules = system_rebuild_module_data();
uasort($modules, 'system_sort_modules_by_info_name');
// Iterate over each of the modules.
$form['modules']['#tree'] = TRUE;
foreach ($modules as $filename => $module) {
if (empty($module->info['hidden'])) {
$package = $module->info['package'];
$form['modules'][$package][$filename] = $this->buildRow($modules, $module, $distribution);
}
}
// Add a wrapper around every package.
foreach (Element::children($form['modules']) as $package) {
$form['modules'][$package] += array(
'#type' => 'details',
'#title' => $this->t($package),
'#open' => TRUE,
'#theme' => 'system_modules_details',
'#header' => array(
array('data' => $this->t('Installed'), 'class' => array('checkbox', 'visually-hidden')),
array('data' => $this->t('Name'), 'class' => array('name', 'visually-hidden')),
array('data' => $this->t('Description'), 'class' => array('description', 'visually-hidden', RESPONSIVE_PRIORITY_LOW)),
),
'#attributes' => array('class' => array('package-listing')),
// Ensure that the "Core" package comes first.
'#weight' => $package == 'Core' ? -10 : NULL,
);
}
// If testing modules are shown, collapse the corresponding package by
// default.
if (isset($form['modules']['Testing'])) {
$form['modules']['Testing']['#open'] = FALSE;
}
// Lastly, sort all packages by title.
uasort($form['modules'], array('\Drupal\Component\Utility\SortArray', 'sortByTitleProperty'));
$form['#attached']['library'][] = 'system/drupal.system.modules';
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Save configuration'),
'#button_type' => 'primary',
);
return $form;
}
/**
* Builds a table row for the system modules page.
*
* @param array $modules
* The list existing modules.
* @param \Drupal\Core\Extension\Extension $module
* The module for which to build the form row.
* @param $distribution
*
* @return array
* The form row for the given module.
*/
protected function buildRow(array $modules, Extension $module, $distribution) {
// Set the basic properties.
$row['#required'] = array();
$row['#requires'] = array();
$row['#required_by'] = array();
$row['name']['#markup'] = $module->info['name'];
$row['description']['#markup'] = $this->t($module->info['description']);
$row['version']['#markup'] = $module->info['version'];
// Generate link for module's help page. Assume that if a hook_help()
// implementation exists then the module provides an overview page, rather
// than checking to see if the page exists, which is costly.
$row['links']['help'] = array();
if ($this->moduleHandler->moduleExists('help') && $module->status && in_array($module->getName(), $this->moduleHandler->getImplementations('help'))) {
$row['links']['help'] = array(
'#type' => 'link',
'#title' => $this->t('Help'),
'#url' => Url::fromRoute('help.page', ['name' => $module->getName()]),
'#options' => array('attributes' => array('class' => array('module-link', 'module-link-help'), 'title' => $this->t('Help'))),
);
}
// Generate link for module's permission, if the user has access to it.
$row['links']['permissions'] = array();
if ($module->status && \Drupal::currentUser()->hasPermission('administer permissions') && in_array($module->getName(), $this->moduleHandler->getImplementations('permission'))) {
$row['links']['permissions'] = array(
'#type' => 'link',
'#title' => $this->t('Permissions'),
'#url' => Url::fromRoute('user.admin_permissions'),
'#options' => array('fragment' => 'module-' . $module->getName(), 'attributes' => array('class' => array('module-link', 'module-link-permissions'), 'title' => $this->t('Configure permissions'))),
);
}
// Generate link for module's configuration page, if it has one.
$row['links']['configure'] = array();
if ($module->status && isset($module->info['configure'])) {
$route_parameters = isset($module->info['configure_parameters']) ? $module->info['configure_parameters'] : array();
if ($this->accessManager->checkNamedRoute($module->info['configure'], $route_parameters, $this->currentUser)) {
$links = $this->menuLinkManager->loadLinksByRoute($module->info['configure']);
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = reset($links);
// Most configure links have a corresponding menu link, though some just
// have a route.
if ($link) {
$description = $link->getDescription();
}
else {
$request = new Request();
$request->attributes->set('_route_name', $module->info['configure']);
$route_object = $this->routeProvider->getRouteByName($module->info['configure']);
$request->attributes->set('_route', $route_object);
$request->attributes->add($route_parameters);
$description = $this->titleResolver->getTitle($request, $route_object);
}
$row['links']['configure'] = array(
'#type' => 'link',
'#title' => $this->t('Configure'),
'#url' => Url::fromRoute($module->info['configure'], $route_parameters),
'#options' => array(
'attributes' => array(
'class' => array('module-link', 'module-link-configure'),
'title' => $description,
),
),
);
}
}
// Present a checkbox for installing and indicating the status of a module.
$row['enable'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Install'),
'#default_value' => (bool) $module->status,
'#disabled' => (bool) $module->status,
);
// Disable the checkbox for required modules.
if (!empty($module->info['required'])) {
// Used when displaying modules that are required by the installation profile
$row['enable']['#disabled'] = TRUE;
$row['#required_by'][] = $distribution . (!empty($module->info['explanation']) ? ' ('. $module->info['explanation'] .')' : '');
}
// Check the compatibilities.
$compatible = TRUE;
// Initialize an empty array of reasons why the module is incompatible. Add
// each reason as a separate element of the array.
$reasons = array();
// Check the core compatibility.
if ($module->info['core'] != \Drupal::CORE_COMPATIBILITY) {
$compatible = FALSE;
$reasons[] = $this->t('This version is not compatible with Drupal !core_version and should be replaced.', array(
'!core_version' => \Drupal::CORE_COMPATIBILITY,
));
}
// Ensure this module is compatible with the currently installed version of PHP.
if (version_compare(phpversion(), $module->info['php']) < 0) {
$compatible = FALSE;
$required = $module->info['php'] . (substr_count($module->info['php'], '.') < 2 ? '.*' : '');
$reasons[] = $this->t('This module requires PHP version @php_required and is incompatible with PHP version !php_version.', array(
'@php_required' => $required,
'!php_version' => phpversion(),
));
}
// If this module is not compatible, disable the checkbox.
if (!$compatible) {
$status = implode(' ', $reasons);
$row['enable']['#disabled'] = TRUE;
$row['description']['#markup'] = $status;
$row['#attributes']['class'][] = 'incompatible';
}
// If this module requires other modules, add them to the array.
foreach ($module->requires as $dependency => $version) {
if (!isset($modules[$dependency])) {
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">missing</span>)', array('@module' => Unicode::ucfirst($dependency)));
$row['enable']['#disabled'] = TRUE;
}
// Only display visible modules.
elseif (empty($modules[$dependency]->hidden)) {
$name = $modules[$dependency]->info['name'];
// Disable the module's checkbox if it is incompatible with the
// dependency's version.
if ($incompatible_version = drupal_check_incompatibility($version, str_replace(\Drupal::CORE_COMPATIBILITY . '-', '', $modules[$dependency]->info['version']))) {
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> version @version)', array(
'@module' => $name . $incompatible_version,
'@version' => $modules[$dependency]->info['version'],
));
$row['enable']['#disabled'] = TRUE;
}
// Disable the checkbox if the dependency is incompatible with this
// version of Drupal core.
elseif ($modules[$dependency]->info['core'] != \Drupal::CORE_COMPATIBILITY) {
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-missing">incompatible with</span> this version of Drupal core)', array(
'@module' => $name,
));
$row['enable']['#disabled'] = TRUE;
}
elseif ($modules[$dependency]->status) {
$row['#requires'][$dependency] = $this->t('@module', array('@module' => $name));
}
else {
$row['#requires'][$dependency] = $this->t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $name));
}
}
}
// If this module is required by other modules, list those, and then make it
// impossible to disable this one.
foreach ($module->required_by as $dependent => $version) {
if (isset($modules[$dependent]) && empty($modules[$dependent]->info['hidden'])) {
if ($modules[$dependent]->status == 1 && $module->status == 1) {
$row['#required_by'][$dependent] = $this->t('@module', array('@module' => $modules[$dependent]->info['name']));
$row['enable']['#disabled'] = TRUE;
}
else {
$row['#required_by'][$dependent] = $this->t('@module (<span class="admin-disabled">disabled</span>)', array('@module' => $modules[$dependent]->info['name']));
}
}
}
return $row;
}
/**
* Helper function for building a list of modules to install.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* An array of modules to install and their dependencies.
*/
protected function buildModuleList(FormStateInterface $form_state) {
$packages = $form_state->getValue('modules');
// Build a list of modules to install.
$modules = array(
'install' => array(),
'dependencies' => array(),
);
// Required modules have to be installed.
// @todo This should really not be handled here.
$data = system_rebuild_module_data();
foreach ($data as $name => $module) {
if (!empty($module->required) && !$this->moduleHandler->moduleExists($name)) {
$modules['install'][$name] = $module->info['name'];
}
}
// First, build a list of all modules that were selected.
foreach ($packages as $items) {
foreach ($items as $name => $checkbox) {
if ($checkbox['enable'] && !$this->moduleHandler->moduleExists($name)) {
$modules['install'][$name] = $data[$name]->info['name'];
}
}
}
// Add all dependencies to a list.
while (list($module) = each($modules['install'])) {
foreach (array_keys($data[$module]->requires) as $dependency) {
if (!isset($modules['install'][$dependency]) && !$this->moduleHandler->moduleExists($dependency)) {
$modules['dependencies'][$module][$dependency] = $data[$dependency]->info['name'];
$modules['install'][$dependency] = $data[$dependency]->info['name'];
}
}
}
// Make sure the install API is available.
include_once DRUPAL_ROOT . '/core/includes/install.inc';
// Invoke hook_requirements('install'). If failures are detected, make
// sure the dependent modules aren't installed either.
foreach (array_keys($modules['install']) as $module) {
if (!drupal_check_module($module)) {
unset($modules['install'][$module]);
foreach (array_keys($data[$module]->required_by) as $dependent) {
unset($modules['install'][$dependent]);
unset($modules['dependencies'][$dependent]);
}
}
}
return $modules;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Retrieve a list of modules to install and their dependencies.
$modules = $this->buildModuleList($form_state);
// Check if we have to install any dependencies. If there is one or more
// dependencies that are not installed yet, redirect to the confirmation
// form.
if (!empty($modules['dependencies']) || !empty($modules['missing'])) {
// Write the list of changed module states into a key value store.
$account = $this->currentUser()->id();
$this->keyValueExpirable->setWithExpire($account, $modules, 60);
// Redirect to the confirmation form.
$form_state->setRedirect('system.modules_list_confirm');
// We can exit here because at least one modules has dependencies
// which we have to prompt the user for in a confirmation form.
return;
}
// Gets list of modules prior to install process.
$before = $this->moduleHandler->getModuleList();
// There seem to be no dependencies that would need approval.
if (!empty($modules['install'])) {
try {
$this->moduleInstaller->install(array_keys($modules['install']));
}
catch (PreExistingConfigException $e) {
$config_objects = $e->flattenConfigObjects($e->getConfigObjects());
drupal_set_message(
$this->formatPlural(
count($config_objects),
'Unable to install @extension, %config_names already exists in active configuration.',
'Unable to install @extension, %config_names already exist in active configuration.',
array(
'%config_names' => implode(', ', $config_objects),
'@extension' => $modules['install'][$e->getExtension()]
)),
'error'
);
return;
}
catch (UnmetDependenciesException $e) {
drupal_set_message(
$e->getTranslatedMessage($this->getStringTranslation(), $modules['install'][$e->getExtension()]),
'error'
);
return;
}
}
// Gets module list after install process, flushes caches and displays a
// message if there are changes.
if ($before != $this->moduleHandler->getModuleList()) {
drupal_set_message(t('The configuration options have been saved.'));
}
}
}

View file

@ -0,0 +1,172 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\ModulesUninstallConfirmForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\Entity\ConfigDependencyDeleteFormTrait;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
/**
* Builds a confirmation form to uninstall selected modules.
*/
class ModulesUninstallConfirmForm extends ConfirmFormBase {
use ConfigDependencyDeleteFormTrait;
/**
* The module installer service.
*
* @var \Drupal\Core\Extension\ModuleInstallerInterface
*/
protected $moduleInstaller;
/**
* The expirable key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
*/
protected $keyValueExpirable;
/**
* The configuration manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* An array of modules to uninstall.
*
* @var array
*/
protected $modules = array();
/**
* Constructs a ModulesUninstallConfirmForm object.
*
* @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
* The module installer.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable
* The key value expirable factory.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration manager.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable, ConfigManagerInterface $config_manager, EntityManagerInterface $entity_manager) {
$this->moduleInstaller = $module_installer;
$this->keyValueExpirable = $key_value_expirable;
$this->configManager = $config_manager;
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_installer'),
$container->get('keyvalue.expirable')->get('modules_uninstall'),
$container->get('config.manager'),
$container->get('entity.manager')
);
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Confirm uninstall');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Uninstall');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('system.modules_uninstall');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Would you like to continue with uninstalling the above?');
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_modules_uninstall_confirm_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Retrieve the list of modules from the key value store.
$account = $this->currentUser()->id();
$this->modules = $this->keyValueExpirable->get($account);
// Prevent this page from showing when the module list is empty.
if (empty($this->modules)) {
drupal_set_message($this->t('The selected modules could not be uninstalled, either due to a website problem or due to the uninstall confirmation form timing out. Please try again.'), 'error');
return $this->redirect('system.modules_uninstall');
}
$data = system_rebuild_module_data();
$form['text']['#markup'] = '<p>' . $this->t('The following modules will be completely uninstalled from your site, and <em>all data from these modules will be lost</em>!') . '</p>';
$form['modules'] = array(
'#theme' => 'item_list',
'#items' => array_map(function ($module) use ($data) {
return $data[$module]->info['name'];
}, $this->modules),
);
// List the dependent entities.
$this->addDependencyListsToForm($form, 'module', $this->modules ,$this->configManager, $this->entityManager);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Clear the key value store entry.
$account = $this->currentUser()->id();
$this->keyValueExpirable->delete($account);
// Uninstall the modules.
$this->moduleInstaller->uninstall($this->modules);
drupal_set_message($this->t('The selected modules have been uninstalled.'));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View file

@ -0,0 +1,194 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\ModulesUninstallForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for uninstalling modules.
*/
class ModulesUninstallForm extends FormBase {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The module installer service.
*
* @var \Drupal\Core\Extension\ModuleInstallerInterface
*/
protected $moduleInstaller;
/**
* The expirable key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
*/
protected $keyValueExpirable;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('module_installer'),
$container->get('keyvalue.expirable')->get('modules_uninstall')
);
}
/**
* Constructs a ModulesUninstallForm object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
* The module installer.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable
* The key value expirable factory.
*/
public function __construct(ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, KeyValueStoreExpirableInterface $key_value_expirable) {
$this->moduleHandler = $module_handler;
$this->moduleInstaller = $module_installer;
$this->keyValueExpirable = $key_value_expirable;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_modules_uninstall';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Make sure the install API is available.
include_once DRUPAL_ROOT . '/core/includes/install.inc';
// Get a list of all available modules.
$modules = system_rebuild_module_data();
$uninstallable = array_filter($modules, function ($module) use ($modules) {
return empty($modules[$module->getName()]->info['required']) && drupal_get_installed_schema_version($module->getName()) > SCHEMA_UNINSTALLED;
});
// Include system.admin.inc so we can use the sort callbacks.
$this->moduleHandler->loadInclude('system', 'inc', 'system.admin');
$form['filters'] = array(
'#type' => 'container',
'#attributes' => array(
'class' => array('table-filter', 'js-show'),
),
);
$form['filters']['text'] = array(
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter module name'),
'#attributes' => array(
'class' => array('table-filter-text'),
'data-table' => '#system-modules-uninstall',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the module name or description to filter by.'),
),
);
$form['modules'] = array();
// Only build the rest of the form if there are any modules available to
// uninstall;
if (empty($uninstallable)) {
return $form;
}
$profile = drupal_get_profile();
// Sort all modules by their name.
uasort($uninstallable, 'system_sort_modules_by_info_name');
$validation_reasons = $this->moduleInstaller->validateUninstall(array_keys($uninstallable));
$form['uninstall'] = array('#tree' => TRUE);
foreach ($uninstallable as $module_key => $module) {
$name = $module->info['name'] ?: $module->getName();
$form['modules'][$module->getName()]['#module_name'] = $name;
$form['modules'][$module->getName()]['name']['#markup'] = $name;
$form['modules'][$module->getName()]['description']['#markup'] = $this->t($module->info['description']);
$form['uninstall'][$module->getName()] = array(
'#type' => 'checkbox',
'#title' => $this->t('Uninstall @module module', array('@module' => $name)),
'#title_display' => 'invisible',
);
// If a validator returns reasons not to uninstall a module,
// list the reasons and disable the check box.
if (isset($validation_reasons[$module_key])) {
$form['modules'][$module->getName()]['#validation_reasons'] = $validation_reasons[$module_key];
$form['uninstall'][$module->getName()]['#disabled'] = TRUE;
}
// All modules which depend on this one must be uninstalled first, before
// we can allow this module to be uninstalled. (The installation profile
// is excluded from this list.)
foreach (array_keys($module->required_by) as $dependent) {
if ($dependent != $profile && drupal_get_installed_schema_version($dependent) != SCHEMA_UNINSTALLED) {
$name = isset($modules[$dependent]->info['name']) ? $modules[$dependent]->info['name'] : $dependent;
$form['modules'][$module->getName()]['#required_by'][] = $name;
$form['uninstall'][$module->getName()]['#disabled'] = TRUE;
}
}
}
$form['#attached']['library'][] = 'system/drupal.system.modules';
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Uninstall'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Form submitted, but no modules selected.
if (!array_filter($form_state->getValue('uninstall'))) {
$form_state->setErrorByName('uninstall', $this->t('No modules selected.'));
$form_state->setRedirect('system.modules_uninstall');
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Save all the values in an expirable key value store.
$modules = $form_state->getValue('uninstall');
$uninstall = array_keys(array_filter($modules));
$account = $this->currentUser()->id();
// Store the values for 6 hours. This expiration time is also used in
// the form cache.
$this->keyValueExpirable->setWithExpire($account, $uninstall, 6*60*60);
// Redirect to the confirm form.
$form_state->setRedirect('system.modules_uninstall_confirm');
}
}

View file

@ -0,0 +1,198 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\PerformanceForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure performance settings for this site.
*/
class PerformanceForm extends ConfigFormBase {
/**
* The render cache bin.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $renderCache;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* The CSS asset collection optimizer service.
*
* @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
*/
protected $cssCollectionOptimizer;
/**
* The JavaScript asset collection optimizer service.
*
* @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
*/
protected $jsCollectionOptimizer;
/**
* Constructs a PerformanceForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Cache\CacheBackendInterface $render_cache
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
* @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
* The CSS asset collection optimizer service.
* @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $js_collection_optimizer
* The JavaScript asset collection optimizer service.
*/
public function __construct(ConfigFactoryInterface $config_factory, CacheBackendInterface $render_cache, DateFormatter $date_formatter, AssetCollectionOptimizerInterface $css_collection_optimizer, AssetCollectionOptimizerInterface $js_collection_optimizer) {
parent::__construct($config_factory);
$this->renderCache = $render_cache;
$this->dateFormatter = $date_formatter;
$this->cssCollectionOptimizer = $css_collection_optimizer;
$this->jsCollectionOptimizer = $js_collection_optimizer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('cache.render'),
$container->get('date.formatter'),
$container->get('asset.css.collection_optimizer'),
$container->get('asset.js.collection_optimizer')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_performance_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.performance'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#attached']['library'][] = 'system/drupal.system';
$config = $this->config('system.performance');
$form['clear_cache'] = array(
'#type' => 'details',
'#title' => t('Clear cache'),
'#open' => TRUE,
);
$form['clear_cache']['clear'] = array(
'#type' => 'submit',
'#value' => t('Clear all caches'),
'#submit' => array('::submitCacheClear'),
);
$form['caching'] = array(
'#type' => 'details',
'#title' => t('Caching'),
'#open' => TRUE,
'#description' => $this->t('Note: Drupal provides an internal page cache module that is recommended for small to medium-sized websites.'),
);
// Identical options to the ones for block caching.
// @see \Drupal\Core\Block\BlockBase::buildConfigurationForm()
$period = array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400);
$period = array_map(array($this->dateFormatter, 'formatInterval'), array_combine($period, $period));
$period[0] = '<' . t('no caching') . '>';
$form['caching']['page_cache_maximum_age'] = array(
'#type' => 'select',
'#title' => t('Page cache maximum age'),
'#default_value' => $config->get('cache.page.max_age'),
'#options' => $period,
'#description' => t('The maximum time a page can be cached by browsers and proxies. This is used as the value for max-age in Cache-Control headers.'),
);
$directory = 'public://';
$is_writable = is_dir($directory) && is_writable($directory);
$disabled = !$is_writable;
$disabled_message = '';
if (!$is_writable) {
$disabled_message = ' ' . t('<strong class="error">Set up the <a href="!file-system">public files directory</a> to make these optimizations available.</strong>', array('!file-system' => $this->url('system.file_system_settings')));
}
$form['bandwidth_optimization'] = array(
'#type' => 'details',
'#title' => t('Bandwidth optimization'),
'#open' => TRUE,
'#description' => t('External resources can be optimized automatically, which can reduce both the size and number of requests made to your website.') . $disabled_message,
);
$form['bandwidth_optimization']['preprocess_css'] = array(
'#type' => 'checkbox',
'#title' => t('Aggregate CSS files'),
'#default_value' => $config->get('css.preprocess'),
'#disabled' => $disabled,
);
$form['bandwidth_optimization']['preprocess_js'] = array(
'#type' => 'checkbox',
'#title' => t('Aggregate JavaScript files'),
'#default_value' => $config->get('js.preprocess'),
'#disabled' => $disabled,
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->cssCollectionOptimizer->deleteAll();
$this->jsCollectionOptimizer->deleteAll();
// This form allows page compression settings to be changed, which can
// invalidate cached pages in the render cache, so it needs to be cleared on
// form submit.
$this->renderCache->deleteAll();
$this->config('system.performance')
->set('cache.page.max_age', $form_state->getValue('page_cache_maximum_age'))
->set('css.preprocess', $form_state->getValue('preprocess_css'))
->set('js.preprocess', $form_state->getValue('preprocess_js'))
->save();
parent::submitForm($form, $form_state);
}
/**
* Clears the caches.
*/
public function submitCacheClear(array &$form, FormStateInterface $form_state) {
drupal_flush_all_caches();
drupal_set_message(t('Caches cleared.'));
}
}

View file

@ -0,0 +1,165 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\RegionalForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Locale\CountryManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure regional settings for this site.
*/
class RegionalForm extends ConfigFormBase {
/**
* The country manager.
*
* @var \Drupal\Core\Locale\CountryManagerInterface
*/
protected $countryManager;
/**
* Constructs a RegionalForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Locale\CountryManagerInterface $country_manager
* The country manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, CountryManagerInterface $country_manager) {
parent::__construct($config_factory);
$this->countryManager = $country_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('country_manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_regional_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.date'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$countries = $this->countryManager->getList();
$system_date = $this->config('system.date');
// Date settings:
$zones = system_time_zones();
$form['locale'] = array(
'#type' => 'details',
'#title' => t('Locale'),
'#open' => TRUE,
);
$form['locale']['site_default_country'] = array(
'#type' => 'select',
'#title' => t('Default country'),
'#empty_value' => '',
'#default_value' => $system_date->get('country.default'),
'#options' => $countries,
'#attributes' => array('class' => array('country-detect')),
);
$form['locale']['date_first_day'] = array(
'#type' => 'select',
'#title' => t('First day of week'),
'#default_value' => $system_date->get('first_day'),
'#options' => array(0 => t('Sunday'), 1 => t('Monday'), 2 => t('Tuesday'), 3 => t('Wednesday'), 4 => t('Thursday'), 5 => t('Friday'), 6 => t('Saturday')),
);
$form['timezone'] = array(
'#type' => 'details',
'#title' => t('Time zones'),
'#open' => TRUE,
);
$form['timezone']['date_default_timezone'] = array(
'#type' => 'select',
'#title' => t('Default time zone'),
'#default_value' => $system_date->get('timezone.default') ?: date_default_timezone_get(),
'#options' => $zones,
);
$configurable_timezones = $system_date->get('timezone.user.configurable');
$form['timezone']['configurable_timezones'] = array(
'#type' => 'checkbox',
'#title' => t('Users may set their own time zone'),
'#default_value' => $configurable_timezones,
);
$form['timezone']['configurable_timezones_wrapper'] = array(
'#type' => 'container',
'#states' => array(
// Hide the user configured timezone settings when users are forced to use
// the default setting.
'invisible' => array(
'input[name="configurable_timezones"]' => array('checked' => FALSE),
),
),
);
$form['timezone']['configurable_timezones_wrapper']['empty_timezone_message'] = array(
'#type' => 'checkbox',
'#title' => t('Remind users at login if their time zone is not set'),
'#default_value' => $system_date->get('timezone.user.warn'),
'#description' => t('Only applied if users may set their own time zone.')
);
$form['timezone']['configurable_timezones_wrapper']['user_default_timezone'] = array(
'#type' => 'radios',
'#title' => t('Time zone for new users'),
'#default_value' => $system_date->get('timezone.user.default'),
'#options' => array(
DRUPAL_USER_TIMEZONE_DEFAULT => t('Default time zone'),
DRUPAL_USER_TIMEZONE_EMPTY => t('Empty time zone'),
DRUPAL_USER_TIMEZONE_SELECT => t('Users may set their own time zone at registration'),
),
'#description' => t('Only applied if users may set their own time zone.')
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.date')
->set('country.default', $form_state->getValue('site_default_country'))
->set('first_day', $form_state->getValue('date_first_day'))
->set('timezone.default', $form_state->getValue('date_default_timezone'))
->set('timezone.user.configurable', $form_state->getValue('configurable_timezones'))
->set('timezone.user.warn', $form_state->getValue('empty_timezone_message'))
->set('timezone.user.default', $form_state->getValue('user_default_timezone'))
->save();
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\RssFeedsForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure RSS settings for this site.
*/
class RssFeedsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_rss_feeds_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.rss'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$rss_config = $this->config('system.rss');
$form['feed_description'] = array(
'#type' => 'textarea',
'#title' => t('Feed description'),
'#default_value' => $rss_config->get('channel.description'),
'#description' => t('Description of your site, included in each feed.')
);
$options = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30);
$form['feed_default_items'] = array(
'#type' => 'select',
'#title' => t('Number of items in each feed'),
'#default_value' => $rss_config->get('items.limit'),
'#options' => array_combine($options, $options),
'#description' => t('Default number of items to include in each feed.')
);
$form['feed_view_mode'] = array(
'#type' => 'select',
'#title' => t('Feed content'),
'#default_value' => $rss_config->get('items.view_mode'),
'#options' => array(
'title' => t('Titles only'),
'teaser' => t('Titles plus teaser'),
'fulltext' => t('Full text'),
),
'#description' => t('Global setting for the default display of content items in each feed.')
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.rss')
->set('channel.description', $form_state->getValue('feed_description'))
->set('items.limit', $form_state->getValue('feed_default_items'))
->set('items.view_mode', $form_state->getValue('feed_view_mode'))
->save();
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,223 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\SiteInformationForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Routing\RequestContext;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure site information settings for this site.
*/
class SiteInformationForm extends ConfigFormBase {
/**
* The path alias manager.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* The request context.
*
* @var \Drupal\Core\Routing\RequestContext
*/
protected $requestContext;
/**
* Constructs a SiteInformationForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* The path alias manager.
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
* The path validator.
* @param \Drupal\Core\Routing\RequestContext $request_context
* The request context.
*/
public function __construct(ConfigFactoryInterface $config_factory, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, RequestContext $request_context) {
parent::__construct($config_factory);
$this->aliasManager = $alias_manager;
$this->pathValidator = $path_validator;
$this->requestContext = $request_context;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('path.alias_manager'),
$container->get('path.validator'),
$container->get('router.request_context')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_site_information_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.site'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$site_config = $this->config('system.site');
$site_mail = $site_config->get('mail');
if (empty($site_mail)) {
$site_mail = ini_get('sendmail_from');
}
$form['site_information'] = array(
'#type' => 'details',
'#title' => t('Site details'),
'#open' => TRUE,
);
$form['site_information']['site_name'] = array(
'#type' => 'textfield',
'#title' => t('Site name'),
'#default_value' => $site_config->get('name'),
'#required' => TRUE,
);
$form['site_information']['site_slogan'] = array(
'#type' => 'textfield',
'#title' => t('Slogan'),
'#default_value' => $site_config->get('slogan'),
'#description' => t("How this is used depends on your site's theme."),
);
$form['site_information']['site_mail'] = array(
'#type' => 'email',
'#title' => t('Email address'),
'#default_value' => $site_mail,
'#description' => t("The <em>From</em> address in automated emails sent during registration and new password requests, and other notifications. (Use an address ending in your site's domain to help prevent this email being flagged as spam.)"),
'#required' => TRUE,
);
$form['front_page'] = array(
'#type' => 'details',
'#title' => t('Front page'),
'#open' => TRUE,
);
$front_page = $site_config->get('page.front') != '/user/login' ? $this->aliasManager->getAliasByPath($site_config->get('page.front')) : '';
$form['front_page']['site_frontpage'] = array(
'#type' => 'textfield',
'#title' => t('Default front page'),
'#default_value' => $front_page,
'#size' => 40,
'#description' => t('Optionally, specify a relative URL to display as the front page. Leave blank to display the default front page.'),
'#field_prefix' => $this->requestContext->getCompleteBaseUrl(),
);
$form['error_page'] = array(
'#type' => 'details',
'#title' => t('Error pages'),
'#open' => TRUE,
);
$form['error_page']['site_403'] = array(
'#type' => 'textfield',
'#title' => t('Default 403 (access denied) page'),
'#default_value' => $site_config->get('page.403'),
'#size' => 40,
'#description' => t('This page is displayed when the requested document is denied to the current user. Leave blank to display a generic "access denied" page.'),
);
$form['error_page']['site_404'] = array(
'#type' => 'textfield',
'#title' => t('Default 404 (not found) page'),
'#default_value' => $site_config->get('page.404'),
'#size' => 40,
'#description' => t('This page is displayed when no other content matches the requested document. Leave blank to display a generic "page not found" page.'),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Check for empty front page path.
if ($form_state->isValueEmpty('site_frontpage')) {
// Set to default "user/login".
$form_state->setValueForElement($form['front_page']['site_frontpage'], '/user/login');
}
else {
// Get the normal path of the front page.
$form_state->setValueForElement($form['front_page']['site_frontpage'], $this->aliasManager->getPathByAlias($form_state->getValue('site_frontpage')));
}
// Validate front page path.
if (($value = $form_state->getValue('site_frontpage')) && $value[0] !== '/') {
$form_state->setErrorByName('site_frontpage', $this->t("The path '%path' has to start with a slash.", ['%path' => $form_state->getValue('site_frontpage')]));
}
if (!$this->pathValidator->isValid($form_state->getValue('site_frontpage'))) {
$form_state->setErrorByName('site_frontpage', $this->t("The path '%path' is either invalid or you do not have access to it.", array('%path' => $form_state->getValue('site_frontpage'))));
}
// Get the normal paths of both error pages.
if (!$form_state->isValueEmpty('site_403')) {
$form_state->setValueForElement($form['error_page']['site_403'], $this->aliasManager->getPathByAlias($form_state->getValue('site_403')));
}
if (!$form_state->isValueEmpty('site_404')) {
$form_state->setValueForElement($form['error_page']['site_404'], $this->aliasManager->getPathByAlias($form_state->getValue('site_404')));
}
if (($value = $form_state->getValue('site_403')) && $value[0] !== '/') {
$form_state->setErrorByName('site_403', $this->t("The path '%path' has to start with a slash.", ['%path' => $form_state->getValue('site_403')]));
}
if (($value = $form_state->getValue('site_404')) && $value[0] !== '/') {
$form_state->setErrorByName('site_404', $this->t("The path '%path' has to start with a slash.", ['%path' => $form_state->getValue('site_404')]));
}
// Validate 403 error path.
if (!$form_state->isValueEmpty('site_403') && !$this->pathValidator->isValid($form_state->getValue('site_403'))) {
$form_state->setErrorByName('site_403', $this->t("The path '%path' is either invalid or you do not have access to it.", array('%path' => $form_state->getValue('site_403'))));
}
// Validate 404 error path.
if (!$form_state->isValueEmpty('site_404') && !$this->pathValidator->isValid($form_state->getValue('site_404'))) {
$form_state->setErrorByName('site_404', $this->t("The path '%path' is either invalid or you do not have access to it.", array('%path' => $form_state->getValue('site_404'))));
}
parent::validateForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.site')
->set('name', $form_state->getValue('site_name'))
->set('mail', $form_state->getValue('site_mail'))
->set('slogan', $form_state->getValue('site_slogan'))
->set('page.front', $form_state->getValue('site_frontpage'))
->set('page.403', $form_state->getValue('site_403'))
->set('page.404', $form_state->getValue('site_404'))
->save();
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,96 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\SiteMaintenanceModeForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Form\ConfigFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure maintenance settings for this site.
*/
class SiteMaintenanceModeForm extends ConfigFormBase {
/**
* The state keyvalue collection.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a new SiteMaintenanceModeForm.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\State\StateInterface $state
* The state keyvalue collection to use.
*/
public function __construct(ConfigFactoryInterface $config_factory, StateInterface $state) {
parent::__construct($config_factory);
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('state')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_site_maintenance_mode';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.maintenance'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('system.maintenance');
$form['maintenance_mode'] = array(
'#type' => 'checkbox',
'#title' => t('Put site into maintenance mode'),
'#default_value' => $this->state->get('system.maintenance_mode'),
'#description' => t('Visitors will only see the maintenance mode message. Only users with the "Access site in maintenance mode" <a href="@permissions-url">permission</a> will be able to access the site. Authorized users can log in directly via the <a href="@user-login">user login</a> page.', array('@permissions-url' => $this->url('user.admin_permissions'), '@user-login' => $this->url('user.login'))),
);
$form['maintenance_mode_message'] = array(
'#type' => 'textarea',
'#title' => t('Message to display when in maintenance mode'),
'#default_value' => $config->get('message'),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.maintenance')
->set('message', $form_state->getValue('maintenance_mode_message'))
->save();
$this->state->set('system.maintenance_mode', $form_state->getValue('maintenance_mode'));
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\ThemeAdminForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form to select the administration theme.
*/
class ThemeAdminForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_themes_admin_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.theme'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, array $theme_options = NULL) {
// Administration theme settings.
$form['admin_theme'] = array(
'#type' => 'details',
'#title' => $this->t('Administration theme'),
'#open' => TRUE,
);
$form['admin_theme']['admin_theme'] = array(
'#type' => 'select',
'#options' => array(0 => $this->t('Default theme')) + $theme_options,
'#title' => $this->t('Administration theme'),
'#description' => $this->t('Choose "Default theme" to always use the same theme as the rest of the site.'),
'#default_value' => $this->config('system.theme')->get('admin'),
);
$form['admin_theme']['actions'] = array('#type' => 'actions');
$form['admin_theme']['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Save configuration'),
'#button_type' => 'primary',
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$this->config('system.theme')->set('admin', $form_state->getValue('admin_theme'))->save();
}
}

View file

@ -0,0 +1,501 @@
<?php
/**
* @file
* Contains \Drupal\system\Form\ThemeSettingsForm.
*/
namespace Drupal\system\Form;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StreamWrapper\PublicStream;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\ConfigFormBase;
/**
* Displays theme configuration for entire site and individual themes.
*/
class ThemeSettingsForm extends ConfigFormBase {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The MIME type guesser.
*
* @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
*/
protected $mimeTypeGuesser;
/**
* An array of configuration names that should be editable.
*
* @var array
*/
protected $editableConfig = [];
/**
* Constructs a ThemeSettingsForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler instance to use.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
* The MIME type guesser instance to use.
*/
public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, MimeTypeGuesserInterface $mime_type_guesser) {
parent::__construct($config_factory);
$this->moduleHandler = $module_handler;
$this->themeHandler = $theme_handler;
$this->mimeTypeGuesser = $mime_type_guesser;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('module_handler'),
$container->get('theme_handler'),
$container->get('file.mime_type.guesser')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_theme_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return $this->editableConfig;
}
/**
* {@inheritdoc}
*
* @param string $theme
* The theme name.
*/
public function buildForm(array $form, FormStateInterface $form_state, $theme = '') {
$form = parent::buildForm($form, $form_state);
$themes = $this->themeHandler->listInfo();
// Deny access if the theme is not installed or not found.
if (!empty($theme) && (empty($themes[$theme]) || !$themes[$theme]->status)) {
throw new NotFoundHttpException();
}
// Default settings are defined in theme_get_setting() in includes/theme.inc
if ($theme) {
$var = 'theme_' . $theme . '_settings';
$config_key = $theme . '.settings';
$themes = $this->themeHandler->listInfo();
$features = $themes[$theme]->info['features'];
}
else {
$var = 'theme_settings';
$config_key = 'system.theme.global';
}
// @todo this is pretty meaningless since we're using theme_get_settings
// which means overrides can bleed into active config here. Will be fixed
// by https://www.drupal.org/node/2402467.
$this->editableConfig = [$config_key];
$form['var'] = array(
'#type' => 'hidden',
'#value' => $var
);
$form['config_key'] = array(
'#type' => 'hidden',
'#value' => $config_key
);
// Toggle settings
$toggles = array(
'logo' => t('Logo'),
'name' => t('Site name'),
'slogan' => t('Site slogan'),
'node_user_picture' => t('User pictures in posts'),
'comment_user_picture' => t('User pictures in comments'),
'comment_user_verification' => t('User verification status in comments'),
'favicon' => t('Shortcut icon'),
);
// Some features are not always available
$disabled = array();
if (!user_picture_enabled()) {
$disabled['toggle_node_user_picture'] = TRUE;
$disabled['toggle_comment_user_picture'] = TRUE;
}
if (!$this->moduleHandler->moduleExists('comment')) {
$disabled['toggle_comment_user_picture'] = TRUE;
$disabled['toggle_comment_user_verification'] = TRUE;
}
$form['theme_settings'] = array(
'#type' => 'details',
'#title' => t('Toggle display'),
'#open' => TRUE,
'#description' => t('Enable or disable the display of certain page elements.'),
);
foreach ($toggles as $name => $title) {
if ((!$theme) || in_array($name, $features)) {
$form['theme_settings']['toggle_' . $name] = array('#type' => 'checkbox', '#title' => $title, '#default_value' => theme_get_setting('features.' . $name, $theme));
// Disable checkboxes for features not supported in the current configuration.
if (isset($disabled['toggle_' . $name])) {
$form['theme_settings']['toggle_' . $name]['#disabled'] = TRUE;
}
}
}
if (!Element::children($form['theme_settings'])) {
// If there is no element in the theme settings details then do not show
// it -- but keep it in the form if another module wants to alter.
$form['theme_settings']['#access'] = FALSE;
}
// Logo settings, only available when file.module is enabled.
if ((!$theme) || in_array('logo', $features) && $this->moduleHandler->moduleExists('file')) {
$form['logo'] = array(
'#type' => 'details',
'#title' => t('Logo image settings'),
'#open' => TRUE,
'#states' => array(
// Hide the logo image settings fieldset when logo display is disabled.
'invisible' => array(
'input[name="toggle_logo"]' => array('checked' => FALSE),
),
),
);
$form['logo']['default_logo'] = array(
'#type' => 'checkbox',
'#title' => t('Use the default logo supplied by the theme'),
'#default_value' => theme_get_setting('logo.use_default', $theme),
'#tree' => FALSE,
);
$form['logo']['settings'] = array(
'#type' => 'container',
'#states' => array(
// Hide the logo settings when using the default logo.
'invisible' => array(
'input[name="default_logo"]' => array('checked' => TRUE),
),
),
);
$form['logo']['settings']['logo_path'] = array(
'#type' => 'textfield',
'#title' => t('Path to custom logo'),
'#default_value' => theme_get_setting('logo.path', $theme),
);
$form['logo']['settings']['logo_upload'] = array(
'#type' => 'file',
'#title' => t('Upload logo image'),
'#maxlength' => 40,
'#description' => t("If you don't have direct file access to the server, use this field to upload your logo.")
);
}
if (((!$theme) || in_array('favicon', $features)) && $this->moduleHandler->moduleExists('file')) {
$form['favicon'] = array(
'#type' => 'details',
'#title' => t('Shortcut icon settings'),
'#open' => TRUE,
'#description' => t("Your shortcut icon, or 'favicon', is displayed in the address bar and bookmarks of most browsers."),
'#states' => array(
// Hide the shortcut icon settings fieldset when shortcut icon display
// is disabled.
'invisible' => array(
'input[name="toggle_favicon"]' => array('checked' => FALSE),
),
),
);
$form['favicon']['default_favicon'] = array(
'#type' => 'checkbox',
'#title' => t('Use the default shortcut icon supplied by the theme'),
'#default_value' => theme_get_setting('favicon.use_default', $theme),
);
$form['favicon']['settings'] = array(
'#type' => 'container',
'#states' => array(
// Hide the favicon settings when using the default favicon.
'invisible' => array(
'input[name="default_favicon"]' => array('checked' => TRUE),
),
),
);
$form['favicon']['settings']['favicon_path'] = array(
'#type' => 'textfield',
'#title' => t('Path to custom icon'),
'#default_value' => theme_get_setting('favicon.path', $theme),
);
$form['favicon']['settings']['favicon_upload'] = array(
'#type' => 'file',
'#title' => t('Upload icon image'),
'#description' => t("If you don't have direct file access to the server, use this field to upload your shortcut icon.")
);
}
// Inject human-friendly values and form element descriptions for logo and
// favicon.
foreach (array('logo' => 'logo.svg', 'favicon' => 'favicon.ico') as $type => $default) {
if (isset($form[$type]['settings'][$type . '_path'])) {
$element = &$form[$type]['settings'][$type . '_path'];
// If path is a public:// URI, display the path relative to the files
// directory; stream wrappers are not end-user friendly.
$original_path = $element['#default_value'];
$friendly_path = NULL;
if (file_uri_scheme($original_path) == 'public') {
$friendly_path = file_uri_target($original_path);
$element['#default_value'] = $friendly_path;
}
// Prepare local file path for description.
if ($original_path && isset($friendly_path)) {
$local_file = strtr($original_path, array('public:/' => PublicStream::basePath()));
}
elseif ($theme) {
$local_file = drupal_get_path('theme', $theme) . '/' . $default;
}
else {
$local_file = \Drupal::theme()->getActiveTheme()->getPath() . '/' . $default;
}
$element['#description'] = t('Examples: <code>@implicit-public-file</code> (for a file in the public filesystem), <code>@explicit-file</code>, or <code>@local-file</code>.', array(
'@implicit-public-file' => isset($friendly_path) ? $friendly_path : $default,
'@explicit-file' => file_uri_scheme($original_path) !== FALSE ? $original_path : 'public://' . $default,
'@local-file' => $local_file,
));
}
}
if ($theme) {
// Call engine-specific settings.
$function = $themes[$theme]->prefix . '_engine_settings';
if (function_exists($function)) {
$form['engine_specific'] = array(
'#type' => 'details',
'#title' => t('Theme-engine-specific settings'),
'#open' => TRUE,
'#description' => t('These settings only exist for the themes based on the %engine theme engine.', array('%engine' => $themes[$theme]->prefix)),
);
$function($form, $form_state);
}
// Create a list which includes the current theme and all its base themes.
if (isset($themes[$theme]->base_themes)) {
$theme_keys = array_keys($themes[$theme]->base_themes);
$theme_keys[] = $theme;
}
else {
$theme_keys = array($theme);
}
// Save the name of the current theme (if any), so that we can temporarily
// override the current theme and allow theme_get_setting() to work
// without having to pass the theme name to it.
$default_active_theme = \Drupal::theme()->getActiveTheme();
$default_theme = $default_active_theme->getName();
/** @var \Drupal\Core\Theme\ThemeInitialization $theme_initialization */
$theme_initialization = \Drupal::service('theme.initialization');
\Drupal::theme()->setActiveTheme($theme_initialization->getActiveThemeByName($theme));
// Process the theme and all its base themes.
foreach ($theme_keys as $theme) {
// Include the theme-settings.php file.
$filename = DRUPAL_ROOT . '/' . $themes[$theme]->getPath() . '/theme-settings.php';
if (file_exists($filename)) {
require_once $filename;
}
// Call theme-specific settings.
$function = $theme . '_form_system_theme_settings_alter';
if (function_exists($function)) {
$function($form, $form_state);
}
}
// Restore the original current theme.
if (isset($default_theme)) {
\Drupal::theme()->setActiveTheme($default_active_theme);
}
else {
\Drupal::theme()->resetActiveTheme();
}
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
if ($this->moduleHandler->moduleExists('file')) {
// Handle file uploads.
$validators = array('file_validate_is_image' => array());
// Check for a new uploaded logo.
$file = file_save_upload('logo_upload', $validators, FALSE, 0);
if (isset($file)) {
// File upload was attempted.
if ($file) {
// Put the temporary file in form_values so we can save it on submit.
$form_state->setValue('logo_upload', $file);
}
else {
// File upload failed.
$form_state->setErrorByName('logo_upload', $this->t('The logo could not be uploaded.'));
}
}
$validators = array('file_validate_extensions' => array('ico png gif jpg jpeg apng svg'));
// Check for a new uploaded favicon.
$file = file_save_upload('favicon_upload', $validators, FALSE, 0);
if (isset($file)) {
// File upload was attempted.
if ($file) {
// Put the temporary file in form_values so we can save it on submit.
$form_state->setValue('favicon_upload', $file);
}
else {
// File upload failed.
$form_state->setErrorByName('favicon_upload', $this->t('The favicon could not be uploaded.'));
}
}
// If the user provided a path for a logo or favicon file, make sure a file
// exists at that path.
if ($form_state->getValue('logo_path')) {
$path = $this->validatePath($form_state->getValue('logo_path'));
if (!$path) {
$form_state->setErrorByName('logo_path', $this->t('The custom logo path is invalid.'));
}
}
if ($form_state->getValue('favicon_path')) {
$path = $this->validatePath($form_state->getValue('favicon_path'));
if (!$path) {
$form_state->setErrorByName('favicon_path', $this->t('The custom favicon path is invalid.'));
}
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$config_key = $form_state->getValue('config_key');
$this->editableConfig = [$config_key];
$config = $this->config($config_key);
// Exclude unnecessary elements before saving.
$form_state->cleanValues();
$form_state->unsetValue('var');
$form_state->unsetValue('config_key');
$values = $form_state->getValues();
// If the user uploaded a new logo or favicon, save it to a permanent location
// and use it in place of the default theme-provided file.
if ($this->moduleHandler->moduleExists('file')) {
if ($file = $values['logo_upload']) {
$filename = file_unmanaged_copy($file->getFileUri());
$values['default_logo'] = 0;
$values['logo_path'] = $filename;
$values['toggle_logo'] = 1;
}
if ($file = $values['favicon_upload']) {
$filename = file_unmanaged_copy($file->getFileUri());
$values['default_favicon'] = 0;
$values['favicon_path'] = $filename;
$values['toggle_favicon'] = 1;
}
unset($values['logo_upload']);
unset($values['favicon_upload']);
// If the user entered a path relative to the system files directory for
// a logo or favicon, store a public:// URI so the theme system can handle it.
if (!empty($values['logo_path'])) {
$values['logo_path'] = $this->validatePath($values['logo_path']);
}
if (!empty($values['favicon_path'])) {
$values['favicon_path'] = $this->validatePath($values['favicon_path']);
}
if (empty($values['default_favicon']) && !empty($values['favicon_path'])) {
$values['favicon_mimetype'] = $this->mimeTypeGuesser->guess($values['favicon_path']);
}
}
theme_settings_convert_to_config($values, $config)->save();
}
/**
* Helper function for the system_theme_settings form.
*
* Attempts to validate normal system paths, paths relative to the public files
* directory, or stream wrapper URIs. If the given path is any of the above,
* returns a valid path or URI that the theme system can display.
*
* @param string $path
* A path relative to the Drupal root or to the public files directory, or
* a stream wrapper URI.
* @return mixed
* A valid path that can be displayed through the theme system, or FALSE if
* the path could not be validated.
*/
protected function validatePath($path) {
// Absolute local file paths are invalid.
if (drupal_realpath($path) == $path) {
return FALSE;
}
// A path relative to the Drupal root or a fully qualified URI is valid.
if (is_file($path)) {
return $path;
}
// Prepend 'public://' for relative file paths within public filesystem.
if (file_uri_scheme($path) === FALSE) {
$path = 'public://' . $path;
}
if (is_file($path)) {
return $path;
}
return FALSE;
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains \Drupal\system\MachineNameController.
*/
namespace Drupal\system;
use Drupal\Component\Transliteration\TransliterationInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controller routines for machine name transliteration routes.
*/
class MachineNameController implements ContainerInjectionInterface {
/**
* The transliteration helper.
*
* @var \Drupal\Component\Transliteration\TransliterationInterface
*/
protected $transliteration;
/**
* Constructs a MachineNameController object.
*
* @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration
* The transliteration helper.
*/
public function __construct(TransliterationInterface $transliteration) {
$this->transliteration = $transliteration;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('transliteration')
);
}
/**
* Transliterates a string in given language. Various postprocessing possible.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The input string and language for the transliteration.
* Optionally may contain the replace_pattern, replace, lowercase parameters.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The transliterated string.
*/
public function transliterate(Request $request) {
$text = $request->query->get('text');
$langcode = $request->query->get('langcode');
$replace_pattern = $request->query->get('replace_pattern');
$replace = $request->query->get('replace');
$lowercase = $request->query->get('lowercase');
$transliterated = $this->transliteration->transliterate($text, $langcode, '_');
if($lowercase) {
$transliterated = Unicode::strtolower($transliterated);
}
if(isset($replace_pattern) && isset($replace)) {
$transliterated = preg_replace('@' . $replace_pattern . '@', $replace, $transliterated);
}
return new JsonResponse($transliterated);
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\system\MenuAccessControlHandler.
*/
namespace Drupal\system;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the menu entity type.
*
* @see \Drupal\system\Entity\Menu
*/
class MenuAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
if ($operation === 'view') {
return AccessResult::allowed();
}
// Locked menus could not be deleted.
elseif ($operation == 'delete') {
if ($entity->isLocked()) {
return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
}
else {
return parent::checkAccess($entity, $operation, $langcode, $account)->cacheUntilEntityChanges($entity);
}
}
return parent::checkAccess($entity, $operation, $langcode, $account);
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\system\MenuInterface.
*/
namespace Drupal\system;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a menu entity.
*/
interface MenuInterface extends ConfigEntityInterface {
/**
* Returns the description of the menu.
*
* @return string
* Description of the menu.
*/
public function getDescription();
/**
* Determines if this menu is locked.
*
* @return bool
* TRUE if the menu is locked, FALSE otherwise.
*/
public function isLocked();
}

View file

@ -0,0 +1,218 @@
<?php
/**
* @file
* Contains \Drupal\system\PathBasedBreadcrumbBuilder.
*/
namespace Drupal\system;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Link;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Routing\RequestContext;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
/**
* Class to define the menu_link breadcrumb builder.
*/
class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
use StringTranslationTrait;
/**
* The router request context.
*
* @var \Drupal\Core\Routing\RequestContext
*/
protected $context;
/**
* The menu link access service.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* The dynamic router service.
*
* @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
*/
protected $router;
/**
* The dynamic router service.
*
* @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
*/
protected $pathProcessor;
/**
* Site config object.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The title resolver.
*
* @var \Drupal\Core\Controller\TitleResolverInterface
*/
protected $titleResolver;
/**
* The current user object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs the PathBasedBreadcrumbBuilder.
*
* @param \Drupal\Core\Routing\RequestContext $context
* The router request context.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The menu link access service.
* @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $router
* The dynamic router service.
* @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor
* The inbound path processor.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
* The title resolver service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user object.
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* The current path.
*/
public function __construct(RequestContext $context, AccessManagerInterface $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, ConfigFactoryInterface $config_factory, TitleResolverInterface $title_resolver, AccountInterface $current_user, CurrentPathStack $current_path) {
$this->context = $context;
$this->accessManager = $access_manager;
$this->router = $router;
$this->pathProcessor = $path_processor;
$this->config = $config_factory->get('system.site');
$this->titleResolver = $title_resolver;
$this->currentUser = $current_user;
$this->currentPath = $current_path;
}
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$links = array();
// General path-based breadcrumbs. Use the actual request path, prior to
// resolving path aliases, so the breadcrumb can be defined by simply
// creating a hierarchy of path aliases.
$path = trim($this->context->getPathInfo(), '/');
$path_elements = explode('/', $path);
$exclude = array();
// Don't show a link to the front-page path.
$front = $this->config->get('page.front');
$exclude[$front] = TRUE;
// /user is just a redirect, so skip it.
// @todo Find a better way to deal with /user.
$exclude['/user'] = TRUE;
while (count($path_elements) > 1) {
array_pop($path_elements);
// Copy the path elements for up-casting.
$route_request = $this->getRequestForPath('/' . implode('/', $path_elements), $exclude);
if ($route_request) {
$route_match = RouteMatch::createFromRequest($route_request);
$access = $this->accessManager->check($route_match, $this->currentUser);
if ($access) {
$title = $this->titleResolver->getTitle($route_request, $route_match->getRouteObject());
}
if ($access) {
if (!isset($title)) {
// Fallback to using the raw path component as the title if the
// route is missing a _title or _title_callback attribute.
$title = str_replace(array('-', '_'), ' ', Unicode::ucfirst(end($path_elements)));
}
$url = Url::fromRouteMatch($route_match);
$links[] = new Link($title, $url);
}
}
}
if ($path && '/' . $path != $front) {
// Add the Home link, except for the front page.
$links[] = Link::createFromRoute($this->t('Home'), '<front>');
}
return array_reverse($links);
}
/**
* Matches a path in the router.
*
* @param string $path
* The request path with a leading slash.
* @param array $exclude
* An array of paths or system paths to skip.
*
* @return \Symfony\Component\HttpFoundation\Request
* A populated request object or NULL if the path couldn't be matched.
*/
protected function getRequestForPath($path, array $exclude) {
if (!empty($exclude[$path])) {
return NULL;
}
// @todo Use the RequestHelper once https://www.drupal.org/node/2090293 is
// fixed.
$request = Request::create($path);
// Performance optimization: set a short accept header to reduce overhead in
// AcceptHeaderMatcher when matching the request.
$request->headers->set('Accept', 'text/html');
// Find the system path by resolving aliases, language prefix, etc.
$processed = $this->pathProcessor->processInbound($path, $request);
if (empty($processed) || !empty($exclude[$processed])) {
// This resolves to the front page, which we already add.
return NULL;
}
$this->currentPath->setPath($processed, $request);
// Attempt to match this path to provide a fully built request.
try {
$request->attributes->add($this->router->matchRequest($request));
return $request;
}
catch (ParamNotConvertedException $e) {
return NULL;
}
catch (ResourceNotFoundException $e) {
return NULL;
}
catch (MethodNotAllowedException $e) {
return NULL;
}
catch (AccessDeniedHttpException $e) {
return NULL;
}
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\system\PathProcessor\PathProcessorFiles.
*/
namespace Drupal\system\PathProcessor;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a path processor to rewrite file URLs.
*
* As the route system does not allow arbitrary amount of parameters convert
* the file path to a query parameter on the request.
*/
class PathProcessorFiles implements InboundPathProcessorInterface {
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
if (strpos($path, '/system/files/') === 0 && !$request->query->has('file')) {
$file_path = preg_replace('|^\/system\/files\/|', '', $path);
$request->query->set('file', $file_path);
return '/system/files';
}
return $path;
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\system\PhpStorage\MockPhpStorage.
*/
namespace Drupal\system\PhpStorage;
/**
* Mock PHP storage class used for testing.
*/
class MockPhpStorage {
/**
* The storage configuration.
*
* @var array
*/
protected $configuration;
/**
* Constructs a MockPhpStorage object.
*
* @param array $configuration
*/
public function __construct(array $configuration) {
$this->configuration = $configuration;
}
/**
* Gets the configuration data.
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* Gets a single configuration key.
*/
public function getConfigurationValue($key) {
return isset($this->configuration[$key]) ? $this->configuration[$key] : NULL;
}
}

View file

@ -0,0 +1,23 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Archiver\Tar.
*/
namespace Drupal\system\Plugin\Archiver;
use Drupal\Core\Archiver\Tar as BaseTar;
/**
* Defines an archiver implementation for .tar files.
*
* @Archiver(
* id = "Tar",
* title = @Translation("Tar"),
* description = @Translation("Handles .tar files."),
* extensions = {"tar", "tgz", "tar.gz", "tar.bz2"}
* )
*/
class Tar extends BaseTar {
}

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Archiver\Zip.
*/
namespace Drupal\system\Plugin\Archiver;
use Drupal\Core\Archiver\Zip as BaseZip;
/**
* Defines an archiver implementation for .zip files.
*
* @link http://php.net/zip
*
* @Archiver(
* id = "Zip",
* title = @Translation("Zip"),
* description = @Translation("Handles zip files."),
* extensions = {"zip"}
* )
*/
class Zip extends BaseZip {
}

View file

@ -0,0 +1,193 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Block\SystemBrandingBlock.
*/
namespace Drupal\system\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block to display 'Site branding' elements.
*
* @Block(
* id = "system_branding_block",
* admin_label = @Translation("Site branding")
* )
*/
class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* Stores the configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Creates a SystemBrandingBlock instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array(
'use_site_logo' => TRUE,
'use_site_name' => TRUE,
'use_site_slogan' => TRUE,
'label_display' => FALSE,
);
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
// Get the theme.
$theme = $form_state->get('block_theme');
// Get permissions.
$url_system_theme_settings = new Url('system.theme_settings');
$url_system_theme_settings_theme = new Url('system.theme_settings_theme', array('theme' => $theme));
if ($url_system_theme_settings->access() && $url_system_theme_settings_theme->access()) {
// Provide links to the Appearance Settings and Theme Settings pages
// if the user has access to administer themes.
$site_logo_description = $this->t('Defined on the <a href="@appearance">Appearance Settings</a> or <a href="@theme">Theme Settings</a> page.', array(
'@appearance' => $url_system_theme_settings->toString(),
'@theme' => $url_system_theme_settings_theme->toString(),
));
}
else {
// Explain that the user does not have access to the Appearance and Theme
// Settings pages.
$site_logo_description = $this->t('Defined on the Appearance or Theme Settings page. You do not have the appropriate permissions to change the site logo.');
}
$url_system_site_information_settings = new Url('system.site_information_settings');
if ($url_system_site_information_settings->access()) {
// Get paths to settings pages.
$site_information_url = $url_system_site_information_settings->toString();
// Provide link to Site Information page if the user has access to
// administer site configuration.
$site_name_description = $this->t('Defined on the <a href="@information">Site Information</a> page.', array('@information' => $site_information_url));
$site_slogan_description = $this->t('Defined on the <a href="@information">Site Information</a> page.', array('@information' => $site_information_url));
}
else {
// Explain that the user does not have access to the Site Information
// page.
$site_name_description = $this->t('Defined on the Site Information page. You do not have the appropriate permissions to change the site logo.');
$site_slogan_description = $this->t('Defined on the Site Information page. You do not have the appropriate permissions to change the site logo.');
}
$form['block_branding'] = array(
'#type' => 'fieldset',
'#title' => $this->t('Toggle branding elements'),
'#description' => $this->t('Choose which branding elements you want to show in this block instance.'),
);
$form['block_branding']['use_site_logo'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Site logo'),
'#description' => $site_logo_description,
'#default_value' => $this->configuration['use_site_logo'],
);
$form['block_branding']['use_site_name'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Site name'),
'#description' => $site_name_description,
'#default_value' => $this->configuration['use_site_name'],
);
$form['block_branding']['use_site_slogan'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Site slogan'),
'#description' => $site_slogan_description,
'#default_value' => $this->configuration['use_site_slogan'],
);
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$block_branding = $form_state->getValue('block_branding');
$this->configuration['use_site_logo'] = $block_branding['use_site_logo'];
$this->configuration['use_site_name'] = $block_branding['use_site_name'];
$this->configuration['use_site_slogan'] = $block_branding['use_site_slogan'];
}
/**
* {@inheritdoc}
*/
public function build() {
$build = array();
$site_config = $this->configFactory->get('system.site');
$logo = theme_get_setting('logo');
$build['site_logo'] = array(
'#theme' => 'image',
'#uri' => $logo['url'],
'#alt' => $this->t('Home'),
'#access' => $this->configuration['use_site_logo'],
);
$build['site_name'] = array(
'#markup' => $site_config->get('name'),
'#access' => $this->configuration['use_site_name'],
);
$build['site_slogan'] = array(
'#markup' => Xss::filterAdmin($site_config->get('slogan')),
'#access' => $this->configuration['use_site_slogan'],
);
return $build;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(
parent::getCacheTags(),
$this->configFactory->get('system.site')->getCacheTags()
);
}
}

View file

@ -0,0 +1,96 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Block\SystemBreadcrumbBlock.
*/
namespace Drupal\system\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block to display the breadcrumbs.
*
* @Block(
* id = "system_breadcrumb_block",
* admin_label = @Translation("Breadcrumbs")
* )
*/
class SystemBreadcrumbBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The breadcrumb manager.
*
* @var \Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface
*/
protected $breadcrumbManager;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new SystemBreadcrumbBlock 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\Breadcrumb\BreadcrumbBuilderInterface $breadcrumb_manager
* The breadcrumb manager.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, BreadcrumbBuilderInterface $breadcrumb_manager, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->breadcrumbManager = $breadcrumb_manager;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('breadcrumb'),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function build() {
$breadcrumb = $this->breadcrumbManager->build($this->routeMatch);
if (!empty($breadcrumb)) {
// $breadcrumb is expected to be an array of rendered breadcrumb links.
return array(
'#theme' => 'breadcrumb',
'#links' => $breadcrumb,
);
}
}
/**
* {@inheritdoc}
*
* @todo Make cacheable in https://www.drupal.org/node/2483183
*/
public function getCacheMaxAge() {
return 0;
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Block\SystemMainBlock.
*/
namespace Drupal\system\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\MainContentBlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a 'Main page content' block.
*
* @Block(
* id = "system_main_block",
* admin_label = @Translation("Main page content")
* )
*/
class SystemMainBlock extends BlockBase implements MainContentBlockPluginInterface {
/**
* The render array representing the main page content.
*
* @var array
*/
protected $mainContent;
/**
* {@inheritdoc}
*/
public function setMainContent(array $main_content) {
$this->mainContent = $main_content;
}
/**
* {@inheritdoc}
*/
public function build() {
return $this->mainContent;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['cache']['#disabled'] = TRUE;
$form['cache']['#description'] = $this->t("This block's maximum age cannot be configured, because it depends on the contents.");
$form['cache']['max_age']['#value'] = Cache::PERMANENT;
return $form;
}
}

View file

@ -0,0 +1,202 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Block\SystemMenuBlock.
*/
namespace Drupal\system\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuActiveTrailInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a generic Menu block.
*
* @Block(
* id = "system_menu_block",
* admin_label = @Translation("Menu"),
* category = @Translation("Menus"),
* deriver = "Drupal\system\Plugin\Derivative\SystemMenuBlock"
* )
*/
class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuTree;
/**
* The active menu trail service.
*
* @var \Drupal\Core\Menu\MenuActiveTrailInterface
*/
protected $menuActiveTrail;
/**
* Constructs a new SystemMenuBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree
* The menu tree service.
* @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail
* The active menu trail service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $menu_active_trail) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->menuTree = $menu_tree;
$this->menuActiveTrail = $menu_active_trail;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('menu.link_tree'),
$container->get('menu.active_trail')
);
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$config = $this->configuration;
$defaults = $this->defaultConfiguration();
$form['menu_levels'] = array(
'#type' => 'details',
'#title' => $this->t('Menu levels'),
// Open if not set to defaults.
'#open' => $defaults['level'] !== $config['level'] || $defaults['depth'] !== $config['depth'],
'#process' => [[get_class(), 'processMenuLevelParents']],
);
$options = range(0, $this->menuTree->maxDepth());
unset($options[0]);
$form['menu_levels']['level'] = array(
'#type' => 'select',
'#title' => $this->t('Initial menu level'),
'#default_value' => $config['level'],
'#options' => $options,
'#description' => $this->t('The menu will only be visible if the menu item for the current page is at or below the selected starting level. Select level 1 to always keep this menu visible.'),
'#required' => TRUE,
);
$options[0] = $this->t('Unlimited');
$form['menu_levels']['depth'] = array(
'#type' => 'select',
'#title' => $this->t('Maximum number of menu levels to display'),
'#default_value' => $config['depth'],
'#options' => $options,
'#description' => $this->t('The maximum number of menu levels to show, starting from the initial menu level. For example: with an initial level 2 and a maximum number of 3, menu levels 2, 3 and 4 can be displayed.'),
'#required' => TRUE,
);
return $form;
}
/**
* Form API callback: Processes the menu_levels field element.
*
* Adjusts the #parents of menu_levels to save its children at the top level.
*/
public static function processMenuLevelParents(&$element, FormStateInterface $form_state, &$complete_form) {
array_pop($element['#parents']);
return $element;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['level'] = $form_state->getValue('level');
$this->configuration['depth'] = $form_state->getValue('depth');
}
/**
* {@inheritdoc}
*/
public function build() {
$menu_name = $this->getDerivativeId();
$parameters = $this->menuTree->getCurrentRouteMenuTreeParameters($menu_name);
// Adjust the menu tree parameters based on the block's configuration.
$level = $this->configuration['level'];
$depth = $this->configuration['depth'];
$parameters->setMinDepth($level);
// When the depth is configured to zero, there is no depth limit. When depth
// is non-zero, it indicates the number of levels that must be displayed.
// Hence this is a relative depth that we must convert to an actual
// (absolute) depth, that may never exceed the maximum depth.
if ($depth > 0) {
$parameters->setMaxDepth(min($level + $depth - 1, $this->menuTree->maxDepth()));
}
$tree = $this->menuTree->load($menu_name, $parameters);
$manipulators = array(
array('callable' => 'menu.default_tree_manipulators:checkAccess'),
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
);
$tree = $this->menuTree->transform($tree, $manipulators);
return $this->menuTree->build($tree);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'level' => 1,
'depth' => 0,
];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
// Even when the menu block renders to the empty string for a user, we want
// the cache tag for this menu to be set: whenever the menu is changed, this
// menu block must also be re-rendered for that user, because maybe a menu
// link that is accessible for that user has been added.
$cache_tags = parent::getCacheTags();
$cache_tags[] = 'config:system.menu.' . $this->getDerivativeId();
return $cache_tags;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
// ::build() uses MenuLinkTreeInterface::getCurrentRouteMenuTreeParameters()
// to generate menu tree parameters, and those take the active menu trail
// into account. Therefore, we must vary the rendered menu by the active
// trail of the rendered menu.
// Additional cache contexts, e.g. those that determine link text or
// accessibility of a menu, will be bubbled automatically.
$menu_name = $this->getDerivativeId();
return [
'route.menu_active_trails:' . $menu_name,
];
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Block\SystemMessagesBlock.
*/
namespace Drupal\system\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\MessagesBlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a block to display the messages.
*
* @see drupal_set_message()
*
* @Block(
* id = "system_messages_block",
* admin_label = @Translation("Messages")
* )
*/
class SystemMessagesBlock extends BlockBase implements MessagesBlockPluginInterface {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array(
'label_display' => FALSE,
);
}
/**
* {@inheritdoc}
*/
public function build() {
return ['#type' => 'status_messages'];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// @see ::getCacheMaxAge()
$form['cache']['#description'] = $this->t('This block is cacheable forever, it is not configurable.');
$form['cache']['max_age']['#value'] = Cache::PERMANENT;
$form['cache']['max_age']['#disabled'] = TRUE;
return $form;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// The messages are session-specific and hence aren't cacheable, but the
// block itself *is* cacheable because it uses a #lazy_builder callback and
// hence the block has a globally cacheable render array.
return Cache::PERMANENT;
}
}

View file

@ -0,0 +1,54 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Block\SystemPoweredByBlock.
*/
namespace Drupal\system\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a 'Powered by Drupal' block.
*
* @Block(
* id = "system_powered_by_block",
* admin_label = @Translation("Powered by Drupal")
* )
*/
class SystemPoweredByBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return array('#markup' => '<span>' . $this->t('Powered by <a href="@poweredby">Drupal</a>', array('@poweredby' => 'https://www.drupal.org')) . '</span>');
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// @see ::getCacheMaxAge()
$form['cache']['#disabled'] = TRUE;
$form['cache']['max_age']['#value'] = Cache::PERMANENT;
$form['cache']['#description'] = $this->t('This block is always cached forever, it is not configurable.');
return $form;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// The 'Powered by Drupal' block is permanently cacheable, because its
// contents can never change.
return Cache::PERMANENT;
}
}

View file

@ -0,0 +1,126 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Condition\CurrentThemeCondition.
*/
namespace Drupal\system\Plugin\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'Current Theme' condition.
*
* @Condition(
* id = "current_theme",
* label = @Translation("Current Theme"),
* )
*/
class CurrentThemeCondition extends ConditionPluginBase implements ContainerFactoryPluginInterface {
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs a CurrentThemeCondition condition plugin.
*
* @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\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ThemeManagerInterface $theme_manager, ThemeHandlerInterface $theme_handler) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->themeManager = $theme_manager;
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('theme.manager'),
$container->get('theme_handler')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array('theme' => '') + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['theme'] = array(
'#type' => 'select',
'#title' => $this->t('Theme'),
'#default_value' => $this->configuration['theme'],
'#options' => array_map(function ($theme_info) {
return $theme_info->info['name'];
}, $this->themeHandler->listInfo()),
);
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['theme'] = $form_state->getValue('theme');
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function evaluate() {
if (!$this->configuration['theme']) {
return TRUE;
}
return $this->themeManager->getActiveTheme()->getName() == $this->configuration['theme'];
}
/**
* {@inheritdoc}
*/
public function summary() {
if ($this->isNegated()) {
return $this->t('The current theme is not @theme', array('@theme' => $this->configuration['theme']));
}
return $this->t('The current theme is @theme', array('@theme' => $this->configuration['theme']));
}
}

View file

@ -0,0 +1,162 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Condition\RequestPath.
*/
namespace Drupal\system\Plugin\Condition;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Path\PathMatcherInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides a 'Request Path' condition.
*
* @Condition(
* id = "request_path",
* label = @Translation("Request Path"),
* )
*/
class RequestPath extends ConditionPluginBase implements ContainerFactoryPluginInterface {
/**
* An alias manager to find the alias for the current system path.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* The path matcher.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected $pathMatcher;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The current path.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* Constructs a RequestPath condition plugin.
*
* @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
* An alias manager to find the alias for the current system path.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* The path matcher service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* The current path.
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
*/
public function __construct(AliasManagerInterface $alias_manager, PathMatcherInterface $path_matcher, RequestStack $request_stack, CurrentPathStack $current_path, array $configuration, $plugin_id, array $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->aliasManager = $alias_manager;
$this->pathMatcher = $path_matcher;
$this->requestStack = $request_stack;
$this->currentPath = $current_path;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('path.alias_manager'),
$container->get('path.matcher'),
$container->get('request_stack'),
$container->get('path.current'),
$configuration,
$plugin_id,
$plugin_definition);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array('pages' => '') + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['pages'] = array(
'#type' => 'textarea',
'#title' => $this->t('Pages'),
'#default_value' => $this->configuration['pages'],
'#description' => $this->t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %user for the current user's page and %user-wildcard for every user page. %front is the front page.", array(
'%user' => 'user',
'%user-wildcard' => 'user/*',
'%front' => '<front>',
)),
);
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['pages'] = $form_state->getValue('pages');
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function summary() {
$pages = array_map('trim', explode("\n", $this->configuration['pages']));
$pages = implode(', ', $pages);
if (!empty($this->configuration['negate'])) {
return $this->t('Do not return true on the following pages: @pages', array('@pages' => $pages));
}
return $this->t('Return true on the following pages: @pages', array('@pages' => $pages));
}
/**
* {@inheritdoc}
*/
public function evaluate() {
// Convert path to lowercase. This allows comparison of the same path
// with different case. Ex: /Page, /page, /PAGE.
$pages = Unicode::strtolower($this->configuration['pages']);
if (!$pages) {
return TRUE;
}
$request = $this->requestStack->getCurrentRequest();
// Compare the lowercase path alias (if any) and internal path.
$path = rtrim($this->currentPath->getPath($request), '/');
$path_alias = Unicode::strtolower($this->aliasManager->getAliasByPath($path));
return $this->pathMatcher->matchPath($path_alias, $pages) || (($path != $path_alias) && $this->pathMatcher->matchPath($path, $pages));
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Derivative\SystemMenuBlock.
*/
namespace Drupal\system\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides block plugin definitions for custom menus.
*
* @see \Drupal\system\Plugin\Block\SystemMenuBlock
*/
class SystemMenuBlock extends DeriverBase implements ContainerDeriverInterface {
/**
* The menu storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $menuStorage;
/**
* Constructs new SystemMenuBlock.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $menu_storage
* The menu storage.
*/
public function __construct(EntityStorageInterface $menu_storage) {
$this->menuStorage = $menu_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity.manager')->getStorage('menu')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->menuStorage->loadMultiple() as $menu => $entity) {
$this->derivatives[$menu] = $base_plugin_definition;
$this->derivatives[$menu]['admin_label'] = $entity->label();
$this->derivatives[$menu]['config_dependencies']['config'] = array($entity->getConfigDependencyName());
}
return $this->derivatives;
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Derivative\ThemeLocalTask.
*/
namespace Drupal\system\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic tabs based on active themes.
*/
class ThemeLocalTask extends DeriverBase implements ContainerDeriverInterface {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs a new ThemeLocalTask instance.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('theme_handler')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->themeHandler->listInfo() as $theme_name => $theme) {
if ($theme->status) {
$this->derivatives[$theme_name] = $base_plugin_definition;
$this->derivatives[$theme_name]['title'] = $theme->info['name'];
$this->derivatives[$theme_name]['route_parameters'] = array('theme' => $theme_name);
}
}
return $this->derivatives;
}
}

View file

@ -0,0 +1,388 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\GDToolkit.
*/
namespace Drupal\system\Plugin\ImageToolkit;
use Drupal\Component\Utility\Color;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\ImageToolkit\ImageToolkitBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
/**
* Defines the GD2 toolkit for image manipulation within Drupal.
*
* @ImageToolkit(
* id = "gd",
* title = @Translation("GD2 image manipulation toolkit")
* )
*/
class GDToolkit extends ImageToolkitBase {
/**
* A GD image resource.
*
* @var resource|null
*/
protected $resource = NULL;
/**
* Image type represented by a PHP IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG).
*
* @var int
*/
protected $type;
/**
* Image information from a file, available prior to loading the GD resource.
*
* This contains a copy of the array returned by executing getimagesize()
* on the image file when the image object is instantiated. It gets reset
* to NULL as soon as the GD resource is loaded.
*
* @var array|null
*
* @see \Drupal\system\Plugin\ImageToolkit\GDToolkit::parseFile()
* @see \Drupal\system\Plugin\ImageToolkit\GDToolkit::setResource()
* @see http://php.net/manual/en/function.getimagesize.php
*/
protected $preLoadInfo = NULL;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('image.toolkit.operation.manager'),
$container->get('logger.channel.image'),
$container->get('config.factory')
);
}
/**
* Sets the GD image resource.
*
* @param resource $resource
* The GD image resource.
*
* @return $this
*/
public function setResource($resource) {
$this->preLoadInfo = NULL;
$this->resource = $resource;
return $this;
}
/**
* Retrieves the GD image resource.
*
* @return resource|null
* The GD image resource, or NULL if not available.
*/
public function getResource() {
if (!$this->resource) {
$this->load();
}
return $this->resource;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['image_jpeg_quality'] = array(
'#type' => 'number',
'#title' => t('JPEG quality'),
'#description' => t('Define the image quality for JPEG manipulations. Ranges from 0 to 100. Higher values mean better image quality but bigger files.'),
'#min' => 0,
'#max' => 100,
'#default_value' => $this->configFactory->getEditable('system.image.gd')->get('jpeg_quality', FALSE),
'#field_suffix' => t('%'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configFactory->getEditable('system.image.gd')
->set('jpeg_quality', $form_state->getValue(array('gd', 'image_jpeg_quality')))
->save();
}
/**
* Loads a GD resource from a file.
*
* @return bool
* TRUE or FALSE, based on success.
*/
protected function load() {
// Return immediately if the image file is not valid.
if (!$this->isValid()) {
return FALSE;
}
$function = 'imagecreatefrom' . image_type_to_extension($this->getType(), FALSE);
if (function_exists($function) && $resource = $function($this->getImage()->getSource())) {
$this->setResource($resource);
if (imageistruecolor($resource)) {
return TRUE;
}
else {
// Convert indexed images to truecolor, copying the image to a new
// truecolor resource, so that filters work correctly and don't result
// in unnecessary dither.
$data = array(
'width' => imagesx($resource),
'height' => imagesy($resource),
'extension' => image_type_to_extension($this->getType(), FALSE),
'transparent_color' => $this->getTransparentColor(),
);
if ($this->apply('create_new', $data)) {
imagecopy($this->getResource(), $resource, 0, 0, 0, 0, imagesx($resource), imagesy($resource));
imagedestroy($resource);
}
}
return (bool) $this->getResource();
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function isValid() {
return ((bool) $this->preLoadInfo || (bool) $this->resource);
}
/**
* {@inheritdoc}
*/
public function save($destination) {
$scheme = file_uri_scheme($destination);
// Work around lack of stream wrapper support in imagejpeg() and imagepng().
if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
// If destination is not local, save image to temporary local file.
$local_wrappers = file_get_stream_wrappers(StreamWrapperInterface::LOCAL);
if (!isset($local_wrappers[$scheme])) {
$permanent_destination = $destination;
$destination = drupal_tempnam('temporary://', 'gd_');
}
// Convert stream wrapper URI to normal path.
$destination = drupal_realpath($destination);
}
$function = 'image' . image_type_to_extension($this->getType(), FALSE);
if (!function_exists($function)) {
return FALSE;
}
if ($this->getType() == IMAGETYPE_JPEG) {
$success = $function($this->getResource(), $destination, $this->configFactory->get('system.image.gd')->get('jpeg_quality'));
}
else {
// Always save PNG images with full transparency.
if ($this->getType() == IMAGETYPE_PNG) {
imagealphablending($this->getResource(), FALSE);
imagesavealpha($this->getResource(), TRUE);
}
$success = $function($this->getResource(), $destination);
}
// Move temporary local file to remote destination.
if (isset($permanent_destination) && $success) {
return (bool) file_unmanaged_move($destination, $permanent_destination, FILE_EXISTS_REPLACE);
}
return $success;
}
/**
* {@inheritdoc}
*/
public function parseFile() {
$data = @getimagesize($this->getImage()->getSource());
if ($data && in_array($data[2], static::supportedTypes())) {
$this->setType($data[2]);
$this->preLoadInfo = $data;
return TRUE;
}
return FALSE;
}
/**
* Gets the color set for transparency in GIF images.
*
* @return string|null
* A color string like '#rrggbb', or NULL if not set or not relevant.
*/
public function getTransparentColor() {
if (!$this->getResource() || $this->getType() != IMAGETYPE_GIF) {
return NULL;
}
// Find out if a transparent color is set, will return -1 if no
// transparent color has been defined in the image.
$transparent = imagecolortransparent($this->getResource());
if ($transparent >= 0) {
// Find out the number of colors in the image palette. It will be 0 for
// truecolor images.
$palette_size = imagecolorstotal($this->getResource());
if ($palette_size == 0 || $transparent < $palette_size) {
// Return the transparent color, either if it is a truecolor image
// or if the transparent color is part of the palette.
// Since the index of the transparent color is a property of the
// image rather than of the palette, it is possible that an image
// could be created with this index set outside the palette size.
// (see http://stackoverflow.com/a/3898007).
$rgb = imagecolorsforindex($this->getResource(), $transparent);
unset($rgb['alpha']);
return Color::rgbToHex($rgb);
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getWidth() {
if ($this->preLoadInfo) {
return $this->preLoadInfo[0];
}
elseif ($res = $this->getResource()) {
return imagesx($res);
}
else {
return NULL;
}
}
/**
* {@inheritdoc}
*/
public function getHeight() {
if ($this->preLoadInfo) {
return $this->preLoadInfo[1];
}
elseif ($res = $this->getResource()) {
return imagesy($res);
}
else {
return NULL;
}
}
/**
* Gets the PHP type of the image.
*
* @return int
* The image type represented by a PHP IMAGETYPE_* constant (e.g.
* IMAGETYPE_JPEG).
*/
public function getType() {
return $this->type;
}
/**
* Sets the PHP type of the image.
*
* @param int $type
* The image type represented by a PHP IMAGETYPE_* constant (e.g.
* IMAGETYPE_JPEG).
*
* @return $this
*/
public function setType($type) {
if (in_array($type, static::supportedTypes())) {
$this->type = $type;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getMimeType() {
return $this->getType() ? image_type_to_mime_type($this->getType()) : '';
}
/**
* {@inheritdoc}
*/
public function getRequirements() {
$requirements = array();
$info = gd_info();
$requirements['version'] = array(
'title' => t('GD library'),
'value' => $info['GD Version'],
);
// Check for filter and rotate support.
if (!function_exists('imagefilter') || !function_exists('imagerotate')) {
$requirements['version']['severity'] = REQUIREMENT_WARNING;
$requirements['version']['description'] = t('The GD Library for PHP is enabled, but was compiled without support for functions used by the rotate and desaturate effects. It was probably compiled using the official GD libraries from http://www.libgd.org instead of the GD library bundled with PHP. You should recompile PHP --with-gd using the bundled GD library. See <a href="@url">the PHP manual</a>.', array('@url' => 'http://www.php.net/manual/book.image.php'));
}
return $requirements;
}
/**
* {@inheritdoc}
*/
public static function isAvailable() {
// GD2 support is available.
return function_exists('imagegd2');
}
/**
* {@inheritdoc}
*/
public static function getSupportedExtensions() {
$extensions = array();
foreach (static::supportedTypes() as $image_type) {
$extensions[] = Unicode::strtolower(image_type_to_extension($image_type, FALSE));
}
return $extensions;
}
/**
* Returns the IMAGETYPE_xxx constant for the given extension.
*
* This is the reverse of the image_type_to_extension() function.
*
* @param string $extension
* The extension to get the IMAGETYPE_xxx constant for.
*
* @return int
* The IMAGETYPE_xxx constant for the given extension, or IMAGETYPE_UNKNOWN
* for unsupported extensions.
*
* @see image_type_to_extension()
*/
public function extensionToImageType($extension) {
foreach ($this->supportedTypes() as $type) {
if (image_type_to_extension($type, FALSE) === $extension) {
return $type;
}
}
return IMAGETYPE_UNKNOWN;
}
/**
* Returns a list of image types supported by the toolkit.
*
* @return array
* An array of available image types. An image type is represented by a PHP
* IMAGETYPE_* constant (e.g. IMAGETYPE_JPEG, IMAGETYPE_PNG, etc.).
*/
protected static function supportedTypes() {
return array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF);
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\Convert.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines GD2 convert operation.
*
* @ImageToolkitOperation(
* id = "gd_convert",
* toolkit = "gd",
* operation = "convert",
* label = @Translation("Convert"),
* description = @Translation("Instructs the toolkit to save the image with a specified extension.")
* )
*/
class Convert extends GDImageToolkitOperationBase {
/**
* {@inheritdoc}
*/
protected function arguments() {
return array(
'extension' => array(
'description' => 'The new extension of the converted image',
),
);
}
/**
* {@inheritdoc}
*/
protected function validateArguments(array $arguments) {
if (!in_array($arguments['extension'], $this->getToolkit()->getSupportedExtensions())) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid extension (@value) specified for the image 'convert' operation", array('@value' => $arguments['extension'])));
}
return $arguments;
}
/**
* {@inheritdoc}
*/
protected function execute(array $arguments) {
// Create a new resource of the required dimensions and format, and copy
// the original resource on it with resampling. Destroy the original
// resource upon success.
$width = $this->getToolkit()->getWidth();
$height = $this->getToolkit()->getHeight();
$original_resource = $this->getToolkit()->getResource();
$original_type = $this->getToolkit()->getType();
$data = array(
'width' => $width,
'height' => $height,
'extension' => $arguments['extension'],
'transparent_color' => $this->getToolkit()->getTransparentColor()
);
if ($this->getToolkit()->apply('create_new', $data)) {
if (imagecopyresampled($this->getToolkit()->getResource(), $original_resource, 0, 0, 0, 0, $width, $height, $width, $height)) {
imagedestroy($original_resource);
return TRUE;
}
// In case of error, reset resource and type to as it was.
$this->getToolkit()->setResource($original_resource);
$this->getToolkit()->setType($original_type);
}
return FALSE;
}
}

View file

@ -0,0 +1,126 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\CreateNew.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
use Drupal\Component\Utility\Color;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines GD2 create_new image operation.
*
* @ImageToolkitOperation(
* id = "gd_create_new",
* toolkit = "gd",
* operation = "create_new",
* label = @Translation("Set a new image"),
* description = @Translation("Creates a new transparent resource and sets it for the image.")
* )
*/
class CreateNew extends GDImageToolkitOperationBase {
/**
* {@inheritdoc}
*/
protected function arguments() {
return array(
'width' => array(
'description' => 'The width of the image, in pixels',
),
'height' => array(
'description' => 'The height of the image, in pixels',
),
'extension' => array(
'description' => 'The extension of the image file (e.g. png, gif, etc.)',
'required' => FALSE,
'default' => 'png',
),
'transparent_color' => array(
'description' => 'The RGB hex color for GIF transparency',
'required' => FALSE,
'default' => '#ffffff',
),
);
}
/**
* {@inheritdoc}
*/
protected function validateArguments(array $arguments) {
// Assure extension is supported.
if (!in_array($arguments['extension'], $this->getToolkit()->getSupportedExtensions())) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid extension (@value) specified for the image 'convert' operation", array('@value' => $arguments['extension'])));
}
// Assure integers for width and height.
$arguments['width'] = (int) round($arguments['width']);
$arguments['height'] = (int) round($arguments['height']);
// Fail when width or height are 0 or negative.
if ($arguments['width'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid width (@value) specified for the image 'create_new' operation", array('@value' => $arguments['width'])));
}
if ($arguments['height'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid height (@value) specified for the image 'create_new' operation", array('@value' => $arguments['height'])));
}
// Assure transparent color is a valid hex string.
if ($arguments['transparent_color'] && !Color::validateHex($arguments['transparent_color'])) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid transparent color (@value) specified for the image 'create_new' operation", array('@value' => $arguments['transparent_color'])));
}
return $arguments;
}
/**
* {@inheritdoc}
*/
protected function execute(array $arguments) {
// Get the image type.
$type = $this->getToolkit()->extensionToImageType($arguments['extension']);
// Create the resource.
if (!$res = imagecreatetruecolor($arguments['width'], $arguments['height'])) {
return FALSE;
}
// Fill the resource with transparency as possible.
switch ($type) {
case IMAGETYPE_PNG:
imagealphablending($res, FALSE);
$transparency = imagecolorallocatealpha($res, 0, 0, 0, 127);
imagefill($res, 0, 0, $transparency);
imagealphablending($res, TRUE);
imagesavealpha($res, TRUE);
break;
case IMAGETYPE_GIF:
if (empty($arguments['transparent_color'])) {
// No transparency color specified, fill white.
$fill_color = imagecolorallocate($res, 255, 255, 255);
}
else {
$fill_rgb = Color::hexToRgb($arguments['transparent_color']);
$fill_color = imagecolorallocate($res, $fill_rgb['red'], $fill_rgb['green'], $fill_rgb['blue']);
imagecolortransparent($res, $fill_color);
}
imagefill($res, 0, 0, $fill_color);
break;
case IMAGETYPE_JPEG:
imagefill($res, 0, 0, imagecolorallocate($res, 255, 255, 255));
break;
}
// Update the toolkit properties.
$this->getToolkit()->setType($type);
$this->getToolkit()->setResource($res);
return TRUE;
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\Crop.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines GD2 Crop operation.
*
* @ImageToolkitOperation(
* id = "gd_crop",
* toolkit = "gd",
* operation = "crop",
* label = @Translation("Crop"),
* description = @Translation("Crops an image to a rectangle specified by the given dimensions.")
* )
*/
class Crop extends GDImageToolkitOperationBase {
/**
* {@inheritdoc}
*/
protected function arguments() {
return array(
'x' => array(
'description' => 'The starting x offset at which to start the crop, in pixels',
),
'y' => array(
'description' => 'The starting y offset at which to start the crop, in pixels',
),
'width' => array(
'description' => 'The width of the cropped area, in pixels',
'required' => FALSE,
'default' => NULL,
),
'height' => array(
'description' => 'The height of the cropped area, in pixels',
'required' => FALSE,
'default' => NULL,
),
);
}
/**
* {@inheritdoc}
*/
protected function validateArguments(array $arguments) {
// Assure at least one dimension.
if (empty($arguments['width']) && empty($arguments['height'])) {
throw new \InvalidArgumentException("At least one dimension ('width' or 'height') must be provided to the image 'crop' operation");
}
// Preserve aspect.
$aspect = $this->getToolkit()->getHeight() / $this->getToolkit()->getWidth();
$arguments['height'] = empty($arguments['height']) ? $arguments['width'] * $aspect : $arguments['height'];
$arguments['width'] = empty($arguments['width']) ? $arguments['height'] / $aspect : $arguments['width'];
// Assure integers for all arguments.
foreach (array('x', 'y', 'width', 'height') as $key) {
$arguments[$key] = (int) round($arguments[$key]);
}
// Fail when width or height are 0 or negative.
if ($arguments['width'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid width (@value) specified for the image 'crop' operation", array('@value' => $arguments['width'])));
}
if ($arguments['height'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid height (@value) specified for the image 'crop' operation", array('@value' => $arguments['height'])));
}
return $arguments;
}
/**
* {@inheritdoc}
*/
protected function execute(array $arguments) {
// Create a new resource of the required dimensions, and copy and resize
// the original resource on it with resampling. Destroy the original
// resource upon success.
$original_resource = $this->getToolkit()->getResource();
$data = array(
'width' => $arguments['width'],
'height' => $arguments['height'],
'extension' => image_type_to_extension($this->getToolkit()->getType(), FALSE),
'transparent_color' => $this->getToolkit()->getTransparentColor()
);
if ($this->getToolkit()->apply('create_new', $data)) {
if (imagecopyresampled($this->getToolkit()->getResource(), $original_resource, 0, 0, $arguments['x'], $arguments['y'], $arguments['width'], $arguments['height'], $arguments['width'], $arguments['height'])) {
imagedestroy($original_resource);
return TRUE;
}
}
return FALSE;
}
}

View file

@ -0,0 +1,44 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\Desaturate.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
/**
* Defines GD2 Desaturate operation.
*
* @ImageToolkitOperation(
* id = "gd_desaturate",
* toolkit = "gd",
* operation = "desaturate",
* label = @Translation("Desaturate"),
* description = @Translation("Converts an image to grayscale.")
* )
*/
class Desaturate extends GDImageToolkitOperationBase {
/**
* {@inheritdoc}
*/
protected function arguments() {
// This operation does not use any parameters.
return array();
}
/**
* {@inheritdoc}
*/
protected function execute(array $arguments) {
// PHP installations using non-bundled GD do not have imagefilter.
if (!function_exists('imagefilter')) {
$this->logger->notice("The image '@file' could not be desaturated because the imagefilter() function is not available in this PHP installation.", array('@file' => $this->getToolkit()->getImage()->getSource()));
return FALSE;
}
return imagefilter($this->getToolkit()->getResource(), IMG_FILTER_GRAYSCALE);
}
}

View file

@ -0,0 +1,23 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\GDImageToolkitOperationBase.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
use Drupal\Core\ImageToolkit\ImageToolkitOperationBase;
abstract class GDImageToolkitOperationBase extends ImageToolkitOperationBase {
/**
* The correctly typed image toolkit for GD operations.
*
* @return \Drupal\system\Plugin\ImageToolkit\GDToolkit
*/
protected function getToolkit() {
return parent::getToolkit();
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\Resize.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines GD2 resize operation.
*
* @ImageToolkitOperation(
* id = "gd_resize",
* toolkit = "gd",
* operation = "resize",
* label = @Translation("Resize"),
* description = @Translation("Resizes an image to the given dimensions (ignoring aspect ratio).")
* )
*/
class Resize extends GDImageToolkitOperationBase {
/**
* {@inheritdoc}
*/
protected function arguments() {
return array(
'width' => array(
'description' => 'The new width of the resized image, in pixels',
),
'height' => array(
'description' => 'The new height of the resized image, in pixels',
),
);
}
/**
* {@inheritdoc}
*/
protected function validateArguments(array $arguments) {
// Assure integers for all arguments.
$arguments['width'] = (int) round($arguments['width']);
$arguments['height'] = (int) round($arguments['height']);
// Fail when width or height are 0 or negative.
if ($arguments['width'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid width (@value) specified for the image 'resize' operation", array('@value' => $arguments['width'])));
}
if ($arguments['height'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid height (@value) specified for the image 'resize' operation", array('@value' => $arguments['height'])));
}
return $arguments;
}
/**
* {@inheritdoc}
*/
protected function execute(array $arguments = array()) {
// Create a new resource of the required dimensions, and copy and resize
// the original resource on it with resampling. Destroy the original
// resource upon success.
$original_resource = $this->getToolkit()->getResource();
$data = array(
'width' => $arguments['width'],
'height' => $arguments['height'],
'extension' => image_type_to_extension($this->getToolkit()->getType(), FALSE),
'transparent_color' => $this->getToolkit()->getTransparentColor()
);
if ($this->getToolkit()->apply('create_new', $data)) {
if (imagecopyresampled($this->getToolkit()->getResource(), $original_resource, 0, 0, 0, 0, $arguments['width'], $arguments['height'], imagesx($original_resource), imagesy($original_resource))) {
imagedestroy($original_resource);
return TRUE;
}
}
return FALSE;
}
}

View file

@ -0,0 +1,114 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\Rotate.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
use Drupal\Component\Utility\Color;
/**
* Defines GD2 rotate operation.
*
* @ImageToolkitOperation(
* id = "gd_rotate",
* toolkit = "gd",
* operation = "rotate",
* label = @Translation("Rotate"),
* description = @Translation("Rotates an image by the given number of degrees.")
* )
*/
class Rotate extends GDImageToolkitOperationBase {
/**
* {@inheritdoc}
*/
protected function arguments() {
return array(
'degrees' => array(
'description' => 'The number of (clockwise) degrees to rotate the image',
),
'background' => array(
'description' => "A string specifying the hexadecimal color code to use as background for the uncovered area of the image after the rotation. E.g. '#000000' for black, '#ff00ff' for magenta, and '#ffffff' for white. For images that support transparency, this will default to transparent white",
'required' => FALSE,
'default' => NULL,
),
);
}
/**
* {@inheritdoc}
*/
protected function validateArguments(array $arguments) {
// PHP 5.5 GD bug: https://bugs.php.net/bug.php?id=65148: To prevent buggy
// behavior on negative multiples of 90 degrees we convert any negative
// angle to a positive one between 0 and 360 degrees.
$arguments['degrees'] -= floor($arguments['degrees'] / 360) * 360;
// Validate or set background color argument.
if (!empty($arguments['background'])) {
// Validate the background color: Color::hexToRgb does so for us.
$background = Color::hexToRgb($arguments['background']) + array( 'alpha' => 0 );
}
else {
// Background color is not specified: use transparent white as background.
$background = array('red' => 255, 'green' => 255, 'blue' => 255, 'alpha' => 127);
}
// Store the color index for the background as that is what GD uses.
$arguments['background_idx'] = imagecolorallocatealpha($this->getToolkit()->getResource(), $background['red'], $background['green'], $background['blue'], $background['alpha']);
if ($this->getToolkit()->getType() === IMAGETYPE_GIF) {
// GIF does not work with a transparency channel, but can define 1 color
// in its palette to act as transparent.
// Get the current transparent color, if any.
$gif_transparent_id = imagecolortransparent($this->getToolkit()->getResource());
if ($gif_transparent_id !== -1) {
// The gif already has a transparent color set: remember it to set it on
// the rotated image as well.
$arguments['gif_transparent_color'] = imagecolorsforindex($this->getToolkit()->getResource(), $gif_transparent_id);
if ($background['alpha'] >= 127) {
// We want a transparent background: use the color already set to act
// as transparent, as background.
$arguments['background_idx'] = $gif_transparent_id;
}
}
else {
// The gif does not currently have a transparent color set.
if ($background['alpha'] >= 127) {
// But as the background is transparent, it should get one.
$arguments['gif_transparent_color'] = $background;
}
}
}
return $arguments;
}
/**
* {@inheritdoc}
*/
protected function execute(array $arguments) {
// PHP installations using non-bundled GD do not have imagerotate.
if (!function_exists('imagerotate')) {
$this->logger->notice('The image %file could not be rotated because the imagerotate() function is not available in this PHP installation.', array('%file' => $this->getToolkit()->getImage()->getSource()));
return FALSE;
}
$this->getToolkit()->setResource(imagerotate($this->getToolkit()->getResource(), 360 - $arguments['degrees'], $arguments['background_idx']));
// GIFs need to reassign the transparent color after performing the rotate,
// but only do so, if the image already had transparency of its own, or the
// rotate added a transparent background.
if (!empty($arguments['gif_transparent_color'])) {
$transparent_idx = imagecolorexactalpha($this->getToolkit()->getResource(), $arguments['gif_transparent_color']['red'], $arguments['gif_transparent_color']['green'], $arguments['gif_transparent_color']['blue'], $arguments['gif_transparent_color']['alpha']);
imagecolortransparent($this->getToolkit()->getResource(), $transparent_idx);
}
return TRUE;
}
}

View file

@ -0,0 +1,99 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\Scale.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines GD2 Scale operation.
*
* @ImageToolkitOperation(
* id = "gd_scale",
* toolkit = "gd",
* operation = "scale",
* label = @Translation("Scale"),
* description = @Translation("Scales an image while maintaining aspect ratio. The resulting image can be smaller for one or both target dimensions.")
* )
*/
class Scale extends Resize {
/**
* {@inheritdoc}
*/
protected function arguments() {
return array(
'width' => array(
'description' => 'The target width, in pixels. This value is omitted then the scaling will based only on the height value',
'required' => FALSE,
'default' => NULL,
),
'height' => array(
'description' => 'The target height, in pixels. This value is omitted then the scaling will based only on the width value',
'required' => FALSE,
'default' => NULL,
),
'upscale' => array(
'description' => 'Boolean indicating that files smaller than the dimensions will be scaled up. This generally results in a low quality image',
'required' => FALSE,
'default' => FALSE,
),
);
}
/**
* {@inheritdoc}
*/
protected function validateArguments(array $arguments) {
// Assure at least one dimension.
if (empty($arguments['width']) && empty($arguments['height'])) {
throw new \InvalidArgumentException("At least one dimension ('width' or 'height') must be provided to the image 'scale' operation");
}
// Calculate one of the dimensions from the other target dimension,
// ensuring the same aspect ratio as the source dimensions. If one of the
// target dimensions is missing, that is the one that is calculated. If both
// are specified then the dimension calculated is the one that would not be
// calculated to be bigger than its target.
$aspect = $this->getToolkit()->getHeight() / $this->getToolkit()->getWidth();
if (($arguments['width'] && !$arguments['height']) || ($arguments['width'] && $arguments['height'] && $aspect < $arguments['height'] / $arguments['width'])) {
$arguments['height'] = (int) round($arguments['width'] * $aspect);
}
else {
$arguments['width'] = (int) round($arguments['height'] / $aspect);
}
// Assure integers for all arguments.
$arguments['width'] = (int) round($arguments['width']);
$arguments['height'] = (int) round($arguments['height']);
// Fail when width or height are 0 or negative.
if ($arguments['width'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid width (@value) specified for the image 'scale' operation", array('@value' => $arguments['width'])));
}
if ($arguments['height'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid height (@value) specified for the image 'scale' operation", array('@value' => $arguments['height'])));
}
return $arguments;
}
/**
* {@inheritdoc}
*/
protected function execute(array $arguments = array()) {
// Don't scale if we don't change the dimensions at all.
if ($arguments['width'] !== $this->getToolkit()->getWidth() || $arguments['height'] !== $this->getToolkit()->getHeight()) {
// Don't upscale if the option isn't enabled.
if ($arguments['upscale'] || ($arguments['width'] <= $this->getToolkit()->getWidth() && $arguments['height'] <= $this->getToolkit()->getHeight())) {
return parent::execute($arguments);
}
}
return TRUE;
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\ImageToolkit\Operation\gd\ScaleAndCrop.
*/
namespace Drupal\system\Plugin\ImageToolkit\Operation\gd;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines GD2 Scale and crop operation.
*
* @ImageToolkitOperation(
* id = "gd_scale_and_crop",
* toolkit = "gd",
* operation = "scale_and_crop",
* label = @Translation("Scale and crop"),
* description = @Translation("Scales an image to the exact width and height given. This plugin achieves the target aspect ratio by cropping the original image equally on both sides, or equally on the top and bottom. This function is useful to create uniform sized avatars from larger images.")
* )
*/
class ScaleAndCrop extends GDImageToolkitOperationBase {
/**
* {@inheritdoc}
*/
protected function arguments() {
return array(
'width' => array(
'description' => 'The target width, in pixels',
),
'height' => array(
'description' => 'The target height, in pixels',
),
);
}
/**
* {@inheritdoc}
*/
protected function validateArguments(array $arguments) {
$actualWidth = $this->getToolkit()->getWidth();
$actualHeight = $this->getToolkit()->getHeight();
$scaleFactor = max($arguments['width'] / $actualWidth, $arguments['height'] / $actualHeight);
$arguments['x'] = (int) round(($actualWidth * $scaleFactor - $arguments['width']) / 2);
$arguments['y'] = (int) round(($actualHeight * $scaleFactor - $arguments['height']) / 2);
$arguments['resize'] = array(
'width' => (int) round($actualWidth * $scaleFactor),
'height' => (int) round($actualHeight * $scaleFactor),
);
// Fail when width or height are 0 or negative.
if ($arguments['width'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid width (@value) specified for the image 'scale_and_crop' operation", array('@value' => $arguments['width'])));
}
if ($arguments['height'] <= 0) {
throw new \InvalidArgumentException(SafeMarkup::format("Invalid height (@value) specified for the image 'scale_and_crop' operation", array('@value' => $arguments['height'])));
}
return $arguments;
}
/**
* {@inheritdoc}
*/
protected function execute(array $arguments = array()) {
return $this->getToolkit()->apply('resize', $arguments['resize'])
&& $this->getToolkit()->apply('crop', $arguments);
}
}

View file

@ -0,0 +1,498 @@
<?php
/**
* @file
* Contains \Drupal\system\Plugin\views\field\BulkForm.
*/
namespace Drupal\system\Plugin\views\field;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\views\Entity\Render\EntityTranslationRenderTrait;
use Drupal\views\Plugin\CacheablePluginInterface;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\Plugin\views\field\UncacheableFieldHandlerTrait;
use Drupal\views\Plugin\views\style\Table;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a actions-based bulk operation form element.
*
* @ViewsField("bulk_form")
*/
class BulkForm extends FieldPluginBase implements CacheablePluginInterface {
use RedirectDestinationTrait;
use UncacheableFieldHandlerTrait;
use EntityTranslationRenderTrait;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The action storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $actionStorage;
/**
* An array of actions that can be executed.
*
* @var \Drupal\system\ActionConfigEntityInterface[]
*/
protected $actions = array();
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new BulkForm 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\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityManager = $entity_manager;
$this->actionStorage = $entity_manager->getStorage('action');
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager'),
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
$entity_type = $this->getEntityType();
// Filter the actions to only include those for this entity type.
$this->actions = array_filter($this->actionStorage->loadMultiple(), function ($action) use ($entity_type) {
return $action->getType() == $entity_type;
});
}
/**
* {@inheritdoc}
*/
public function isCacheable() {
// @todo Consider making the bulk operation form cacheable. See
// https://www.drupal.org/node/2503009.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->languageManager->isMultilingual() ? $this->getEntityTranslationRenderer()->getCacheContexts() : [];
}
/**
* {@inheritdoc}
*/
public function getEntityTypeId() {
return $this->getEntityType();
}
/**
* {@inheritdoc}
*/
protected function getEntityManager() {
return $this->entityManager;
}
/**
* {@inheritdoc}
*/
protected function getLanguageManager() {
return $this->languageManager;
}
/**
* {@inheritdoc}
*/
protected function getView() {
return $this->view;
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['action_title'] = array('default' => $this->t('With selection'));
$options['include_exclude'] = array(
'default' => 'exclude',
);
$options['selected_actions'] = array(
'default' => array(),
);
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['action_title'] = array(
'#type' => 'textfield',
'#title' => $this->t('Action title'),
'#default_value' => $this->options['action_title'],
'#description' => $this->t('The title shown above the actions dropdown.'),
);
$form['include_exclude'] = array(
'#type' => 'radios',
'#title' => $this->t('Available actions'),
'#options' => array(
'exclude' => $this->t('All actions, except selected'),
'include' => $this->t('Only selected actions'),
),
'#default_value' => $this->options['include_exclude'],
);
$form['selected_actions'] = array(
'#type' => 'checkboxes',
'#title' => $this->t('Selected actions'),
'#options' => $this->getBulkOptions(FALSE),
'#default_value' => $this->options['selected_actions'],
);
parent::buildOptionsForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
parent::validateOptionsForm($form, $form_state);
$selected_actions = $form_state->getValue(array('options', 'selected_actions'));
$form_state->setValue(array('options', 'selected_actions'), array_values(array_filter($selected_actions)));
}
/**
* {@inheritdoc}
*/
public function preRender(&$values) {
parent::preRender($values);
// If the view is using a table style, provide a placeholder for a
// "select all" checkbox.
if (!empty($this->view->style_plugin) && $this->view->style_plugin instanceof Table) {
// Add the tableselect css classes.
$this->options['element_label_class'] .= 'select-all';
// Hide the actual label of the field on the table header.
$this->options['label'] = '';
}
}
/**
* {@inheritdoc}
*/
public function getValue(ResultRow $row, $field = NULL) {
return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->';
}
/**
* Form constructor for the bulk form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function viewsForm(&$form, FormStateInterface $form_state) {
// Make sure we do not accidentally cache this form.
// @todo Evaluate this again in https://www.drupal.org/node/2503009.
$form['#cache']['max-age'] = 0;
// Add the tableselect javascript.
$form['#attached']['library'][] = 'core/drupal.tableselect';
$use_revision = array_key_exists('revision', $this->view->getQuery()->getEntityTableInfo());
// Only add the bulk form options and buttons if there are results.
if (!empty($this->view->result)) {
// Render checkboxes for all rows.
$form[$this->options['id']]['#tree'] = TRUE;
foreach ($this->view->result as $row_index => $row) {
$entity = $this->getEntityTranslation($this->getEntity($row), $row);
$form[$this->options['id']][$row_index] = array(
'#type' => 'checkbox',
// We are not able to determine a main "title" for each row, so we can
// only output a generic label.
'#title' => $this->t('Update this item'),
'#title_display' => 'invisible',
'#default_value' => !empty($form_state->getValue($this->options['id'])[$row_index]) ? 1 : NULL,
'#return_value' => $this->calculateEntityBulkFormKey($entity, $use_revision),
);
}
// Replace the form submit button label.
$form['actions']['submit']['#value'] = $this->t('Apply');
// Ensure a consistent container for filters/operations in the view header.
$form['header'] = array(
'#type' => 'container',
'#weight' => -100,
);
// Build the bulk operations action widget for the header.
// Allow themes to apply .container-inline on this separate container.
$form['header'][$this->options['id']] = array(
'#type' => 'container',
);
$form['header'][$this->options['id']]['action'] = array(
'#type' => 'select',
'#title' => $this->options['action_title'],
'#options' => $this->getBulkOptions(),
);
// Duplicate the form actions into the action container in the header.
$form['header'][$this->options['id']]['actions'] = $form['actions'];
}
else {
// Remove the default actions build array.
unset($form['actions']);
}
}
/**
* Returns the available operations for this form.
*
* @param bool $filtered
* (optional) Whether to filter actions to selected actions.
* @return array
* An associative array of operations, suitable for a select element.
*/
protected function getBulkOptions($filtered = TRUE) {
$options = array();
// Filter the action list.
foreach ($this->actions as $id => $action) {
if ($filtered) {
$in_selected = in_array($id, $this->options['selected_actions']);
// If the field is configured to include only the selected actions,
// skip actions that were not selected.
if (($this->options['include_exclude'] == 'include') && !$in_selected) {
continue;
}
// Otherwise, if the field is configured to exclude the selected
// actions, skip actions that were selected.
elseif (($this->options['include_exclude'] == 'exclude') && $in_selected) {
continue;
}
}
$options[$id] = $action->label();
}
return $options;
}
/**
* Submit handler for the bulk form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Thrown when the user tried to access an action without access to it.
*/
public function viewsFormSubmit(&$form, FormStateInterface $form_state) {
if ($form_state->get('step') == 'views_form_views_form') {
// Filter only selected checkboxes.
$selected = array_filter($form_state->getValue($this->options['id']));
$entities = array();
$action = $this->actions[$form_state->getValue('action')];
$count = 0;
foreach ($selected as $bulk_form_key) {
$entity = $this->loadEntityFromBulkFormKey($bulk_form_key);
// Skip execution if the user did not have access.
if (!$action->getPlugin()->access($entity, $this->view->getUser())) {
$this->drupalSetMessage($this->t('No access to execute %action on the @entity_type_label %entity_label.', [
'%action' => $action->label(),
'@entity_type_label' => $entity->getEntityType()->getLabel(),
'%entity_label' => $entity->label()
]), 'error');
continue;
}
$count++;
$entities[$bulk_form_key] = $entity;
}
$action->execute($entities);
$operation_definition = $action->getPluginDefinition();
if (!empty($operation_definition['confirm_form_route_name'])) {
$options = array(
'query' => $this->getDestinationArray(),
);
$form_state->setRedirect($operation_definition['confirm_form_route_name'], array(), $options);
}
else {
// Don't display the message unless there are some elements affected and
// there is no confirmation form.
$count = count(array_filter($form_state->getValue($this->options['id'])));
if ($count) {
drupal_set_message($this->formatPlural($count, '%action was applied to @count item.', '%action was applied to @count items.', array(
'%action' => $action->label(),
)));
}
}
}
}
/**
* Returns the message to be displayed when there are no selected items.
*
* @return string
* Message displayed when no items are selected.
*/
protected function emptySelectedMessage() {
return $this->t('No items selected.');
}
/**
* {@inheritdoc}
*/
public function viewsFormValidate(&$form, FormStateInterface $form_state) {
$selected = array_filter($form_state->getValue($this->options['id']));
if (empty($selected)) {
$form_state->setErrorByName('', $this->emptySelectedMessage());
}
}
/**
* {@inheritdoc}
*/
public function query() {
if ($this->languageManager->isMultilingual()) {
$this->getEntityTranslationRenderer()->query($this->query, $this->relationship);
}
}
/**
* {@inheritdoc}
*/
public function clickSortable() {
return FALSE;
}
/**
* Wraps drupal_set_message().
*/
protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
drupal_set_message($message, $type, $repeat);
}
/**
* Calculates a bulk form key.
*
* This generates a key that is used as the checkbox return value when
* submitting a bulk form. This key allows the entity for the row to be loaded
* totally independently of the executed view row.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to calculate a bulk form key for.
* @param bool $use_revision
* Whether the revision id should be added to the bulk form key. This should
* be set to TRUE only if the view is listing entity revisions.
*
* @return string
* The bulk form key representing the entity's id, language and revision (if
* applicable) as one string.
*
* @see self::loadEntityFromBulkFormKey()
*/
protected function calculateEntityBulkFormKey(EntityInterface $entity, $use_revision) {
$key_parts = [$entity->language()->getId(), $entity->id()];
if ($entity instanceof RevisionableInterface && $use_revision) {
$key_parts[] = $entity->getRevisionId();
}
return implode('-', $key_parts);
}
/**
* Loads an entity based on a bulk form key.
*
* @param string $bulk_form_key
* The bulk form key representing the entity's id, language and revision (if
* applicable) as one string.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity loaded in the state (language, optionally revision) specified
* as part of the bulk form key.
*/
protected function loadEntityFromBulkFormKey($bulk_form_key) {
$key_parts = explode('-', $bulk_form_key);
$revision_id = NULL;
// If there are 3 items, vid will be last.
if (count($key_parts) === 3) {
$revision_id = array_pop($key_parts);
}
// The first two items will always be langcode and ID.
$id = array_pop($key_parts);
$langcode = array_pop($key_parts);
// Load the entity or a specific revision depending on the given key.
$storage = $this->entityManager->getStorage($this->getEntityType());
$entity = $revision_id ? $storage->loadRevision($revision_id) : $storage->load($id);
if ($entity instanceof TranslatableInterface) {
$entity = $entity->getTranslation($langcode);
}
return $entity;
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\system\SystemConfigSubscriber.
*/
namespace Drupal\system;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\ConfigImporterEvent;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* System Config subscriber.
*/
class SystemConfigSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* Checks that the configuration synchronization is valid.
*
* This event listener prevents deleting all configuration. If there is
* nothing to import then event propagation is stopped because there is no
* config import to validate.
*
* @param \Drupal\Core\Config\ConfigImporterEvent $event
* The config import event.
*/
public function onConfigImporterValidateNotEmpty(ConfigImporterEvent $event) {
$importList = $event->getConfigImporter()->getStorageComparer()->getSourceStorage()->listAll();
if (empty($importList)) {
$event->getConfigImporter()->logError($this->t('This import is empty and if applied would delete all of your configuration, so has been rejected.'));
$event->stopPropagation();
}
}
/**
* Checks that the configuration synchronization is valid.
*
* This event listener checks that the system.site:uuid's in the source and
* target match.
*
* @param ConfigImporterEvent $event
* The config import event.
*/
public function onConfigImporterValidateSiteUUID(ConfigImporterEvent $event) {
if (!$event->getConfigImporter()->getStorageComparer()->validateSiteUuid()) {
$event->getConfigImporter()->logError($this->t('Site UUID in source storage does not match the target storage.'));
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// The empty check has a high priority so that is can stop propagation if
// there is no configuration to import.
$events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidateNotEmpty', 512);
$events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidateSiteUUID', 256);
return $events;
}
}

View file

@ -0,0 +1,222 @@
<?php
/**
* @file
* Contains \Drupal\system\SystemManager.
*/
namespace Drupal\system;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Menu\MenuActiveTrailInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* System Manager Service.
*/
class SystemManager {
/**
* Module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The menu link tree manager.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuTree;
/**
* The active menu trail service.
*
* @var \Drupal\Core\Menu\MenuActiveTrailInterface
*/
protected $menuActiveTrail;
/**
* A static cache of menu items.
*
* @var array
*/
protected $menuItems;
/**
* Requirement severity -- Requirement successfully met.
*/
const REQUIREMENT_OK = 0;
/**
* Requirement severity -- Warning condition; proceed but flag warning.
*/
const REQUIREMENT_WARNING = 1;
/**
* Requirement severity -- Error condition; abort installation.
*/
const REQUIREMENT_ERROR = 2;
/**
* Constructs a SystemManager object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree
* The menu tree manager.
* @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail
* The active menu trail service.
*/
public function __construct(ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager, RequestStack $request_stack, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $menu_active_trail) {
$this->moduleHandler = $module_handler;
$this->requestStack = $request_stack;
$this->menuTree = $menu_tree;
$this->menuActiveTrail = $menu_active_trail;
}
/**
* Checks for requirement severity.
*
* @return boolean
* Returns the status of the system.
*/
public function checkRequirements() {
$requirements = $this->listRequirements();
return $this->getMaxSeverity($requirements) == static::REQUIREMENT_ERROR;
}
/**
* Displays the site status report. Can also be used as a pure check.
*
* @return array
* An array of system requirements.
*/
public function listRequirements() {
// Load .install files
include_once DRUPAL_ROOT . '/core/includes/install.inc';
drupal_load_updates();
// Check run-time requirements and status information.
$requirements = $this->moduleHandler->invokeAll('requirements', array('runtime'));
usort($requirements, function($a, $b) {
if (!isset($a['weight'])) {
if (!isset($b['weight'])) {
return strcmp($a['title'], $b['title']);
}
return -$b['weight'];
}
return isset($b['weight']) ? $a['weight'] - $b['weight'] : $a['weight'];
});
return $requirements;
}
/**
* Extracts the highest severity from the requirements array.
*
* @param $requirements
* An array of requirements, in the same format as is returned by
* hook_requirements().
*
* @return
* The highest severity in the array.
*/
public function getMaxSeverity(&$requirements) {
$severity = static::REQUIREMENT_OK;
foreach ($requirements as $requirement) {
if (isset($requirement['severity'])) {
$severity = max($severity, $requirement['severity']);
}
}
return $severity;
}
/**
* Loads the contents of a menu block.
*
* This function is often a destination for these blocks.
* For example, 'admin/structure/types' needs to have a destination to be
* valid in the Drupal menu system, but too much information there might be
* hidden, so we supply the contents of the block.
*
* @return array
* A render array suitable for drupal_render.
*/
public function getBlockContents() {
// We hard-code the menu name here since otherwise a link in the tools menu
// or elsewhere could give us a blank block.
$link = $this->menuActiveTrail->getActiveLink('admin');
if ($link && $content = $this->getAdminBlock($link)) {
$output = array(
'#theme' => 'admin_block_content',
'#content' => $content,
);
}
else {
$output = array(
'#markup' => t('You do not have any administrative items.'),
);
}
return $output;
}
/**
* Provide a single block on the administration overview page.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $instance
* The menu item to be displayed.
*
* @return array
* An array of menu items, as expected by admin-block-content.html.twig.
*/
public function getAdminBlock(MenuLinkInterface $instance) {
$content = array();
// Only find the children of this link.
$link_id = $instance->getPluginId();
$parameters = new MenuTreeParameters();
$parameters->setRoot($link_id)->excludeRoot()->setTopLevelOnly()->onlyEnabledLinks();
$tree = $this->menuTree->load(NULL, $parameters);
$manipulators = array(
array('callable' => 'menu.default_tree_manipulators:checkAccess'),
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
);
$tree = $this->menuTree->transform($tree, $manipulators);
foreach ($tree as $key => $element) {
// Only render accessible links.
if (!$element->access->isAllowed()) {
// @todo Bubble cacheability metadata of both accessible and
// inaccessible links. Currently made impossible by the way admin
// blocks are rendered.
continue;
}
/** @var $link \Drupal\Core\Menu\MenuLinkInterface */
$link = $element->link;
$content[$key]['title'] = $link->getTitle();
$content[$key]['options'] = $link->getOptions();
$content[$key]['description'] = $link->getDescription();
$content[$key]['url'] = $link->getUrlObject();
}
ksort($content);
return $content;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\system\SystemRequirements.
*/
namespace Drupal\system;
/**
* Class for helper methods used for the system requirements.
*/
class SystemRequirements {
/**
* Determines whether the passed in PHP version disallows multiple statements.
*
* @param string $phpversion
*
* @return bool
*/
public static function phpVersionWithPdoDisallowMultipleStatements($phpversion) {
// PDO::MYSQL_ATTR_MULTI_STATEMENTS was introduced in PHP versions 5.5.21
// and 5.6.5.
return (version_compare($phpversion, '5.5.21', '>=') && version_compare($phpversion, '5.6.0', '<'))
|| version_compare($phpversion, '5.6.5', '>=');
}
}

View file

@ -0,0 +1,105 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Action\ActionUnitTest.
*/
namespace Drupal\system\Tests\Action;
use Drupal\simpletest\KernelTestBase;
use Drupal\Core\Action\ActionInterface;
use Drupal\user\RoleInterface;
/**
* Tests action plugins.
*
* @group Action
*/
class ActionUnitTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('system', 'field', 'user', 'action_test');
/**
* The action manager.
*
* @var \Drupal\Core\Action\ActionManager
*/
protected $actionManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->actionManager = $this->container->get('plugin.manager.action');
$this->installEntitySchema('user');
$this->installSchema('system', array('sequences'));
}
/**
* Tests the functionality of test actions.
*/
public function testOperations() {
// Test that actions can be discovered.
$definitions = $this->actionManager->getDefinitions();
$this->assertTrue(count($definitions) > 1, 'Action definitions are found.');
$this->assertTrue(!empty($definitions['action_test_no_type']), 'The test action is among the definitions found.');
$definition = $this->actionManager->getDefinition('action_test_no_type');
$this->assertTrue(!empty($definition), 'The test action definition is found.');
$definitions = $this->actionManager->getDefinitionsByType('user');
$this->assertTrue(empty($definitions['action_test_no_type']), 'An action with no type is not found.');
// Create an instance of the 'save entity' action.
$action = $this->actionManager->createInstance('action_test_save_entity');
$this->assertTrue($action instanceof ActionInterface, 'The action implements the correct interface.');
// Create a new unsaved user.
$name = $this->randomMachineName();
$user_storage = $this->container->get('entity.manager')->getStorage('user');
$account = $user_storage->create(array('name' => $name, 'bundle' => 'user'));
$loaded_accounts = $user_storage->loadMultiple();
$this->assertEqual(count($loaded_accounts), 0);
// Execute the 'save entity' action.
$action->execute($account);
$loaded_accounts = $user_storage->loadMultiple();
$this->assertEqual(count($loaded_accounts), 1);
$account = reset($loaded_accounts);
$this->assertEqual($name, $account->label());
}
/**
* Tests the dependency calculation of actions.
*/
public function testDependencies() {
// Create a new action that depends on a user role.
$action = entity_create('action', array(
'id' => 'user_add_role_action.' . RoleInterface::ANONYMOUS_ID,
'type' => 'user',
'label' => t('Add the anonymous role to the selected users'),
'configuration' => array(
'rid' => RoleInterface::ANONYMOUS_ID,
),
'plugin' => 'user_add_role_action',
));
$action->save();
$expected = array(
'config' => array(
'user.role.' . RoleInterface::ANONYMOUS_ID,
),
'module' => array(
'user',
),
);
$this->assertIdentical($expected, $action->calculateDependencies());
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\AjaxFormCacheTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Url;
/**
* Tests the usage of form caching for AJAX forms.
*
* @group Ajax
*/
class AjaxFormCacheTest extends AjaxTestBase {
/**
* Tests the usage of form cache for AJAX forms.
*/
public function testFormCacheUsage() {
/** @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable */
$key_value_expirable = \Drupal::service('keyvalue.expirable')->get('form');
$this->drupalLogin($this->rootUser);
// Ensure that the cache is empty.
$this->assertEqual(0, count($key_value_expirable->getAll()));
// Visit an AJAX form that is not cached, 3 times.
$uncached_form_url = Url::fromRoute('ajax_forms_test.commands_form');
$this->drupalGet($uncached_form_url);
$this->drupalGet($uncached_form_url);
$this->drupalGet($uncached_form_url);
// The number of cache entries should not have changed.
$this->assertEqual(0, count($key_value_expirable->getAll()));
// Visit a form that is explicitly cached, 3 times.
$cached_form_url = Url::fromRoute('ajax_forms_test.cached_form');
$this->drupalGet($cached_form_url);
$this->drupalGet($cached_form_url);
$this->drupalGet($cached_form_url);
// The number of cache entries should be exactly 3.
$this->assertEqual(3, count($key_value_expirable->getAll()));
}
/**
* Tests AJAX forms in blocks.
*/
public function testBlockForms() {
$this->container->get('module_installer')->install(['block', 'search']);
$this->rebuildContainer();
$this->container->get('router.builder')->rebuild();
$this->drupalLogin($this->rootUser);
$this->drupalPlaceBlock('search_form_block', ['weight' => -5]);
$this->drupalPlaceBlock('ajax_forms_test_block', ['cache' => ['max_age' => 0]]);
$this->drupalGet('');
$this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
$this->assertOptionSelectedWithDrupalSelector('edit-test1', 'option1');
$this->assertOptionWithDrupalSelector('edit-test1', 'option3');
$this->drupalPostForm(NULL, ['test1' => 'option1'], 'Submit');
$this->assertText('Submission successful.');
}
/**
* Tests AJAX forms on pages with a query string.
*/
public function testQueryString() {
$this->container->get('module_installer')->install(['block']);
$this->drupalLogin($this->rootUser);
$this->drupalPlaceBlock('ajax_forms_test_block', ['cache' => ['max_age' => 0]]);
$url = Url::fromRoute('entity.user.canonical', ['user' => $this->rootUser->id()], ['query' => ['foo' => 'bar']]);
$this->drupalGet($url);
$this->drupalPostAjaxForm(NULL, ['test1' => 'option1'], 'test1');
$url->setOption('query', [
'foo' => 'bar',
FormBuilderInterface::AJAX_FORM_REQUEST => 1,
MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
]);
$this->assertUrl($url);
}
}

View file

@ -0,0 +1,107 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\AjaxFormPageCacheTest.
*/
namespace Drupal\system\Tests\Ajax;
/**
* Performs tests on AJAX forms in cached pages.
*
* @group Ajax
*/
class AjaxFormPageCacheTest extends AjaxTestBase {
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Return the build id of the current form.
*/
protected function getFormBuildId() {
$build_id_fields = $this->xpath('//input[@name="form_build_id"]');
$this->assertEqual(count($build_id_fields), 1, 'One form build id field on the page');
return (string) $build_id_fields[0]['value'];
}
/**
* Create a simple form, then POST to system/ajax to change to it.
*/
public function testSimpleAJAXFormValue() {
$this->drupalGet('ajax_forms_test_get_form');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
$build_id_initial = $this->getFormBuildId();
$edit = ['select' => 'green'];
$commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
$build_id_first_ajax = $this->getFormBuildId();
$this->assertNotEqual($build_id_initial, $build_id_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission');
$expected = [
'command' => 'update_build_id',
'old' => $build_id_initial,
'new' => $build_id_first_ajax,
];
$this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission');
$edit = ['select' => 'red'];
$commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
$build_id_second_ajax = $this->getFormBuildId();
$this->assertNotEqual($build_id_first_ajax, $build_id_second_ajax, 'Build id changes on subsequent AJAX submissions');
$expected = [
'command' => 'update_build_id',
'old' => $build_id_first_ajax,
'new' => $build_id_second_ajax,
];
$this->assertCommand($commands, $expected, 'Build id change command issued on subsequent AJAX submissions');
// Repeat the test sequence but this time with a page loaded from the cache.
$this->drupalGet('ajax_forms_test_get_form');
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
$build_id_from_cache_initial = $this->getFormBuildId();
$this->assertEqual($build_id_initial, $build_id_from_cache_initial, 'Build id is the same as on the first request');
$edit = ['select' => 'green'];
$commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
$build_id_from_cache_first_ajax = $this->getFormBuildId();
$this->assertNotEqual($build_id_from_cache_initial, $build_id_from_cache_first_ajax, 'Build id is changed in the simpletest-DOM on first AJAX submission');
$this->assertNotEqual($build_id_first_ajax, $build_id_from_cache_first_ajax, 'Build id from first user is not reused');
$expected = [
'command' => 'update_build_id',
'old' => $build_id_from_cache_initial,
'new' => $build_id_from_cache_first_ajax,
];
$this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission');
$edit = ['select' => 'red'];
$commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
$build_id_from_cache_second_ajax = $this->getFormBuildId();
$this->assertNotEqual($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id changes on subsequent AJAX submissions');
$expected = [
'command' => 'update_build_id',
'old' => $build_id_from_cache_first_ajax,
'new' => $build_id_from_cache_second_ajax,
];
$this->assertCommand($commands, $expected, 'Build id change command issued on subsequent AJAX submissions');
}
/**
* Tests a form that uses an #ajax callback.
*
* @see \Drupal\system\Tests\Ajax\ElementValidationTest::testAjaxElementValidation()
*/
public function testAjaxElementValidation() {
$edit = ['drivertext' => t('some dumb text')];
$this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivertext');
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\AjaxTestBase.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\simpletest\WebTestBase;
/**
* Provides a base class for Ajax tests.
*/
abstract class AjaxTestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'ajax_test', 'ajax_forms_test');
/**
* Asserts the array of Ajax commands contains the searched command.
*
* An AjaxResponse object stores an array of Ajax commands. This array
* sometimes includes commands automatically provided by the framework in
* addition to commands returned by a particular controller. During testing,
* we're usually interested that a particular command is present, and don't
* care whether other commands precede or follow the one we're interested in.
* Additionally, the command we're interested in may include additional data
* that we're not interested in. Therefore, this function simply asserts that
* one of the commands in $haystack contains all of the keys and values in
* $needle. Furthermore, if $needle contains a 'settings' key with an array
* value, we simply assert that all keys and values within that array are
* present in the command we're checking, and do not consider it a failure if
* the actual command contains additional settings that aren't part of
* $needle.
*
* @param $haystack
* An array of rendered Ajax commands returned by the server.
* @param $needle
* Array of info we're expecting in one of those commands.
* @param $message
* An assertion message.
*/
protected function assertCommand($haystack, $needle, $message) {
$found = FALSE;
foreach ($haystack as $command) {
// If the command has additional settings that we're not testing for, do
// not consider that a failure.
if (isset($command['settings']) && is_array($command['settings']) && isset($needle['settings']) && is_array($needle['settings'])) {
$command['settings'] = array_intersect_key($command['settings'], $needle['settings']);
}
// If the command has additional data that we're not testing for, do not
// consider that a failure. Also, == instead of ===, because we don't
// require the key/value pairs to be in any particular order
// (http://www.php.net/manual/language.operators.array.php).
if (array_intersect_key($command, $needle) == $needle) {
$found = TRUE;
break;
}
}
$this->assertTrue($found, $message);
}
}

View file

@ -0,0 +1,163 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\CommandsTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\Ajax\AddCssCommand;
use Drupal\Core\Ajax\AfterCommand;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\BeforeCommand;
use Drupal\Core\Ajax\ChangedCommand;
use Drupal\Core\Ajax\CssCommand;
use Drupal\Core\Ajax\DataCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\RestripeCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Performs tests on AJAX framework commands.
*
* @group Ajax
*/
class CommandsTest extends AjaxTestBase {
/**
* Tests the various Ajax Commands.
*/
function testAjaxCommands() {
$form_path = 'ajax_forms_test_ajax_commands_form';
$web_user = $this->drupalCreateUser(array('access content'));
$this->drupalLogin($web_user);
$edit = array();
// Tests the 'add_css' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'add_css' command")));
$expected = new AddCssCommand('my/file.css');
$this->assertCommand($commands, $expected->render(), "'add_css' AJAX command issued with correct data.");
// Tests the 'after' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'After': Click to put something after the div")));
$expected = new AfterCommand('#after_div', 'This will be placed after');
$this->assertCommand($commands, $expected->render(), "'after' AJAX command issued with correct data.");
// Tests the 'alert' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'Alert': Click to alert")));
$expected = new AlertCommand(t('Alert'));
$this->assertCommand($commands, $expected->render(), "'alert' AJAX Command issued with correct text.");
// Tests the 'append' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'Append': Click to append something")));
$expected = new AppendCommand('#append_div', 'Appended text');
$this->assertCommand($commands, $expected->render(), "'append' AJAX command issued with correct data.");
// Tests the 'before' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'before': Click to put something before the div")));
$expected = new BeforeCommand('#before_div', 'Before text');
$this->assertCommand($commands, $expected->render(), "'before' AJAX command issued with correct data.");
// Tests the 'changed' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed.")));
$expected = new ChangedCommand('#changed_div');
$this->assertCommand($commands, $expected->render(), "'changed' AJAX command issued with correct selector.");
// Tests the 'changed' command using the second argument.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed with asterisk.")));
$expected = new ChangedCommand('#changed_div', '#changed_div_mark_this');
$this->assertCommand($commands, $expected->render(), "'changed' AJAX command (with asterisk) issued with correct selector.");
// Tests the 'css' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("Set the '#box' div to be blue.")));
$expected = new CssCommand('#css_div', array('background-color' => 'blue'));
$this->assertCommand($commands, $expected->render(), "'css' AJAX command issued with correct selector.");
// Tests the 'data' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX data command: Issue command.")));
$expected = new DataCommand('#data_div', 'testkey', 'testvalue');
$this->assertCommand($commands, $expected->render(), "'data' AJAX command issued with correct key and value.");
// Tests the 'html' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX html: Replace the HTML in a selector.")));
$expected = new HtmlCommand('#html_div', 'replacement text');
$this->assertCommand($commands, $expected->render(), "'html' AJAX command issued with correct data.");
// Tests the 'insert' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX insert: Let client insert based on #ajax['method'].")));
$expected = new InsertCommand('#insert_div', 'insert replacement text');
$this->assertCommand($commands, $expected->render(), "'insert' AJAX command issued with correct data.");
// Tests the 'invoke' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX invoke command: Invoke addClass() method.")));
$expected = new InvokeCommand('#invoke_div', 'addClass', array('error'));
$this->assertCommand($commands, $expected->render(), "'invoke' AJAX command issued with correct method and argument.");
// Tests the 'prepend' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'prepend': Click to prepend something")));
$expected = new PrependCommand('#prepend_div', 'prepended text');
$this->assertCommand($commands, $expected->render(), "'prepend' AJAX command issued with correct data.");
// Tests the 'remove' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'remove': Click to remove text")));
$expected = new RemoveCommand('#remove_text');
$this->assertCommand($commands, $expected->render(), "'remove' AJAX command issued with correct command and selector.");
// Tests the 'restripe' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'restripe' command")));
$expected = new RestripeCommand('#restripe_table');
$this->assertCommand($commands, $expected->render(), "'restripe' AJAX command issued with correct selector.");
// Tests the 'settings' command.
$commands = $this->drupalPostAjaxForm($form_path, $edit, array('op' => t("AJAX 'settings' command")));
$expected = new SettingsCommand(array('ajax_forms_test' => array('foo' => 42)));
$this->assertCommand($commands, $expected->render(), "'settings' AJAX command issued with correct data.");
}
/**
* Regression test: Settings command exists regardless of JS aggregation.
*/
public function testAttachedSettings() {
$assert = function($message) {
$response = new AjaxResponse();
$response->setAttachments([
'library' => ['core/drupalSettings'],
'drupalSettings' => ['foo' => 'bar'],
]);
$ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor');
$subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor);
$event = new FilterResponseEvent(
\Drupal::service('http_kernel'),
new Request(),
HttpKernelInterface::MASTER_REQUEST,
$response
);
$subscriber->onResponse($event);
$expected = [
'command' => 'settings',
];
$this->assertCommand($response->getCommands(), $expected, $message);
};
$config = $this->config('system.performance');
$config->set('js.preprocess', FALSE)->save();
$assert('Settings command exists when JS aggregation is disabled.');
$config->set('js.preprocess', TRUE)->save();
$assert('Settings command exists when JS aggregation is enabled.');
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\DialogTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Url;
/**
* Performs tests on opening and manipulating dialogs via AJAX commands.
*
* @group Ajax
*/
class DialogTest extends AjaxTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('ajax_test', 'ajax_forms_test', 'contact');
/**
* Test sending non-JS and AJAX requests to open and manipulate modals.
*/
public function testDialog() {
$this->drupalLogin($this->drupalCreateUser(array('administer contact forms')));
// Ensure the elements render without notices or exceptions.
$this->drupalGet('ajax-test/dialog');
// Set up variables for this test.
$dialog_renderable = \Drupal\ajax_test\Controller\AjaxTestController::dialogContents();
$dialog_contents = \Drupal::service('renderer')->renderRoot($dialog_renderable);
$modal_expected_response = array(
'command' => 'openDialog',
'selector' => '#drupal-modal',
'settings' => NULL,
'data' => $dialog_contents,
'dialogOptions' => array(
'modal' => TRUE,
'title' => 'AJAX Dialog contents',
),
);
$form_expected_response = array(
'command' => 'openDialog',
'selector' => '#drupal-modal',
'settings' => NULL,
'dialogOptions' => array(
'modal' => TRUE,
'title' => 'Ajax Form contents',
),
);
$entity_form_expected_response = array(
'command' => 'openDialog',
'selector' => '#drupal-modal',
'settings' => NULL,
'dialogOptions' => array(
'modal' => TRUE,
'title' => 'Add contact form',
),
);
$normal_expected_response = array(
'command' => 'openDialog',
'selector' => '#ajax-test-dialog-wrapper-1',
'settings' => NULL,
'data' => $dialog_contents,
'dialogOptions' => array(
'modal' => FALSE,
'title' => 'AJAX Dialog contents',
),
);
$no_target_expected_response = array(
'command' => 'openDialog',
'selector' => '#drupal-dialog-ajax-testdialog-contents',
'settings' => NULL,
'data' => $dialog_contents,
'dialogOptions' => array(
'modal' => FALSE,
'title' => 'AJAX Dialog contents',
),
);
$close_expected_response = array(
'command' => 'closeDialog',
'selector' => '#ajax-test-dialog-wrapper-1',
'persist' => FALSE,
);
// Check that requesting a modal dialog without JS goes to a page.
$this->drupalGet('ajax-test/dialog-contents');
$this->assertRaw($dialog_contents, 'Non-JS modal dialog page present.');
// Emulate going to the JS version of the page and check the JSON response.
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$this->assertEqual($modal_expected_response, $ajax_result[3], 'Modal dialog JSON response matches.');
// Check that requesting a "normal" dialog without JS goes to a page.
$this->drupalGet('ajax-test/dialog-contents');
$this->assertRaw($dialog_contents, 'Non-JS normal dialog page present.');
// Emulate going to the JS version of the page and check the JSON response.
// This needs to use WebTestBase::drupalPostAjaxForm() so that the correct
// dialog options are sent.
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(
// We have to mock a form element to make drupalPost submit from a link.
'textfield' => 'test',
), array(), 'ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog')), array(), NULL, array(
'submit' => array(
'dialogOptions[target]' => 'ajax-test-dialog-wrapper-1',
)
));
$this->assertEqual($normal_expected_response, $ajax_result[3], 'Normal dialog JSON response matches.');
// Emulate going to the JS version of the page and check the JSON response.
// This needs to use WebTestBase::drupalPostAjaxForm() so that the correct
// dialog options are sent.
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(
// We have to mock a form element to make drupalPost submit from a link.
'textfield' => 'test',
), array(), 'ajax-test/dialog-contents', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog')), array(), NULL, array(
// Don't send a target.
'submit' => array()
));
// Make sure the selector ID starts with the right string.
$this->assert(strpos($ajax_result[3]['selector'], $no_target_expected_response['selector']) === 0, 'Selector starts with right string.');
unset($ajax_result[3]['selector']);
unset($no_target_expected_response['selector']);
$this->assertEqual($no_target_expected_response, $ajax_result[3], 'Normal dialog with no target JSON response matches.');
// Emulate closing the dialog via an AJAX request. There is no non-JS
// version of this test.
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-close');
$this->assertEqual($close_expected_response, $ajax_result[0], 'Close dialog JSON response matches.');
// Test submitting via a POST request through the button for modals. This
// approach more accurately reflects the real responses by Drupal because
// all of the necessary page variables are emulated.
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(), 'button1');
// Check that CSS and JavaScript are "added" to the page dynamically.
$this->assertTrue(in_array('core/drupal.dialog.ajax', explode(',', $ajax_result[0]['settings']['ajaxPageState']['libraries'])), 'core/drupal.dialog.ajax library is added to the page.');
$dialog_css_exists = strpos($ajax_result[1]['data'], 'dialog.css') !== FALSE;
$this->assertTrue($dialog_css_exists, 'jQuery UI dialog CSS added to the page.');
$dialog_js_exists = strpos($ajax_result[2]['data'], 'dialog-min.js') !== FALSE;
$this->assertTrue($dialog_js_exists, 'jQuery UI dialog JS added to the page.');
$dialog_js_exists = strpos($ajax_result[2]['data'], 'dialog.ajax.js') !== FALSE;
$this->assertTrue($dialog_js_exists, 'Drupal dialog JS added to the page.');
// Check that the response matches the expected value.
$this->assertEqual($modal_expected_response, $ajax_result[4], 'POST request modal dialog JSON response matches.');
// Abbreviated test for "normal" dialogs, testing only the difference.
$ajax_result = $this->drupalPostAjaxForm('ajax-test/dialog', array(), 'button2');
$this->assertEqual($normal_expected_response, $ajax_result[4], 'POST request normal dialog JSON response matches.');
// Check that requesting a form dialog without JS goes to a page.
$this->drupalGet('ajax-test/dialog-form');
// Check we get a chunk of the code, we can't test the whole form as form
// build id and token with be different.
$form = $this->xpath("//form[@id='ajax-test-form']");
$this->assertTrue(!empty($form), 'Non-JS form page present.');
// Emulate going to the JS version of the form and check the JSON response.
$ajax_result = $this->drupalGetAjax('ajax-test/dialog-form', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$expected_ajax_settings = [
'edit-preview' => [
'callback' => '::preview',
'event' => 'click',
'url' => Url::fromRoute('ajax_test.dialog_form', [], ['query' => [
MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal',
FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
]])->toString(),
'dialogType' => 'ajax',
'submit' => [
'_triggering_element_name' => 'op',
'_triggering_element_value' => 'Preview',
],
],
];
$this->assertEqual($expected_ajax_settings, $ajax_result[0]['settings']['ajax']);
$this->setRawContent($ajax_result[3]['data']);
// Remove the data, the form build id and token will never match.
unset($ajax_result[3]['data']);
$form = $this->xpath("//form[@id='ajax-test-form']");
$this->assertTrue(!empty($form), 'Modal dialog JSON contains form.');
$this->assertEqual($form_expected_response, $ajax_result[3]);
// Check that requesting an entity form dialog without JS goes to a page.
$this->drupalGet('admin/structure/contact/add');
// Check we get a chunk of the code, we can't test the whole form as form
// build id and token with be different.
$form = $this->xpath("//form[@id='contact-form-add-form']");
$this->assertTrue(!empty($form), 'Non-JS entity form page present.');
// Emulate going to the JS version of the form and check the JSON response.
$ajax_result = $this->drupalGetAjax('admin/structure/contact/add', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_modal')));
$this->setRawContent($ajax_result[3]['data']);
// Remove the data, the form build id and token will never match.
unset($ajax_result[3]['data']);
$form = $this->xpath("//form[@id='contact-form-add-form']");
$this->assertTrue(!empty($form), 'Modal dialog JSON contains entity form.');
$this->assertEqual($entity_form_expected_response, $ajax_result[3]);
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\ElementValidationTest.
*/
namespace Drupal\system\Tests\Ajax;
/**
* Various tests of AJAX behavior.
*
* @group Ajax
*/
class ElementValidationTest extends AjaxTestBase {
/**
* Tries to post an Ajax change to a form that has a validated element.
*
* The drivertext field is Ajax-enabled. An additional field is not, but
* is set to be a required field. In this test the required field is not
* filled in, and we want to see if the activation of the "drivertext"
* Ajax-enabled field fails due to the required field being empty.
*/
function testAjaxElementValidation() {
$edit = array('drivertext' => t('some dumb text'));
// Post with 'drivertext' as the triggering element.
$this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivertext');
// Look for a validation failure in the resultant JSON.
$this->assertNoText(t('Error message'), 'No error message in resultant JSON');
$this->assertText('ajax_forms_test_validation_form_callback invoked', 'The correct callback was invoked');
$this->drupalGet('ajax_validation_test');
$edit = array('drivernumber' => 12345);
// Post with 'drivernumber' as the triggering element.
$this->drupalPostAjaxForm('ajax_validation_test', $edit, 'drivernumber');
// Look for a validation failure in the resultant JSON.
$this->assertNoText(t('Error message'), 'No error message in resultant JSON');
$this->assertText('ajax_forms_test_validation_number_form_callback invoked', 'The correct callback was invoked');
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\FormValuesTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\Ajax\DataCommand;
/**
* Tests that form values are properly delivered to AJAX callbacks.
*
* @group Ajax
*/
class FormValuesTest extends AjaxTestBase {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser(array('access content')));
}
/**
* Submits forms with select and checkbox elements via Ajax.
*/
function testSimpleAjaxFormValue() {
// Verify form values of a select element.
foreach (array('red', 'green', 'blue') as $item) {
$edit = array(
'select' => $item,
);
$commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'select');
$expected = new DataCommand('#ajax_selected_color', 'form_state_value_select', $item);
$this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a selectbox issued with a correct value.');
}
// Verify form values of a checkbox element.
foreach (array(FALSE, TRUE) as $item) {
$edit = array(
'checkbox' => $item,
);
$commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, 'checkbox');
$expected = new DataCommand('#ajax_checkbox_value', 'form_state_value_select', (int) $item);
$this->assertCommand($commands, $expected->render(), 'Verification of AJAX form values from a checkbox issued with a correct value.');
}
// Verify that AJAX elements with invalid callbacks return error code 500.
// Ensure the test error log is empty before these tests.
$this->assertNoErrorsLogged();
foreach (array('null', 'empty', 'nonexistent') as $key) {
$element_name = 'select_' . $key . '_callback';
$edit = array(
$element_name => 'red',
);
$commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, $element_name);
$this->assertResponse(500);
}
// 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');
}
}

View file

@ -0,0 +1,219 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\FrameworkTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\Ajax\AddCssCommand;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\SettingsCommand;
use Drupal\Core\Asset\AttachedAssets;
/**
* Performs tests on AJAX framework functions.
*
* @group Ajax
*/
class FrameworkTest extends AjaxTestBase {
/**
* Ensures \Drupal\Core\Ajax\AjaxResponse::ajaxRender() returns JavaScript settings from the page request.
*/
public function testAJAXRender() {
// Verify that settings command is generated if JavaScript settings exist.
$commands = $this->drupalGetAjax('ajax-test/render');
$expected = new SettingsCommand(array('ajax' => 'test'), TRUE);
$this->assertCommand($commands, $expected->render(), '\Drupal\Core\Ajax\AjaxResponse::ajaxRender() loads JavaScript settings.');
}
/**
* Tests AjaxResponse::prepare() AJAX commands ordering.
*/
public function testOrder() {
$expected_commands = array();
// Expected commands, in a very specific order.
$asset_resolver = \Drupal::service('asset.resolver');
$css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
$js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
$renderer = \Drupal::service('renderer');
$expected_commands[0] = new SettingsCommand(array('ajax' => 'test'), TRUE);
$build['#attached']['library'][] = 'ajax_test/order-css-command';
$assets = AttachedAssets::createFromRenderArray($build);
$css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
$expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array));
$build['#attached']['library'][] = 'ajax_test/order-header-js-command';
$build['#attached']['library'][] = 'ajax_test/order-footer-js-command';
$assets = AttachedAssets::createFromRenderArray($build);
list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, FALSE);
$js_header_render_array = $js_collection_renderer->render($js_assets_header);
$js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
$expected_commands[2] = new PrependCommand('head', $js_header_render_array);
$expected_commands[3] = new AppendCommand('body', $js_footer_render_array);
$expected_commands[4] = new HtmlCommand('body', 'Hello, world!');
// Load any page with at least one CSS file, at least one JavaScript file
// and at least one #ajax-powered element. The latter is an assumption of
// drupalPostAjaxForm(), the two former are assumptions of
// AjaxResponse::ajaxRender().
// @todo refactor AJAX Framework + tests to make less assumptions.
$this->drupalGet('ajax_forms_test_lazy_load_form');
// Verify AJAX command order — this should always be the order:
// 1. JavaScript settings
// 2. CSS files
// 3. JavaScript files in the header
// 4. JavaScript files in the footer
// 5. Any other AJAX commands, in whatever order they were added.
$commands = $this->drupalPostAjaxForm(NULL, array(), NULL, 'ajax-test/order', array(), array(), NULL, array());
$this->assertCommand(array_slice($commands, 0, 1), $expected_commands[0]->render(), 'Settings command is first.');
$this->assertCommand(array_slice($commands, 1, 1), $expected_commands[1]->render(), 'CSS command is second (and CSS files are ordered correctly).');
$this->assertCommand(array_slice($commands, 2, 1), $expected_commands[2]->render(), 'Header JS command is third.');
$this->assertCommand(array_slice($commands, 3, 1), $expected_commands[3]->render(), 'Footer JS command is fourth.');
$this->assertCommand(array_slice($commands, 4, 1), $expected_commands[4]->render(), 'HTML command is fifth.');
}
/**
* Tests the behavior of an error alert command.
*/
public function testAJAXRenderError() {
// Verify custom error message.
$edit = array(
'message' => 'Custom error message.',
);
$commands = $this->drupalGetAjax('ajax-test/render-error', array('query' => $edit));
$expected = new AlertCommand($edit['message']);
$this->assertCommand($commands, $expected->render(), 'Custom error message is output.');
}
/**
* Tests that new JavaScript and CSS files are lazy-loaded on an AJAX request.
*/
public function testLazyLoad() {
$asset_resolver = \Drupal::service('asset.resolver');
$css_collection_renderer = \Drupal::service('asset.css.collection_renderer');
$js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
$renderer = \Drupal::service('renderer');
$expected = array(
'setting_name' => 'ajax_forms_test_lazy_load_form_submit',
'setting_value' => 'executed',
'library_1' => 'system/admin',
'library_2' => 'system/drupal.system',
);
// Get the base page.
$this->drupalGet('ajax_forms_test_lazy_load_form');
$original_settings = $this->getDrupalSettings();
$original_libraries = explode(',', $original_settings['ajaxPageState']['libraries']);
// Verify that the base page doesn't have the settings and files that are to
// be lazy loaded as part of the next requests.
$this->assertTrue(!isset($original_settings[$expected['setting_name']]), format_string('Page originally lacks the %setting, as expected.', array('%setting' => $expected['setting_name'])));
$this->assertTrue(!in_array($expected['library_1'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', array('%library' => $expected['library_1'])));
$this->assertTrue(!in_array($expected['library_2'], $original_libraries), format_string('Page originally lacks the %library library, as expected.', array('%library' => $expected['library_2'])));
// Calculate the expected CSS and JS.
$assets = new AttachedAssets();
$assets->setLibraries([$expected['library_1']])
->setAlreadyLoadedLibraries($original_libraries);
$css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
$expected_css_html = $renderer->renderRoot($css_render_array);
$assets->setLibraries([$expected['library_2']])
->setAlreadyLoadedLibraries($original_libraries);
$js_assets = $asset_resolver->getJsAssets($assets, FALSE)[1];
unset($js_assets['drupalSettings']);
$js_render_array = $js_collection_renderer->render($js_assets);
$expected_js_html = $renderer->renderRoot($js_render_array);
// Submit the AJAX request without triggering files getting added.
$commands = $this->drupalPostAjaxForm(NULL, array('add_files' => FALSE), array('op' => t('Submit')));
$new_settings = $this->getDrupalSettings();
$new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
// Verify the setting was not added when not expected.
$this->assertTrue(!isset($new_settings[$expected['setting_name']]), format_string('Page still lacks the %setting, as expected.', array('%setting' => $expected['setting_name'])));
$this->assertTrue(!in_array($expected['library_1'], $new_libraries), format_string('Page still lacks the %library library, as expected.', array('%library' => $expected['library_1'])));
$this->assertTrue(!in_array($expected['library_2'], $new_libraries), format_string('Page still lacks the %library library, as expected.', array('%library' => $expected['library_2'])));
// Verify a settings command does not add CSS or scripts to drupalSettings
// and no command inserts the corresponding tags on the page.
$found_settings_command = FALSE;
$found_markup_command = FALSE;
foreach ($commands as $command) {
if ($command['command'] == 'settings' && (array_key_exists('css', $command['settings']['ajaxPageState']) || array_key_exists('js', $command['settings']['ajaxPageState']))) {
$found_settings_command = TRUE;
}
if (isset($command['data']) && ($command['data'] == $expected_js_html || $command['data'] == $expected_css_html)) {
$found_markup_command = TRUE;
}
}
$this->assertFalse($found_settings_command, format_string('Page state still lacks the %library_1 and %library_2 libraries, as expected.', array('%library_1' => $expected['library_1'], '%library_2' => $expected['library_2'])));
$this->assertFalse($found_markup_command, format_string('Page still lacks the %library_1 and %library_2 libraries, as expected.', array('%library_1' => $expected['library_1'], '%library_2' => $expected['library_2'])));
// Submit the AJAX request and trigger adding files.
$commands = $this->drupalPostAjaxForm(NULL, array('add_files' => TRUE), array('op' => t('Submit')));
$new_settings = $this->getDrupalSettings();
$new_libraries = explode(',', $new_settings['ajaxPageState']['libraries']);
// Verify the expected setting was added, both to drupalSettings, and as
// the first AJAX command.
$this->assertIdentical($new_settings[$expected['setting_name']], $expected['setting_value'], format_string('Page now has the %setting.', array('%setting' => $expected['setting_name'])));
$expected_command = new SettingsCommand(array($expected['setting_name'] => $expected['setting_value']), TRUE);
$this->assertCommand(array_slice($commands, 0, 1), $expected_command->render(), format_string('The settings command was first.'));
// Verify the expected CSS file was added, both to drupalSettings, and as
// the second AJAX command for inclusion into the HTML.
$this->assertTrue(in_array($expected['library_1'], $new_libraries), format_string('Page state now has the %library library.', array('%library' => $expected['library_1'])));
$this->assertCommand(array_slice($commands, 1, 1), array('data' => $expected_css_html), format_string('Page now has the %library library.', array('%library' => $expected['library_1'])));
// Verify the expected JS file was added, both to drupalSettings, and as
// the third AJAX command for inclusion into the HTML. By testing for an
// exact HTML string containing the SCRIPT tag, we also ensure that
// unexpected JavaScript code, such as a jQuery.extend() that would
// potentially clobber rather than properly merge settings, didn't
// accidentally get added.
$this->assertTrue(in_array($expected['library_2'], $new_libraries), format_string('Page state now has the %library library.', array('%library' => $expected['library_2'])));
$this->assertCommand(array_slice($commands, 2, 1), array('data' => $expected_js_html), format_string('Page now has the %library library.', array('%library' => $expected['library_2'])));
}
/**
* Tests that drupalSettings.currentPath is not updated on AJAX requests.
*/
public function testCurrentPathChange() {
$commands = $this->drupalPostAjaxForm('ajax_forms_test_lazy_load_form', array('add_files' => FALSE), array('op' => t('Submit')));
foreach ($commands as $command) {
if ($command['command'] == 'settings') {
$this->assertFalse(isset($command['settings']['currentPath']), 'Value of drupalSettings.currentPath is not updated after an AJAX request.');
}
}
}
/**
* Tests that overridden CSS files are not added during lazy load.
*/
public function testLazyLoadOverriddenCSS() {
// The test theme overrides system.module.css without an implementation,
// thereby removing it.
\Drupal::service('theme_handler')->install(array('test_theme'));
$this->config('system.theme')
->set('default', 'test_theme')
->save();
// This gets the form, and emulates an Ajax submission on it, including
// adding markup to the HEAD and BODY for any lazy loaded JS/CSS files.
$this->drupalPostAjaxForm('ajax_forms_test_lazy_load_form', array('add_files' => TRUE), array('op' => t('Submit')));
// Verify that the resulting HTML does not load the overridden CSS file.
// We add a "?" to the assertion, because drupalSettings may include
// information about the file; we only really care about whether it appears
// in a LINK or STYLE tag, for which Drupal always adds a query string for
// cache control.
$this->assertNoText('system.module.css?', 'Ajax lazy loading does not add overridden CSS files.');
}
}

View file

@ -0,0 +1,100 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Ajax\MultiFormTest.
*/
namespace Drupal\system\Tests\Ajax;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Tests that AJAX-enabled forms work when multiple instances of the same form
* are on a page.
*
* @group Ajax
*/
class MultiFormTest extends AjaxTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('form_test');
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Page'));
// Create a multi-valued field for 'page' nodes to use for Ajax testing.
$field_name = 'field_ajax_test';
entity_create('field_storage_config', array(
'entity_type' => 'node',
'field_name' => $field_name,
'type' => 'text',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
))->save();
entity_create('field_config', array(
'field_name' => $field_name,
'entity_type' => 'node',
'bundle' => 'page',
))->save();
entity_get_form_display('node', 'page', 'default')
->setComponent($field_name, array('type' => 'text_textfield'))
->save();
// Login a user who can create 'page' nodes.
$this->drupalLogin ($this->drupalCreateUser(array('create page content')));
}
/**
* Tests that pages with the 'node_page_form' included twice work correctly.
*/
function testMultiForm() {
// HTML IDs for elements within the field are potentially modified with
// each Ajax submission, but these variables are stable and help target the
// desired elements.
$field_name = 'field_ajax_test';
$form_xpath = '//form[starts-with(@id, "node-page-form")]';
$field_xpath = '//div[contains(@class, "field-name-field-ajax-test")]';
$button_name = $field_name . '_add_more';
$button_value = t('Add another item');
$button_xpath_suffix = '//input[@name="' . $button_name . '"]';
$field_items_xpath_suffix = '//input[@type="text"]';
// Ensure the initial page contains both node forms and the correct number
// of field items and "add more" button for the multi-valued field within
// each form.
$this->drupalGet('form-test/two-instances-of-same-form');
$fields = $this->xpath($form_xpath . $field_xpath);
$this->assertEqual(count($fields), 2);
foreach ($fields as $field) {
$this->assertEqual(count($field->xpath('.' . $field_items_xpath_suffix)), 1, 'Found the correct number of field items on the initial page.');
$this->assertFieldsByValue($field->xpath('.' . $button_xpath_suffix), NULL, 'Found the "add more" button on the initial page.');
}
$this->assertNoDuplicateIds(t('Initial page contains unique IDs'), 'Other');
// Submit the "add more" button of each form twice. After each corresponding
// page update, ensure the same as above.
for ($i = 0; $i < 2; $i++) {
$forms = $this->xpath($form_xpath);
foreach ($forms as $offset => $form) {
$form_html_id = (string) $form['id'];
$this->drupalPostAjaxForm(NULL, array(), array($button_name => $button_value), NULL, array(), array(), $form_html_id);
$form = $this->xpath($form_xpath)[$offset];
$field = $form->xpath('.' . $field_xpath);
$this->assertEqual(count($field[0]->xpath('.' . $field_items_xpath_suffix)), $i+2, 'Found the correct number of field items after an AJAX submission.');
$this->assertFieldsByValue($field[0]->xpath('.' . $button_xpath_suffix), NULL, 'Found the "add more" button after an AJAX submission.');
$this->assertNoDuplicateIds(t('Updated page contains unique IDs'), 'Other');
}
}
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Asset\LibraryDiscoveryIntegrationTest.
*/
namespace Drupal\system\Tests\Asset;
use Drupal\simpletest\KernelTestBase;
/**
* Tests the element info.
*
* @group Render
*/
class LibraryDiscoveryIntegrationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->container->get('theme_handler')->install(['test_theme', 'classy']);
}
/**
* Ensures that the element info can be altered by themes.
*/
public function testElementInfoByTheme() {
/** @var \Drupal\Core\Theme\ThemeInitializationInterface $theme_initializer */
$theme_initializer = $this->container->get('theme.initialization');
/** @var \Drupal\Core\Theme\ThemeManagerInterface $theme_manager */
$theme_manager = $this->container->get('theme.manager');
/** @var \Drupal\Core\Render\ElementInfoManagerInterface $element_info */
$library_discovery = $this->container->get('library.discovery');
$theme_manager->setActiveTheme($theme_initializer->getActiveThemeByName('test_theme'));
$this->assertTrue($library_discovery->getLibraryByName('test_theme', 'kitten'));
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Batch\PageTest.
*/
namespace Drupal\system\Tests\Batch;
use Drupal\simpletest\WebTestBase;
/**
* Tests the content of the progress page.
*
* @group Batch
*/
class PageTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('batch_test');
/**
* Tests that the batch API progress page uses the correct theme.
*/
function testBatchProgressPageTheme() {
// Make sure that the page which starts the batch (an administrative page)
// is using a different theme than would normally be used by the batch API.
$this->container->get('theme_handler')->install(array('seven', 'bartik'));
$this->config('system.theme')
->set('default', 'bartik')
->set('admin', 'seven')
->save();
// Log in as an administrator who can see the administrative theme.
$admin_user = $this->drupalCreateUser(array('view the administration theme'));
$this->drupalLogin($admin_user);
// Visit an administrative page that runs a test batch, and check that the
// theme that was used during batch execution (which the batch callback
// function saved as a variable) matches the theme used on the
// administrative page.
$this->drupalGet('admin/batch-test/test-theme');
// The stack should contain the name of the theme used on the progress
// page.
$this->assertEqual(batch_test_stack(), array('seven'), 'A progressive batch correctly uses the theme of the page that started the batch.');
}
/**
* Tests that the batch API progress page shows the title correctly.
*/
function testBatchProgressPageTitle() {
// Visit an administrative page that runs a test batch, and check that the
// title shown during batch execution (which the batch callback function
// saved as a variable) matches the theme used on the administrative page.
$this->drupalGet('batch-test/test-title');
// The stack should contain the title shown on the progress page.
$this->assertEqual(batch_test_stack(), ['Batch Test'], 'The batch title is shown on the batch page.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
}

View file

@ -0,0 +1,279 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Batch\ProcessingTest.
*/
namespace Drupal\system\Tests\Batch;
use Drupal\simpletest\WebTestBase;
/**
* Tests batch processing in form and non-form workflow.
*
* @group Batch
*/
class ProcessingTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('batch_test');
/**
* Tests batches triggered outside of form submission.
*/
function testBatchNoForm() {
// Displaying the page triggers batch 1.
$this->drupalGet('batch-test/no-form');
$this->assertBatchMessages($this->_resultMessages('batch_1'), 'Batch for step 2 performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Tests batches defined in a form submit handler.
*/
function testBatchForm() {
// Batch 0: no operation.
$edit = array('batch' => 'batch_0');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_0'), 'Batch with no operation performed successfully.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
// Batch 1: several simple operations.
$edit = array('batch' => 'batch_1');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_1'), 'Batch with simple operations performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
// Batch 2: one multistep operation.
$edit = array('batch' => 'batch_2');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_2'), 'Batch with multistep operation performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_2'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
// Batch 3: simple + multistep combined.
$edit = array('batch' => 'batch_3');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_3'), 'Batch with simple and multistep operations performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_3'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
// Batch 4: nested batch.
$edit = array('batch' => 'batch_4');
$this->drupalPostForm('batch-test', $edit, 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_4'), 'Nested batch performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_4'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Tests batches defined in a multistep form.
*/
function testBatchFormMultistep() {
$this->drupalGet('batch-test/multistep');
$this->assertText('step 1', 'Form is displayed in step 1.');
// First step triggers batch 1.
$this->drupalPostForm(NULL, array(), 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_1'), 'Batch for step 1 performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), 'Execution order was correct.');
$this->assertText('step 2', 'Form is displayed in step 2.');
// Second step triggers batch 2.
$this->drupalPostForm(NULL, array(), 'Submit');
$this->assertBatchMessages($this->_resultMessages('batch_2'), 'Batch for step 2 performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_2'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Tests batches defined in different submit handlers on the same form.
*/
function testBatchFormMultipleBatches() {
// Batches 1, 2 and 3 are triggered in sequence by different submit
// handlers. Each submit handler modify the submitted 'value'.
$value = rand(0, 255);
$edit = array('value' => $value);
$this->drupalPostForm('batch-test/chained', $edit, 'Submit');
// Check that result messages are present and in the correct order.
$this->assertBatchMessages($this->_resultMessages('chained'), 'Batches defined in separate submit handlers performed successfully.');
// The stack contains execution order of batch callbacks and submit
// handlers and logging of corresponding $form_state->getValues().
$this->assertEqual(batch_test_stack(), $this->_resultStack('chained', $value), 'Execution order was correct, and $form_state is correctly persisted.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Tests batches defined in a programmatically submitted form.
*
* Same as above, but the form is submitted through drupal_form_execute().
*/
function testBatchFormProgrammatic() {
// Batches 1, 2 and 3 are triggered in sequence by different submit
// handlers. Each submit handler modify the submitted 'value'.
$value = rand(0, 255);
$this->drupalGet('batch-test/programmatic/' . $value);
// Check that result messages are present and in the correct order.
$this->assertBatchMessages($this->_resultMessages('chained'), 'Batches defined in separate submit handlers performed successfully.');
// The stack contains execution order of batch callbacks and submit
// handlers and logging of corresponding $form_state->getValues().
$this->assertEqual(batch_test_stack(), $this->_resultStack('chained', $value), 'Execution order was correct, and $form_state is correctly persisted.');
$this->assertText('Got out of a programmatic batched form.', 'Page execution continues normally.');
}
/**
* Test form submission during a batch operation.
*/
function testDrupalFormSubmitInBatch() {
// Displaying the page triggers a batch that programmatically submits a
// form.
$value = rand(0, 255);
$this->drupalGet('batch-test/nested-programmatic/' . $value);
$this->assertEqual(batch_test_stack(), array('mock form submitted with value = ' . $value), '\Drupal::formBuilder()->submitForm() ran successfully within a batch operation.');
}
/**
* Tests batches that return $context['finished'] > 1 do in fact complete.
*
* @see https://www.drupal.org/node/600836
*/
function testBatchLargePercentage() {
// Displaying the page triggers batch 5.
$this->drupalGet('batch-test/large-percentage');
$this->assertBatchMessages($this->_resultMessages('batch_5'), 'Batch for step 2 performed successfully.');
$this->assertEqual(batch_test_stack(), $this->_resultStack('batch_5'), 'Execution order was correct.');
$this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
}
/**
* Triggers a pass if the texts were found in order in the raw content.
*
* @param $texts
* Array of raw strings to look for .
* @param $message
* Message to display.
*
* @return
* TRUE on pass, FALSE on fail.
*/
function assertBatchMessages($texts, $message) {
$pattern = '|' . implode('.*', $texts) .'|s';
return $this->assertPattern($pattern, $message);
}
/**
* Returns expected execution stacks for the test batches.
*/
function _resultStack($id, $value = 0) {
$stack = array();
switch ($id) {
case 'batch_1':
for ($i = 1; $i <= 10; $i++) {
$stack[] = "op 1 id $i";
}
break;
case 'batch_2':
for ($i = 1; $i <= 10; $i++) {
$stack[] = "op 2 id $i";
}
break;
case 'batch_3':
for ($i = 1; $i <= 5; $i++) {
$stack[] = "op 1 id $i";
}
for ($i = 1; $i <= 5; $i++) {
$stack[] = "op 2 id $i";
}
for ($i = 6; $i <= 10; $i++) {
$stack[] = "op 1 id $i";
}
for ($i = 6; $i <= 10; $i++) {
$stack[] = "op 2 id $i";
}
break;
case 'batch_4':
for ($i = 1; $i <= 5; $i++) {
$stack[] = "op 1 id $i";
}
$stack[] = 'setting up batch 2';
for ($i = 6; $i <= 10; $i++) {
$stack[] = "op 1 id $i";
}
$stack = array_merge($stack, $this->_resultStack('batch_2'));
break;
case 'batch_5':
for ($i = 1; $i <= 10; $i++) {
$stack[] = "op 5 id $i";
}
break;
case 'chained':
$stack[] = 'submit handler 1';
$stack[] = 'value = ' . $value;
$stack = array_merge($stack, $this->_resultStack('batch_1'));
$stack[] = 'submit handler 2';
$stack[] = 'value = ' . ($value + 1);
$stack = array_merge($stack, $this->_resultStack('batch_2'));
$stack[] = 'submit handler 3';
$stack[] = 'value = ' . ($value + 2);
$stack[] = 'submit handler 4';
$stack[] = 'value = ' . ($value + 3);
$stack = array_merge($stack, $this->_resultStack('batch_3'));
break;
}
return $stack;
}
/**
* Returns expected result messages for the test batches.
*/
function _resultMessages($id) {
$messages = array();
switch ($id) {
case 'batch_0':
$messages[] = 'results for batch 0<br>none';
break;
case 'batch_1':
$messages[] = 'results for batch 1<br>op 1: processed 10 elements';
break;
case 'batch_2':
$messages[] = 'results for batch 2<br>op 2: processed 10 elements';
break;
case 'batch_3':
$messages[] = 'results for batch 3<br>op 1: processed 10 elements<br>op 2: processed 10 elements';
break;
case 'batch_4':
$messages[] = 'results for batch 4<br>op 1: processed 10 elements';
$messages = array_merge($messages, $this->_resultMessages('batch_2'));
break;
case 'batch_5':
$messages[] = 'results for batch 5<br>op 5: processed 10 elements';
break;
case 'chained':
$messages = array_merge($messages, $this->_resultMessages('batch_1'));
$messages = array_merge($messages, $this->_resultMessages('batch_2'));
$messages = array_merge($messages, $this->_resultMessages('batch_3'));
break;
}
return $messages;
}
}

View file

@ -0,0 +1,314 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Block\SystemMenuBlockTest.
*/
namespace Drupal\system\Tests\Block;
use Drupal\Core\Render\Element;
use Drupal\simpletest\KernelTestBase;
use Drupal\system\Tests\Routing\MockRouteProvider;
use Drupal\Tests\Core\Menu\MenuLinkMock;
use Drupal\user\Entity\User;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Tests \Drupal\system\Plugin\Block\SystemMenuBlock.
*
* @group Block
* @todo Expand test coverage to all SystemMenuBlock functionality, including
* block_menu_delete().
*
* @see \Drupal\system\Plugin\Derivative\SystemMenuBlock
* @see \Drupal\system\Plugin\Block\SystemMenuBlock
*/
class SystemMenuBlockTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array(
'system',
'block',
'menu_test',
'menu_link_content',
'field',
'user',
'link',
);
/**
* The block under test.
*
* @var \Drupal\system\Plugin\Block\SystemMenuBlock
*/
protected $block;
/**
* The menu for testing.
*
* @var \Drupal\system\MenuInterface
*/
protected $menu;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTree
*/
protected $linkTree;
/**
* The menu link plugin manager service.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
*/
protected $menuLinkManager;
/**
* The block manager service.
*
* @var \Drupal\Core\block\BlockManagerInterface
*/
protected $blockManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', 'sequences');
$this->installEntitySchema('user');
$this->installSchema('system', array('router'));
$this->installEntitySchema('menu_link_content');
$account = User::create([
'name' => $this->randomMachineName(),
'status' => 1,
]);
$account->save();
$this->container->get('current_user')->setAccount($account);
$this->menuLinkManager = $this->container->get('plugin.manager.menu.link');
$this->linkTree = $this->container->get('menu.link_tree');
$this->blockManager = $this->container->get('plugin.manager.block');
$routes = new RouteCollection();
$requirements = array('_access' => 'TRUE');
$options = array('_access_checks' => array('access_check.default'));
$routes->add('example1', new Route('/example1', array(), $requirements, $options));
$routes->add('example2', new Route('/example2', array(), $requirements, $options));
$routes->add('example3', new Route('/example3', array(), $requirements, $options));
$routes->add('example4', new Route('/example4', array(), $requirements, $options));
$routes->add('example5', new Route('/example5', array(), $requirements, $options));
$routes->add('example6', new Route('/example6', array(), $requirements, $options));
$routes->add('example7', new Route('/example7', array(), $requirements, $options));
$routes->add('example8', new Route('/example8', array(), $requirements, $options));
$mock_route_provider = new MockRouteProvider($routes);
$this->container->set('router.route_provider', $mock_route_provider);
// Add a new custom menu.
$menu_name = 'mock';
$label = $this->randomMachineName(16);
$this->menu = entity_create('menu', array(
'id' => $menu_name,
'label' => $label,
'description' => 'Description text',
));
$this->menu->save();
// This creates a tree with the following structure:
// - 1
// - 2
// - 3
// - 4
// - 5
// - 7
// - 6
// - 8
// With link 6 being the only external link.
$links = array(
1 => MenuLinkMock::create(array('id' => 'test.example1', 'route_name' => 'example1', 'title' => 'foo', 'parent' => '', 'weight' => 0)),
2 => MenuLinkMock::create(array('id' => 'test.example2', 'route_name' => 'example2', 'title' => 'bar', 'parent' => '', 'route_parameters' => array('foo' => 'bar'), 'weight' => 1)),
3 => MenuLinkMock::create(array('id' => 'test.example3', 'route_name' => 'example3', 'title' => 'baz', 'parent' => 'test.example2', 'weight' => 2)),
4 => MenuLinkMock::create(array('id' => 'test.example4', 'route_name' => 'example4', 'title' => 'qux', 'parent' => 'test.example3', 'weight' => 3)),
5 => MenuLinkMock::create(array('id' => 'test.example5', 'route_name' => 'example5', 'title' => 'foofoo', 'parent' => '', 'expanded' => TRUE, 'weight' => 4)),
6 => MenuLinkMock::create(array('id' => 'test.example6', 'route_name' => '', 'url' => 'https://www.drupal.org/', 'title' => 'barbar', 'parent' => '', 'weight' => 5)),
7 => MenuLinkMock::create(array('id' => 'test.example7', 'route_name' => 'example7', 'title' => 'bazbaz', 'parent' => 'test.example5', 'weight' => 6)),
8 => MenuLinkMock::create(array('id' => 'test.example8', 'route_name' => 'example8', 'title' => 'quxqux', 'parent' => '', 'weight' => 7)),
);
foreach ($links as $instance) {
$this->menuLinkManager->addDefinition($instance->getPluginId(), $instance->getPluginDefinition());
}
}
/**
* Tests calculation of a system menu block's configuration dependencies.
*/
public function testSystemMenuBlockConfigDependencies() {
$block = entity_create('block', array(
'plugin' => 'system_menu_block:' . $this->menu->id(),
'region' => 'footer',
'id' => 'machinename',
'theme' => 'stark',
));
$dependencies = $block->calculateDependencies();
$expected = array(
'config' => array(
'system.menu.' . $this->menu->id()
),
'module' => array(
'system'
),
'theme' => array(
'stark'
),
);
$this->assertIdentical($expected, $dependencies);
}
/**
* Tests the config start level and depth.
*/
public function testConfigLevelDepth() {
// Helper function to generate a configured block instance.
$place_block = function ($level, $depth) {
return $this->blockManager->createInstance('system_menu_block:' . $this->menu->id(), array(
'region' => 'footer',
'id' => 'machinename',
'theme' => 'stark',
'level' => $level,
'depth' => $depth,
));
};
// All the different block instances we're going to test.
$blocks = [
'all' => $place_block(1, 0),
'level_1_only' => $place_block(1, 1),
'level_2_only' => $place_block(2, 1),
'level_3_only' => $place_block(3, 1),
'level_1_and_beyond' => $place_block(1, 0),
'level_2_and_beyond' => $place_block(2, 0),
'level_3_and_beyond' => $place_block(3, 0),
];
// Scenario 1: test all block instances when there's no active trail.
$no_active_trail_expectations = [];
$no_active_trail_expectations['all'] = [
'test.example1' => [],
'test.example2' => [],
'test.example5' => [
'test.example7' => [],
],
'test.example6' => [],
'test.example8' => [],
];
$no_active_trail_expectations['level_1_only'] = [
'test.example1' => [],
'test.example2' => [],
'test.example5' => [],
'test.example6' => [],
'test.example8' => [],
];
$no_active_trail_expectations['level_2_only'] = [
'test.example7' => [],
];
$no_active_trail_expectations['level_3_only'] = [];
$no_active_trail_expectations['level_1_and_beyond'] = $no_active_trail_expectations['all'];
$no_active_trail_expectations['level_2_and_beyond'] = $no_active_trail_expectations['level_2_only'];
$no_active_trail_expectations['level_3_and_beyond'] = [];
foreach ($blocks as $id => $block) {
$block_build = $block->build();
$items = isset($block_build['#items']) ? $block_build['#items'] : [];
$this->assertIdentical($no_active_trail_expectations[$id], $this->convertBuiltMenuToIdTree($items), format_string('Menu block %id with no active trail renders the expected tree.', ['%id' => $id]));
}
// Scenario 2: test all block instances when there's an active trail.
$route = $this->container->get('router.route_provider')->getRouteByName('example3');
$request = new Request();
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'example3');
$request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $route);
$this->container->get('request_stack')->push($request);
// \Drupal\Core\Menu\MenuActiveTrail uses the cache collector pattern, which
// includes static caching. Since this second scenario simulates a second
// request, we must also simulate it for the MenuActiveTrail service, by
// clearing the cache collector's static cache.
\Drupal::service('menu.active_trail')->clear();
$active_trail_expectations = [];
$active_trail_expectations['all'] = [
'test.example1' => [],
'test.example2' => [
'test.example3' => [
'test.example4' => [],
]
],
'test.example5' => [
'test.example7' => [],
],
'test.example6' => [],
'test.example8' => [],
];
$active_trail_expectations['level_1_only'] = [
'test.example1' => [],
'test.example2' => [],
'test.example5' => [],
'test.example6' => [],
'test.example8' => [],
];
$active_trail_expectations['level_2_only'] = [
'test.example3' => [],
'test.example7' => [],
];
$active_trail_expectations['level_3_only'] = [
'test.example4' => [],
];
$active_trail_expectations['level_1_and_beyond'] = $active_trail_expectations['all'];
$active_trail_expectations['level_2_and_beyond'] = [
'test.example3' => [
'test.example4' => [],
],
'test.example7' => [],
];
$active_trail_expectations['level_3_and_beyond'] = $active_trail_expectations['level_3_only'];
foreach ($blocks as $id => $block) {
$block_build = $block->build();
$items = isset($block_build['#items']) ? $block_build['#items'] : [];
$this->assertIdentical($active_trail_expectations[$id], $this->convertBuiltMenuToIdTree($items), format_string('Menu block %id with an active trail renders the expected tree.', ['%id' => $id]));
}
}
/**
* Helper method to allow for easy menu link tree structure assertions.
*
* Converts the result of MenuLinkTree::build() in a "menu link ID tree".
*
* @param array $build
* The return value of of MenuLinkTree::build()
*
* @return array
* The "menu link ID tree" representation of the given render array.
*/
protected function convertBuiltMenuToIdTree(array $build) {
$level = [];
foreach (Element::children($build) as $id) {
$level[$id] = [];
if (isset($build[$id]['below'])) {
$level[$id] = $this->convertBuiltMenuToIdTree($build[$id]['below']);
}
}
return $level;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Bootstrap\DrupalSetMessageTest.
*/
namespace Drupal\system\Tests\Bootstrap;
use Drupal\simpletest\WebTestBase;
/**
* Tests drupal_set_message() and related functions.
*
* @group Bootstrap
*/
class DrupalSetMessageTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system_test');
/**
* Tests setting messages and removing one before it is displayed.
*/
function testSetRemoveMessages() {
// The page at system-test/drupal-set-message sets two messages and then
// removes the first before it is displayed.
$this->drupalGet('system-test/drupal-set-message');
$this->assertNoText('First message (removed).');
$this->assertText('Second message (not removed).');
}
/**
* Tests setting duplicated messages.
*/
function testDuplicatedMessages() {
$this->drupalGet('system-test/drupal-set-message');
$this->assertUniqueText('Non Duplicated message');
$this->assertNoUniqueText('Duplicated message');
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Bootstrap\GetFilenameUnitTest.
*/
namespace Drupal\system\Tests\Bootstrap;
use Drupal\simpletest\KernelTestBase;
/**
* Tests that drupal_get_filename() works correctly.
*
* @group Bootstrap
*/
class GetFilenameUnitTest extends KernelTestBase {
/**
* 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');
system_rebuild_module_data();
// Retrieving the location of a module.
$this->assertIdentical(drupal_get_filename('module', 'system'), 'core/modules/system/system.info.yml');
// Retrieving the location of a theme.
\Drupal::service('theme_handler')->install(array('stark'));
$this->assertIdentical(drupal_get_filename('theme', 'stark'), 'core/themes/stark/stark.info.yml');
// Retrieving the location of a theme engine.
$this->assertIdentical(drupal_get_filename('theme_engine', 'twig'), 'core/themes/engines/twig/twig.info.yml');
// Retrieving the location of a profile. Profiles are a special case with
// a fixed location and naming.
$this->assertIdentical(drupal_get_filename('profile', 'testing'), 'core/profiles/testing/testing.info.yml');
// Generate a non-existing module name.
$non_existing_module = uniqid("", TRUE);
// Set a custom error handler so we can ignore the file not found error.
set_error_handler(function($severity, $message, $file, $line) {
// Skip error handling if this is a "file not found" error.
if (strstr($message, 'is missing from the file system:')) {
\Drupal::state()->set('get_filename_test_triggered_error', TRUE);
return;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
$this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for an item that does not exist returns NULL.');
$this->assertTrue(\Drupal::state()->get('get_filename_test_triggered_error'), 'Searching for an item that does not exist triggers an error.');
// Restore the original error handler.
restore_error_handler();
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Bootstrap\ResettableStaticUnitTest.
*/
namespace Drupal\system\Tests\Bootstrap;
use Drupal\simpletest\KernelTestBase;
/**
* Tests that drupal_static() and drupal_static_reset() work.
*
* @group Bootstrap
*/
class ResettableStaticUnitTest extends KernelTestBase {
/**
* Tests drupal_static() function.
*
* Tests that a variable reference returned by drupal_static() gets reset when
* drupal_static_reset() is called.
*/
function testDrupalStatic() {
$name = __CLASS__ . '_' . __METHOD__;
$var = &drupal_static($name, 'foo');
$this->assertEqual($var, 'foo', 'Variable returned by drupal_static() was set to its default.');
// Call the specific reset and the global reset each twice to ensure that
// multiple resets can be issued without odd side effects.
$var = 'bar';
drupal_static_reset($name);
$this->assertEqual($var, 'foo', 'Variable was reset after first invocation of name-specific reset.');
$var = 'bar';
drupal_static_reset($name);
$this->assertEqual($var, 'foo', 'Variable was reset after second invocation of name-specific reset.');
$var = 'bar';
drupal_static_reset();
$this->assertEqual($var, 'foo', 'Variable was reset after first invocation of global reset.');
$var = 'bar';
drupal_static_reset();
$this->assertEqual($var, 'foo', 'Variable was reset after second invocation of global reset.');
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\ApcuBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\ApcuBackend;
/**
* Tests the APCu cache backend.
*
* @group Cache
* @requires extension apc
*/
class ApcuBackendUnitTest extends GenericCacheBackendUnitTestBase {
protected function checkRequirements() {
$requirements = parent::checkRequirements();
if (!extension_loaded('apc')) {
$requirements[] = 'APC extension not found.';
}
else {
if (version_compare(phpversion('apc'), '3.1.1', '<')) {
$requirements[] = 'APC extension must be newer than 3.1.1 for APCIterator support.';
}
if (PHP_SAPI === 'cli' && !ini_get('apc.enable_cli')) {
$requirements[] = 'apc.enable_cli must be enabled to run this test.';
}
}
return $requirements;
}
protected function createCacheBackend($bin) {
return new ApcuBackend($bin, $this->databasePrefix, \Drupal::service('cache_tags.invalidator.checksum'));
}
protected function tearDown() {
foreach ($this->cachebackends as $bin => $cachebackend) {
$this->cachebackends[$bin]->removeBin();
}
parent::tearDown();
}
}

View file

@ -0,0 +1,147 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Url;
/**
* Provides test assertions for testing page-level cache contexts & tags.
*
* Can be used by test classes that extend \Drupal\simpletest\WebTestBase.
*/
trait AssertPageCacheContextsAndTagsTrait {
/**
* Enables page caching.
*/
protected function enablePageCaching() {
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 300);
$config->save();
}
/**
* Gets a specific header value as array.
*
* @param string $header_name
* The header name.
*
* @return string[]
* The header value, potentially exploded by spaces.
*/
protected function getCacheHeaderValues($header_name) {
$header_value = $this->drupalGetHeader($header_name);
if (empty($header_value)) {
return [];
}
else {
return explode(' ', $header_value);
}
}
/**
* Asserts page cache miss, then hit for the given URL; checks cache headers.
*
* @param \Drupal\Core\Url $url
* The URL to test.
* @param string[] $expected_contexts
* The expected cache contexts for the given URL.
* @param string[] $expected_tags
* The expected cache tags for the given URL.
*/
protected function assertPageCacheContextsAndTags(Url $url, array $expected_contexts, array $expected_tags) {
$absolute_url = $url->setAbsolute()->toString();
sort($expected_contexts);
sort($expected_tags);
// Assert cache miss + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
$this->assertCacheTags($expected_tags);
$this->assertCacheContexts($expected_contexts);
// Assert cache hit + expected cache contexts + tags.
$this->drupalGet($absolute_url);
$this->assertCacheTags($expected_tags);
$this->assertCacheContexts($expected_contexts);
// Assert page cache item + expected cache tags.
$cid_parts = array($url->setAbsolute()->toString(), 'html');
$cid = implode(':', $cid_parts);
$cache_entry = \Drupal::cache('render')->get($cid);
sort($cache_entry->tags);
$this->assertEqual($cache_entry->tags, $expected_tags);
$this->debugCacheTags($cache_entry->tags, $expected_tags);
}
/**
* Provides debug information for cache tags.
*
* @param string[] $actual_tags
* The actual cache tags.
* @param string[] $expected_tags
* The expected cache tags.
*/
protected function debugCacheTags(array $actual_tags, array $expected_tags) {
if ($actual_tags !== $expected_tags) {
debug('Missing cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
debug('Unwanted cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
}
}
/**
* Ensures that some cache tags are present in the current response.
*
* @param string[] $expected_tags
* The expected tags.
*/
protected function assertCacheTags(array $expected_tags) {
$actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags');
sort($expected_tags);
sort($actual_tags);
$this->assertIdentical($actual_tags, $expected_tags);
$this->debugCacheTags($actual_tags, $expected_tags);
}
/**
* Ensures that some cache contexts are present in the current response.
*
* @param string[] $expected_contexts
* The expected cache contexts.
* @param string $message
* (optional) A verbose message to output.
*
* @return
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertCacheContexts(array $expected_contexts, $message = NULL) {
$actual_contexts = $this->getCacheHeaderValues('X-Drupal-Cache-Contexts');
sort($expected_contexts);
sort($actual_contexts);
$return = $this->assertIdentical($actual_contexts, $expected_contexts, $message);
if (!$return) {
debug('Missing cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
debug('Unwanted cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
}
return $return;
}
/**
* Asserts the max age header.
*
* @param int $max_age
*/
protected function assertCacheMaxAge($max_age) {
$cache_control_header = $this->drupalGetHeader('Cache-Control');
if (strpos($cache_control_header, 'max-age:' . $max_age) === FALSE) {
debug('Expected max-age:' . $max_age . '; Response max-age:' . $cache_control_header);
}
$this->assertTrue(strpos($cache_control_header, 'max-age:' . $max_age));
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\BackendChainUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\BackendChain;
use Drupal\Core\Cache\MemoryBackend;
/**
* Unit test of the backend chain using the generic cache unit test base.
*
* @group Cache
*/
class BackendChainUnitTest extends GenericCacheBackendUnitTestBase {
protected function createCacheBackend($bin) {
$chain = new BackendChain($bin);
// We need to create some various backends in the chain.
$chain
->appendBackend(new MemoryBackend('foo'))
->prependBackend(new MemoryBackend('bar'))
->appendBackend(new MemoryBackend('baz'));
\Drupal::service('cache_tags.invalidator')->addInvalidator($chain);
return $chain;
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\CacheTestBase.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\simpletest\WebTestBase;
/**
* Provides helper methods for cache tests.
*/
abstract class CacheTestBase extends WebTestBase {
protected $defaultBin = 'render';
protected $defaultCid = 'test_temporary';
protected $defaultValue = 'CacheTest';
/**
* Checks whether or not a cache entry exists.
*
* @param $cid
* The cache id.
* @param $var
* The variable the cache should contain.
* @param $bin
* The bin the cache item was stored in.
* @return
* TRUE on pass, FALSE on fail.
*/
protected function checkCacheExists($cid, $var, $bin = NULL) {
if ($bin == NULL) {
$bin = $this->defaultBin;
}
$cached = \Drupal::cache($bin)->get($cid);
return isset($cached->data) && $cached->data == $var;
}
/**
* Asserts that a cache entry exists.
*
* @param $message
* Message to display.
* @param $var
* The variable the cache should contain.
* @param $cid
* The cache id.
* @param $bin
* The bin the cache item was stored in.
*/
protected function assertCacheExists($message, $var = NULL, $cid = NULL, $bin = NULL) {
if ($bin == NULL) {
$bin = $this->defaultBin;
}
if ($cid == NULL) {
$cid = $this->defaultCid;
}
if ($var == NULL) {
$var = $this->defaultValue;
}
$this->assertTrue($this->checkCacheExists($cid, $var, $bin), $message);
}
/**
* Asserts that a cache entry has been removed.
*
* @param $message
* Message to display.
* @param $cid
* The cache id.
* @param $bin
* The bin the cache item was stored in.
*/
function assertCacheRemoved($message, $cid = NULL, $bin = NULL) {
if ($bin == NULL) {
$bin = $this->defaultBin;
}
if ($cid == NULL) {
$cid = $this->defaultCid;
}
$cached = \Drupal::cache($bin)->get($cid);
$this->assertFalse($cached, $message);
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\ChainedFastBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\ChainedFastBackend;
use Drupal\Core\Cache\DatabaseBackend;
use Drupal\Core\Cache\PhpBackend;
/**
* Unit test of the fast chained backend using the generic cache unit test base.
*
* @group Cache
*/
class ChainedFastBackendUnitTest extends GenericCacheBackendUnitTestBase {
/**
* Creates a new instance of ChainedFastBackend.
*
* @return \Drupal\Core\Cache\ChainedFastBackend
* A new ChainedFastBackend object.
*/
protected function createCacheBackend($bin) {
$consistent_backend = new DatabaseBackend(\Drupal::service('database'), \Drupal::service('cache_tags.invalidator.checksum'), $bin);
$fast_backend = new PhpBackend($bin, \Drupal::service('cache_tags.invalidator.checksum'));
$backend = new ChainedFastBackend($consistent_backend, $fast_backend, $bin);
// Explicitly register the cache bin as it can not work through the
// cache bin list in the container.
\Drupal::service('cache_tags.invalidator')->addInvalidator($backend);
return $backend;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\ClearTest.
*/
namespace Drupal\system\Tests\Cache;
/**
* Tests our clearing is done the proper way.
*
* @group Cache
*/
use Drupal\Core\Cache\Cache;
class ClearTest extends CacheTestBase {
protected function setUp() {
$this->defaultBin = 'render';
$this->defaultValue = $this->randomMachineName(10);
parent::setUp();
}
/**
* Tests drupal_flush_all_caches().
*/
function testFlushAllCaches() {
// Create cache entries for each flushed cache bin.
$bins = Cache::getBins();
$this->assertTrue($bins, 'Cache::getBins() returned bins to flush.');
foreach ($bins as $bin => $cache_backend) {
$cid = 'test_cid_clear' . $bin;
$cache_backend->set($cid, $this->defaultValue);
}
// Remove all caches then make sure that they are cleared.
drupal_flush_all_caches();
foreach ($bins as $bin => $cache_backend) {
$cid = 'test_cid_clear' . $bin;
$this->assertFalse($this->checkCacheExists($cid, $this->defaultValue, $bin), format_string('All cache entries removed from @bin.', array('@bin' => $bin)));
}
}
}

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\DatabaseBackendTagTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\simpletest\KernelTestBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Tests DatabaseBackend cache tag implementation.
*
* @group Cache
*/
class DatabaseBackendTagTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system');
/**
* {@inheritdoc}
*/
public function containerBuild(ContainerBuilder $container) {
parent::containerBuild($container);
// Change container to database cache backends.
$container
->register('cache_factory', 'Drupal\Core\Cache\CacheFactory')
->addArgument(new Reference('settings'))
->addMethodCall('setContainer', array(new Reference('service_container')));
}
public function testTagInvalidations() {
// Create cache entry in multiple bins.
$tags = array('test_tag:1', 'test_tag:2', 'test_tag:3');
$bins = array('data', 'bootstrap', 'render');
foreach ($bins as $bin) {
$bin = \Drupal::cache($bin);
$bin->set('test', 'value', Cache::PERMANENT, $tags);
$this->assertTrue($bin->get('test'), 'Cache item was set in bin.');
}
$invalidations_before = intval(db_select('cachetags')->fields('cachetags', array('invalidations'))->condition('tag', 'test_tag:2')->execute()->fetchField());
Cache::invalidateTags(array('test_tag:2'));
// Test that cache entry has been invalidated in multiple bins.
foreach ($bins as $bin) {
$bin = \Drupal::cache($bin);
$this->assertFalse($bin->get('test'), 'Tag invalidation affected item in bin.');
}
// Test that only one tag invalidation has occurred.
$invalidations_after = intval(db_select('cachetags')->fields('cachetags', array('invalidations'))->condition('tag', 'test_tag:2')->execute()->fetchField());
$this->assertEqual($invalidations_after, $invalidations_before + 1, 'Only one addition cache tag invalidation has occurred after invalidating a tag used in multiple bins.');
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\DatabaseBackendUnitTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\DatabaseBackend;
/**
* Unit test of the database backend using the generic cache unit test base.
*
* @group Cache
*/
class DatabaseBackendUnitTest extends GenericCacheBackendUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system');
/**
* Creates a new instance of DatabaseBackend.
*
* @return
* A new DatabaseBackend object.
*/
protected function createCacheBackend($bin) {
return new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), $bin);
}
/**
* {@inheritdoc}
*/
public function testSetGet() {
parent::testSetGet();
$backend = $this->getCacheBackend();
// Set up a cache ID that is not ASCII and longer than 255 characters so we
// can test cache ID normalization.
$cid_long = str_repeat('愛€', 500);
$cached_value_long = $this->randomMachineName();
$backend->set($cid_long, $cached_value_long);
$this->assertIdentical($cached_value_long, $backend->get($cid_long)->data, "Backend contains the correct value for long, non-ASCII cache id.");
$cid_short = '愛1€';
$cached_value_short = $this->randomMachineName();
$backend->set($cid_short, $cached_value_short);
$this->assertIdentical($cached_value_short, $backend->get($cid_short)->data, "Backend contains the correct value for short, non-ASCII cache id.");
}
}

View file

@ -0,0 +1,624 @@
<?php
/**
* @file
* Contains \Drupal\system\Tests\Cache\GenericCacheBackendUnitTestBase.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\simpletest\KernelTestBase;
/**
* Tests any cache backend.
*
* Full generic unit test suite for any cache backend. In order to use it for a
* cache backend implementation, extend this class and override the
* createBackendInstance() method to return an object.
*
* @see DatabaseBackendUnitTestCase
* For a full working implementation.
*/
abstract class GenericCacheBackendUnitTestBase extends KernelTestBase {
/**
* Array of objects implementing Drupal\Core\Cache\CacheBackendInterface.
*
* @var array
*/
protected $cachebackends;
/**
* Cache bin to use for testing.
*
* @var string
*/
protected $testBin;
/**
* Random value to use in tests.
*
* @var string
*/
protected $defaultValue;
/**
* Gets the testing bin.
*
* Override this method if you want to work on a different bin than the
* default one.
*
* @return string
* Bin name.
*/
protected function getTestBin() {
if (!isset($this->testBin)) {
$this->testBin = 'page';
}
return $this->testBin;
}
/**
* Creates a cache backend to test.
*
* Override this method to test a CacheBackend.
*
* @param string $bin
* Bin name to use for this backend instance.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* Cache backend to test.
*/
protected abstract function createCacheBackend($bin);
/**
* Allows specific implementation to change the environment before a test run.
*/
public function setUpCacheBackend() {
}
/**
* Allows alteration of environment after a test run but before tear down.
*
* Used before the real tear down because the tear down will change things
* such as the database prefix.
*/
public function tearDownCacheBackend() {
}
/**
* Gets a backend to test; this will get a shared instance set in the object.
*
* @return \Drupal\Core\Cache\CacheBackendInterface
* Cache backend to test.
*/
protected function getCacheBackend($bin = null) {
if (!isset($bin)) {
$bin = $this->getTestBin();
}
if (!isset($this->cachebackends[$bin])) {
$this->cachebackends[$bin] = $this->createCacheBackend($bin);
// Ensure the backend is empty.
$this->cachebackends[$bin]->deleteAll();
}
return $this->cachebackends[$bin];
}
protected function setUp() {
$this->cachebackends = array();
$this->defaultValue = $this->randomMachineName(10);
parent::setUp();
$this->setUpCacheBackend();
}
protected function tearDown() {
// Destruct the registered backend, each test will get a fresh instance,
// properly emptying it here ensure that on persistent data backends they
// will come up empty the next test.
foreach ($this->cachebackends as $bin => $cachebackend) {
$this->cachebackends[$bin]->deleteAll();
}
unset($this->cachebackends);
$this->tearDownCacheBackend();
parent::tearDown();
}
/**
* Tests the get and set methods of Drupal\Core\Cache\CacheBackendInterface.
*/
public function testSetGet() {
$backend = $this->getCacheBackend();
$this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1.");
$with_backslash = array('foo' => '\Drupal\foo\Bar');
$backend->set('test1', $with_backslash);
$cached = $backend->get('test1');
$this->assert(is_object($cached), "Backend returned an object for cache id test1.");
$this->assertIdentical($with_backslash, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
// We need to round because microtime may be rounded up in the backend.
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.');
$this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2.");
$backend->set('test2', array('value' => 3), REQUEST_TIME + 3);
$cached = $backend->get('test2');
$this->assert(is_object($cached), "Backend returned an object for cache id test2.");
$this->assertIdentical(array('value' => 3), $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, REQUEST_TIME + 3, 'Expire time is correct.');
$backend->set('test3', 'foobar', REQUEST_TIME - 3);
$this->assertFalse($backend->get('test3'), 'Invalid item not returned.');
$cached = $backend->get('test3', TRUE);
$this->assert(is_object($cached), 'Backend returned an object for cache id test3.');
$this->assertFalse($cached->valid, 'Item is marked as valid.');
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, REQUEST_TIME - 3, 'Expire time is correct.');
$this->assertIdentical(FALSE, $backend->get('test4'), "Backend does not contain data for cache id test4.");
$with_eof = array('foo' => "\nEOF\ndata");
$backend->set('test4', $with_eof);
$cached = $backend->get('test4');
$this->assert(is_object($cached), "Backend returned an object for cache id test4.");
$this->assertIdentical($with_eof, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.');
$this->assertIdentical(FALSE, $backend->get('test5'), "Backend does not contain data for cache id test5.");
$with_eof_and_semicolon = array('foo' => "\nEOF;\ndata");
$backend->set('test5', $with_eof_and_semicolon);
$cached = $backend->get('test5');
$this->assert(is_object($cached), "Backend returned an object for cache id test5.");
$this->assertIdentical($with_eof_and_semicolon, $cached->data);
$this->assertTrue($cached->valid, 'Item is marked as valid.');
$this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.');
$with_variable = array('foo' => '$bar');
$backend->set('test6', $with_variable);
$cached = $backend->get('test6');
$this->assert(is_object($cached), "Backend returned an object for cache id test6.");
$this->assertIdentical($with_variable, $cached->data);
// Make sure that a cached object is not affected by changing the original.
$data = new \stdClass();
$data->value = 1;
$data->obj = new \stdClass();
$data->obj->value = 2;
$backend->set('test7', $data);
$expected_data = clone $data;
// Add a property to the original. It should not appear in the cached data.
$data->this_should_not_be_in_the_cache = TRUE;
$cached = $backend->get('test7');
$this->assert(is_object($cached), "Backend returned an object for cache id test7.");
$this->assertEqual($expected_data, $cached->data);
$this->assertFalse(isset($cached->data->this_should_not_be_in_the_cache));
// Add a property to the cache data. It should not appear when we fetch
// the data from cache again.
$cached->data->this_should_not_be_in_the_cache = TRUE;
$fresh_cached = $backend->get('test7');
$this->assertFalse(isset($fresh_cached->data->this_should_not_be_in_the_cache));
// Check with a long key.
$cid = str_repeat('a', 300);
$backend->set($cid, 'test');
$this->assertEqual('test', $backend->get($cid)->data);
// Check that the cache key is case sensitive.
$backend->set('TEST8', 'value');
$this->assertEqual('value', $backend->get('TEST8')->data);
$this->assertFalse($backend->get('test8'));
// Calling ::set() with invalid cache tags.
try {
$backend->set('exception_test', 'value', Cache::PERMANENT, ['node' => [3, 5, 7]]);
$this->fail('::set() was called with invalid cache tags, no exception was thrown.');
}
catch (\LogicException $e) {
$this->pass('::set() was called with invalid cache tags, an exception was thrown.');
}
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::delete().
*/
public function testDelete() {
$backend = $this->getCacheBackend();
$this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1.");
$backend->set('test1', 7);
$this->assert(is_object($backend->get('test1')), "Backend returned an object for cache id test1.");
$this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2.");
$backend->set('test2', 3);
$this->assert(is_object($backend->get('test2')), "Backend returned an object for cache id %cid.");
$backend->delete('test1');
$this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1 after deletion.");
$this->assert(is_object($backend->get('test2')), "Backend still has an object for cache id test2.");
$backend->delete('test2');
$this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2 after deletion.");
$long_cid = str_repeat('a', 300);
$backend->set($long_cid, 'test');
$backend->delete($long_cid);
$this->assertIdentical(FALSE, $backend->get($long_cid), "Backend does not contain data for long cache id after deletion.");
}
/**
* Tests data type preservation.
*/
public function testValueTypeIsKept() {
$backend = $this->getCacheBackend();
$variables = array(
'test1' => 1,
'test2' => '0',
'test3' => '',
'test4' => 12.64,
'test5' => FALSE,
'test6' => array(1,2,3),
);
// Create cache entries.
foreach ($variables as $cid => $data) {
$backend->set($cid, $data);
}
// Retrieve and test cache objects.
foreach ($variables as $cid => $value) {
$object = $backend->get($cid);
$this->assert(is_object($object), sprintf("Backend returned an object for cache id %s.", $cid));
$this->assertIdentical($value, $object->data, sprintf("Data of cached id %s kept is identical in type and value", $cid));
}
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
public function testGetMultiple() {
$backend = $this->getCacheBackend();
// Set numerous testing keys.
$long_cid = str_repeat('a', 300);
$backend->set('test1', 1);
$backend->set('test2', 3);
$backend->set('test3', 5);
$backend->set('test4', 7);
$backend->set('test5', 11);
$backend->set('test6', 13);
$backend->set('test7', 17);
$backend->set($long_cid, 300);
// Mismatch order for harder testing.
$reference = array(
'test3',
'test7',
'test21', // Cid does not exist.
'test6',
'test19', // Cid does not exist until added before second getMultiple().
'test2',
);
$cids = $reference;
$ret = $backend->getMultiple($cids);
// Test return - ensure it contains existing cache ids.
$this->assert(isset($ret['test2']), "Existing cache id test2 is set.");
$this->assert(isset($ret['test3']), "Existing cache id test3 is set.");
$this->assert(isset($ret['test6']), "Existing cache id test6 is set.");
$this->assert(isset($ret['test7']), "Existing cache id test7 is set.");
// Test return - ensure that objects has expected properties.
$this->assertTrue($ret['test2']->valid, 'Item is marked as valid.');
$this->assertTrue($ret['test2']->created >= REQUEST_TIME && $ret['test2']->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($ret['test2']->expire, Cache::PERMANENT, 'Expire time is correct.');
// Test return - ensure it does not contain nonexistent cache ids.
$this->assertFalse(isset($ret['test19']), "Nonexistent cache id test19 is not set.");
$this->assertFalse(isset($ret['test21']), "Nonexistent cache id test21 is not set.");
// Test values.
$this->assertIdentical($ret['test2']->data, 3, "Existing cache id test2 has the correct value.");
$this->assertIdentical($ret['test3']->data, 5, "Existing cache id test3 has the correct value.");
$this->assertIdentical($ret['test6']->data, 13, "Existing cache id test6 has the correct value.");
$this->assertIdentical($ret['test7']->data, 17, "Existing cache id test7 has the correct value.");
// Test $cids array - ensure it contains cache id's that do not exist.
$this->assert(in_array('test19', $cids), "Nonexistent cache id test19 is in cids array.");
$this->assert(in_array('test21', $cids), "Nonexistent cache id test21 is in cids array.");
// Test $cids array - ensure it does not contain cache id's that exist.
$this->assertFalse(in_array('test2', $cids), "Existing cache id test2 is not in cids array.");
$this->assertFalse(in_array('test3', $cids), "Existing cache id test3 is not in cids array.");
$this->assertFalse(in_array('test6', $cids), "Existing cache id test6 is not in cids array.");
$this->assertFalse(in_array('test7', $cids), "Existing cache id test7 is not in cids array.");
// Test a second time after deleting and setting new keys which ensures that
// if the backend uses statics it does not cause unexpected results.
$backend->delete('test3');
$backend->delete('test6');
$backend->set('test19', 57);
$cids = $reference;
$ret = $backend->getMultiple($cids);
// Test return - ensure it contains existing cache ids.
$this->assert(isset($ret['test2']), "Existing cache id test2 is set");
$this->assert(isset($ret['test7']), "Existing cache id test7 is set");
$this->assert(isset($ret['test19']), "Added cache id test19 is set");
// Test return - ensure it does not contain nonexistent cache ids.
$this->assertFalse(isset($ret['test3']), "Deleted cache id test3 is not set");
$this->assertFalse(isset($ret['test6']), "Deleted cache id test6 is not set");
$this->assertFalse(isset($ret['test21']), "Nonexistent cache id test21 is not set");
// Test values.
$this->assertIdentical($ret['test2']->data, 3, "Existing cache id test2 has the correct value.");
$this->assertIdentical($ret['test7']->data, 17, "Existing cache id test7 has the correct value.");
$this->assertIdentical($ret['test19']->data, 57, "Added cache id test19 has the correct value.");
// Test $cids array - ensure it contains cache id's that do not exist.
$this->assert(in_array('test3', $cids), "Deleted cache id test3 is in cids array.");
$this->assert(in_array('test6', $cids), "Deleted cache id test6 is in cids array.");
$this->assert(in_array('test21', $cids), "Nonexistent cache id test21 is in cids array.");
// Test $cids array - ensure it does not contain cache id's that exist.
$this->assertFalse(in_array('test2', $cids), "Existing cache id test2 is not in cids array.");
$this->assertFalse(in_array('test7', $cids), "Existing cache id test7 is not in cids array.");
$this->assertFalse(in_array('test19', $cids), "Added cache id test19 is not in cids array.");
// Test with a long $cid and non-numeric array key.
$cids = array('key:key' => $long_cid);
$return = $backend->getMultiple($cids);
$this->assertEqual(300, $return[$long_cid]->data);
$this->assertTrue(empty($cids));
}
/**
* Tests \Drupal\Core\Cache\CacheBackendInterface::setMultiple().
*/
public function testSetMultiple() {
$backend = $this->getCacheBackend();
$future_expiration = REQUEST_TIME + 100;
// Set multiple testing keys.
$backend->set('cid_1', 'Some other value');
$items = array(
'cid_1' => array('data' => 1),
'cid_2' => array('data' => 2),
'cid_3' => array('data' => array(1, 2)),
'cid_4' => array('data' => 1, 'expire' => $future_expiration),
'cid_5' => array('data' => 1, 'tags' => array('test:a', 'test:b')),
);
$backend->setMultiple($items);
$cids = array_keys($items);
$cached = $backend->getMultiple($cids);
$this->assertEqual($cached['cid_1']->data, $items['cid_1']['data'], 'Over-written cache item set correctly.');
$this->assertTrue($cached['cid_1']->valid, 'Item is marked as valid.');
$this->assertTrue($cached['cid_1']->created >= REQUEST_TIME && $cached['cid_1']->created <= round(microtime(TRUE), 3), 'Created time is correct.');
$this->assertEqual($cached['cid_1']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.');
$this->assertEqual($cached['cid_2']->data, $items['cid_2']['data'], 'New cache item set correctly.');
$this->assertEqual($cached['cid_2']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.');
$this->assertEqual($cached['cid_3']->data, $items['cid_3']['data'], 'New cache item with serialized data set correctly.');
$this->assertEqual($cached['cid_3']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.');
$this->assertEqual($cached['cid_4']->data, $items['cid_4']['data'], 'New cache item set correctly.');
$this->assertEqual($cached['cid_4']->expire, $future_expiration, 'Cache expiration has been correctly set.');
$this->assertEqual($cached['cid_5']->data, $items['cid_5']['data'], 'New cache item set correctly.');
// Calling ::setMultiple() with invalid cache tags.
try {
$items = [
'exception_test_1' => array('data' => 1, 'tags' => []),
'exception_test_2' => array('data' => 2, 'tags' => ['valid']),
'exception_test_3' => array('data' => 3, 'tags' => ['node' => [3, 5, 7]]),
];
$backend->setMultiple($items);
$this->fail('::setMultiple() was called with invalid cache tags, no exception was thrown.');
}
catch (\LogicException $e) {
$this->pass('::setMultiple() was called with invalid cache tags, an exception was thrown.');
}
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::delete() and
* Drupal\Core\Cache\CacheBackendInterface::deleteMultiple().
*/
public function testDeleteMultiple() {
$backend = $this->getCacheBackend();
// Set numerous testing keys.
$backend->set('test1', 1);
$backend->set('test2', 3);
$backend->set('test3', 5);
$backend->set('test4', 7);
$backend->set('test5', 11);
$backend->set('test6', 13);
$backend->set('test7', 17);
$backend->delete('test1');
$backend->delete('test23'); // Nonexistent key should not cause an error.
$backend->deleteMultiple(array(
'test3',
'test5',
'test7',
'test19', // Nonexistent key should not cause an error.
'test21', // Nonexistent key should not cause an error.
));
// Test if expected keys have been deleted.
$this->assertIdentical(FALSE, $backend->get('test1'), "Cache id test1 deleted.");
$this->assertIdentical(FALSE, $backend->get('test3'), "Cache id test3 deleted.");
$this->assertIdentical(FALSE, $backend->get('test5'), "Cache id test5 deleted.");
$this->assertIdentical(FALSE, $backend->get('test7'), "Cache id test7 deleted.");
// Test if expected keys exist.
$this->assertNotIdentical(FALSE, $backend->get('test2'), "Cache id test2 exists.");
$this->assertNotIdentical(FALSE, $backend->get('test4'), "Cache id test4 exists.");
$this->assertNotIdentical(FALSE, $backend->get('test6'), "Cache id test6 exists.");
// Test if that expected keys do not exist.
$this->assertIdentical(FALSE, $backend->get('test19'), "Cache id test19 does not exist.");
$this->assertIdentical(FALSE, $backend->get('test21'), "Cache id test21 does not exist.");
// Calling deleteMultiple() with an empty array should not cause an error.
$this->assertFalse($backend->deleteMultiple(array()));
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::deleteAll().
*/
public function testDeleteAll() {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$backend_a->deleteAll();
$this->assertFalse($backend_a->get('test1'), 'First key has been deleted.');
$this->assertFalse($backend_a->get('test2'), 'Second key has been deleted.');
$this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.');
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::invalidate() and
* Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple().
*/
function testInvalidate() {
$backend = $this->getCacheBackend();
$backend->set('test1', 1);
$backend->set('test2', 2);
$backend->set('test3', 2);
$backend->set('test4', 2);
$reference = array('test1', 'test2', 'test3', 'test4');
$cids = $reference;
$ret = $backend->getMultiple($cids);
$this->assertEqual(count($ret), 4, 'Four items returned.');
$backend->invalidate('test1');
$backend->invalidateMultiple(array('test2', 'test3'));
$cids = $reference;
$ret = $backend->getMultiple($cids);
$this->assertEqual(count($ret), 1, 'Only one item element returned.');
$cids = $reference;
$ret = $backend->getMultiple($cids, TRUE);
$this->assertEqual(count($ret), 4, 'Four items returned.');
// Calling invalidateMultiple() with an empty array should not cause an
// error.
$this->assertFalse($backend->invalidateMultiple(array()));
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
*/
function testInvalidateTags() {
$backend = $this->getCacheBackend();
// Create two cache entries with the same tag and tag value.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:2'));
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:2'));
$this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Two cache items were created.');
// Invalidate test_tag of value 1. This should invalidate both entries.
Cache::invalidateTags(array('test_tag:2'));
$this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two cache items invalidated after invalidating a cache tag.');
$this->assertTrue($backend->get('test_cid_invalidate1', TRUE) && $backend->get('test_cid_invalidate2', TRUE), 'Cache items not deleted after invalidating a cache tag.');
// Create two cache entries with the same tag and an array tag value.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:1'));
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:1'));
$this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Two cache items were created.');
// Invalidate test_tag of value 1. This should invalidate both entries.
Cache::invalidateTags(array('test_tag:1'));
$this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two caches removed after invalidating a cache tag.');
$this->assertTrue($backend->get('test_cid_invalidate1', TRUE) && $backend->get('test_cid_invalidate2', TRUE), 'Cache items not deleted after invalidating a cache tag.');
// Create three cache entries with a mix of tags and tag values.
$backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:1'));
$backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:2'));
$backend->set('test_cid_invalidate3', $this->defaultValue, Cache::PERMANENT, array('test_tag_foo:3'));
$this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2') && $backend->get('test_cid_invalidate3'), 'Three cached items were created.');
Cache::invalidateTags(array('test_tag_foo:3'));
$this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Cache items not matching the tag were not invalidated.');
$this->assertFalse($backend->get('test_cid_invalidated3'), 'Cached item matching the tag was removed.');
// Create cache entry in multiple bins. Two cache entries
// (test_cid_invalidate1 and test_cid_invalidate2) still exist from previous
// tests.
$tags = array('test_tag:1', 'test_tag:2', 'test_tag:3');
$bins = array('path', 'bootstrap', 'page');
foreach ($bins as $bin) {
$this->getCacheBackend($bin)->set('test', $this->defaultValue, Cache::PERMANENT, $tags);
$this->assertTrue($this->getCacheBackend($bin)->get('test'), 'Cache item was set in bin.');
}
Cache::invalidateTags(array('test_tag:2'));
// Test that the cache entry has been invalidated in multiple bins.
foreach ($bins as $bin) {
$this->assertFalse($this->getCacheBackend($bin)->get('test'), 'Tag invalidation affected item in bin.');
}
// Test that the cache entry with a matching tag has been invalidated.
$this->assertFalse($this->getCacheBackend($bin)->get('test_cid_invalidate2'), 'Cache items matching tag were invalidated.');
// Test that the cache entry with without a matching tag still exists.
$this->assertTrue($this->getCacheBackend($bin)->get('test_cid_invalidate1'), 'Cache items not matching tag were not invalidated.');
}
/**
* Test Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*/
public function testInvalidateAll() {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$backend_a->invalidateAll();
$this->assertFalse($backend_a->get('test1'), 'First key has been invalidated.');
$this->assertFalse($backend_a->get('test2'), 'Second key has been invalidated.');
$this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.');
$this->assertTrue($backend_a->get('test1', TRUE), 'First key has not been deleted.');
$this->assertTrue($backend_a->get('test2', TRUE), 'Second key has not been deleted.');
}
/**
* Tests Drupal\Core\Cache\CacheBackendInterface::removeBin().
*/
public function testRemoveBin() {
$backend_a = $this->getCacheBackend();
$backend_b = $this->getCacheBackend('bootstrap');
// Set both expiring and permanent keys.
$backend_a->set('test1', 1, Cache::PERMANENT);
$backend_a->set('test2', 3, time() + 1000);
$backend_b->set('test3', 4, Cache::PERMANENT);
$backend_a->removeBin();
$this->assertFalse($backend_a->get('test1'), 'First key has been deleted.');
$this->assertFalse($backend_a->get('test2', TRUE), 'Second key has been deleted.');
$this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.');
}
}

Some files were not shown because too many files have changed in this diff Show more