Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: fallback
label: 'Fallback date format'
locked: true
pattern: 'D, j M Y - H:i'

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: html_date
label: 'HTML Date'
locked: true
pattern: Y-m-d

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: html_datetime
label: 'HTML Datetime'
locked: true
pattern: 'Y-m-d\TH:i:sO'

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: html_month
label: 'HTML Month'
locked: true
pattern: Y-m

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: html_time
label: 'HTML Time'
locked: true
pattern: 'H:i:s'

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: html_week
label: 'HTML Week'
locked: true
pattern: Y-\WW

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: html_year
label: 'HTML Year'
locked: true
pattern: 'Y'

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: html_yearless_date
label: 'HTML Yearless date'
locked: true
pattern: m-d

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: long
label: 'Default long date'
locked: false
pattern: 'l, j F Y - H:i'

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: medium
label: 'Default medium date'
locked: false
pattern: 'D, j M Y - H:i'

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: short
label: 'Default short date'
locked: false
pattern: 'j M Y - H:i'

View File

@ -0,0 +1,2 @@
enabled: true
interval_hours: 6

View File

@ -0,0 +1,4 @@
threshold:
requirements_warning: 172800
requirements_error: 1209600
logging: true

View File

@ -0,0 +1,9 @@
first_day: 0
country:
default: null
timezone:
default: null
user:
configurable: true
default: 0
warn: false

View File

@ -0,0 +1,3 @@
context:
lines_leading: 2
lines_trailing: 2

View File

@ -0,0 +1 @@
linkset_endpoint: false

View File

@ -0,0 +1,3 @@
allow_insecure_uploads: false
default_scheme: 'public'
temporary_maximum_age: 21600

View File

@ -0,0 +1 @@
jpeg_quality: 75

View File

@ -0,0 +1 @@
toolkit: gd

View File

@ -0,0 +1 @@
error_level: hide

View File

@ -0,0 +1,9 @@
interface:
default: 'php_mail'
mailer_dsn:
scheme: 'sendmail'
host: 'default'
user: null
password: null
port: null
options: []

View File

@ -0,0 +1,2 @@
langcode: en
message: '@site is currently under maintenance. We should be back shortly. Thank you for your patience.'

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: account
label: 'User account menu'
description: 'Links related to the active user account'
locked: true

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: admin
label: Administration
description: 'Administrative task links'
locked: true

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: footer
label: Footer
description: 'Site information links'
locked: true

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: main
label: 'Main navigation'
description: 'Site section links'
locked: true

View File

@ -0,0 +1,7 @@
langcode: en
status: true
dependencies: { }
id: tools
label: Tools
description: 'User tool links, often added by modules'
locked: true

View File

@ -0,0 +1,14 @@
cache:
page:
max_age: 0
css:
preprocess: true
gzip: true
fast_404:
enabled: true
paths: '/\.(?:txt|png|gif|jpe?g|css|js|ico|swf|flv|cgi|bat|pl|dll|exe|asp)$/i'
exclude_paths: '/\/(?:styles|imagecache)\//'
html: '<!DOCTYPE html><html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL "@path" was not found on this server.</p></body></html>'
js:
preprocess: true
gzip: true

View File

@ -0,0 +1,2 @@
items:
view_mode: rss

View File

@ -0,0 +1,13 @@
langcode: en
uuid: ''
name: ''
mail: ''
slogan: ''
page:
403: ''
404: ''
front: /user/login
admin_compact_mode: false
weight_select_max: 100
default_langcode: en
mail_notification: null

View File

@ -0,0 +1,14 @@
favicon:
mimetype: image/vnd.microsoft.icon
path: ''
url: ''
use_default: true
features:
comment_user_picture: true
comment_user_verification: true
favicon: true
node_user_picture: true
logo:
path: ''
url: ~
use_default: true

View File

@ -0,0 +1,2 @@
admin: ''
default: stark

View File

@ -0,0 +1,514 @@
# Schema for the configuration files of the System module.
system.site:
type: config_object
label: 'Site information'
mapping:
uuid:
type: uuid
label: 'Site UUID'
constraints:
Uuid: []
NotNull: []
name:
type: label
label: 'Site name'
mail:
type: email
label: 'Email address'
slogan:
type: label
label: 'Slogan'
page:
type: mapping
label: 'Pages'
mapping:
403:
type: path
label: 'Default 403 (access denied) page'
404:
type: path
label: 'Default 404 (not found) page'
front:
type: path
label: 'Default front page'
admin_compact_mode:
type: boolean
label: 'Compact mode'
weight_select_max:
type: integer
label: 'Weight element maximum value'
default_langcode:
type: langcode
label: 'Site default language code'
mail_notification:
type: string
label: 'Notification email address'
system.maintenance:
type: config_object
label: 'Maintenance mode'
constraints:
FullyValidatable: ~
mapping:
message:
type: text
label: 'Message to display when in maintenance mode'
system.cron:
type: config_object
label: 'Cron settings'
constraints:
FullyValidatable: ~
mapping:
threshold:
type: mapping
label: 'Thresholds'
mapping:
requirements_warning:
type: integer
label: 'Requirements warning period'
constraints:
# @see system_requirements()
Range:
min: 60
requirements_error:
type: integer
label: 'Requirements error period'
constraints:
# @see system_requirements()
Range:
min: 300
logging:
type: boolean
label: 'Detailed cron logging'
system.date:
type: config_object
label: 'Date settings'
constraints:
FullyValidatable: ~
mapping:
first_day:
type: integer
label: 'First day of week'
constraints:
Range:
# @see \Drupal\system\Form\RegionalForm::buildForm()
min: 0
max: 6
country:
type: mapping
label: 'Country'
mapping:
default:
nullable: true
type: string
label: 'Default country'
constraints:
# @see \Drupal\system\Form\RegionalForm::buildForm()
CountryCode: []
timezone:
type: mapping
label: 'Time zone settings'
mapping:
default:
type: string
label: 'Default time zone'
nullable: true
constraints:
# @see \Drupal\system\Form\RegionalForm::buildForm()
Choice:
callback: 'DateTimeZone::listIdentifiers'
user:
type: mapping
label: 'User'
mapping:
configurable:
type: boolean
label: 'Users may set their own time zone'
default:
type: integer
label: 'Time zone for new users'
constraints:
# Time zone for new users can have one of the following values:
# - UserInterface::TIMEZONE_DEFAULT
# - UserInterface::TIMEZONE_EMPTY
# - UserInterface::TIMEZONE_SELECT
# @see \Drupal\user\UserInterface::TIMEZONE_*
# @todo Update this to use enum in https://www.drupal.org/project/drupal/issues/3402178
Choice: [0, 1, 2]
warn:
type: boolean
label: 'Remind users at login if their time zone is not set'
system.diff:
type: config_object
label: 'Diff settings'
constraints:
FullyValidatable: ~
mapping:
context:
type: mapping
label: 'Context'
mapping:
lines_leading:
type: integer
label: 'Number of leading lines in a diff'
constraints:
# @see \Drupal\Component\Diff\DiffFormatter
Range:
min: 0
lines_trailing:
type: integer
label: 'Number of trailing lines in a diff'
constraints:
# @see \Drupal\Component\Diff\DiffFormatter
Range:
min: 0
system.logging:
type: config_object
label: 'Logging settings'
constraints:
FullyValidatable: ~
mapping:
error_level:
type: string
label: 'Error messages to display'
# @see core/includes/bootstrap.inc
# @todo Update this to use enum in https://www.drupal.org/project/drupal/issues/2951046
constraints:
Choice:
choices:
- 'hide'
- 'some'
- 'all'
- 'verbose'
system.performance:
type: config_object
label: 'Performance settings'
mapping:
cache:
type: mapping
label: 'Caching'
mapping:
page:
type: mapping
label: 'Page caching'
mapping:
max_age:
type: integer
label: 'Max age'
css:
type: mapping
label: 'CSS performance settings'
mapping:
preprocess:
type: boolean
label: 'Aggregate CSS files'
gzip:
type: boolean
label: 'Compress CSS files'
fast_404:
type: mapping
label: 'Fast 404 settings'
mapping:
enabled:
type: boolean
label: 'Fast 404 enabled'
paths:
type: string
label: 'Regular expression to match'
exclude_paths:
type: string
label: 'Regular expression to not match'
html:
type: string
label: 'Fast 404 page html'
js:
type: mapping
label: 'JavaScript performance settings'
mapping:
preprocess:
type: boolean
label: 'JavaScript preprocess'
gzip:
type: boolean
label: 'Compress JavaScript files.'
system.rss:
type: config_object
label: 'Feed settings'
mapping:
items:
type: mapping
label: 'Feed items'
mapping:
view_mode:
type: string
label: 'Feed content'
system.theme:
type: config_object
label: 'Theme settings'
mapping:
admin:
type: string
label: 'Administration theme'
default:
type: string
label: 'Default theme'
system.menu.*:
type: config_entity
label: 'Menu'
mapping:
id:
type: machine_name
label: 'ID'
# Menu IDs are specifically limited to 32 characters, and allow dashes but not
# underscores.
# @see \Drupal\menu_ui\MenuForm::form()
constraints:
Regex:
pattern: '/^[a-z0-9-]+$/'
message: "The %value machine name is not valid."
Length:
max: 32
label:
type: required_label
label: 'Label'
description:
type: label
label: 'Menu description'
# @see \Drupal\menu_ui\MenuForm::form()
nullable: true
constraints:
Length:
max: 512
locked:
type: boolean
label: 'Locked'
constraints:
FullyValidatable: ~
system.action.*:
type: config_entity
label: 'System action'
constraints:
FullyValidatable: ~
mapping:
id:
type: machine_name
label: 'ID'
constraints:
# Action IDs also allow periods.
# @see user_user_role_insert()
Regex:
pattern: '/^[a-z0-9_\.]+$/'
message: "The %value machine name is not valid."
label:
type: required_label
label: 'Label'
type:
type: string
label: 'Type'
# Action can be specified without type.
# @see \Drupal\action_test\Plugin\Action\NoType
nullable: true
constraints:
NotBlank:
allowNull: true
plugin:
type: string
label: 'Plugin'
constraints:
PluginExists:
manager: plugin.manager.action
interface: 'Drupal\Core\Action\ActionInterface'
configuration:
type: action.configuration.[%parent.plugin]
system.file:
type: config_object
label: 'File system'
constraints:
FullyValidatable: ~
mapping:
allow_insecure_uploads:
type: boolean
label: 'Allow insecure uploads'
default_scheme:
type: string
label: 'Default download method'
constraints:
ClassResolver:
classOrService: 'stream_wrapper_manager'
method: 'isValidScheme'
path:
type: mapping
label: 'Path settings'
deprecated: "The 'path' config schema is deprecated in drupal:11.2.0 and will be removed from drupal 12.0.0. Use 'file_temp_path' key in settings.php instead. See https://www.drupal.org/node/3039255."
temporary_maximum_age:
type: integer
label: 'Maximum age for temporary files'
constraints:
Range:
min: 0
system.image:
type: config_object
label: 'Image settings'
constraints:
FullyValidatable: ~
mapping:
toolkit:
type: string
label: 'Toolkit'
constraints:
PluginExists:
manager: 'image.toolkit.manager'
interface: '\Drupal\Core\ImageToolkit\ImageToolkitInterface'
system.image.gd:
type: config_object
label: 'Image settings'
constraints:
FullyValidatable: ~
mapping:
jpeg_quality:
type: integer
label: 'JPEG quality'
constraints:
# @see \Drupal\system\Plugin\ImageToolkit\GDToolkit::buildConfigurationForm()
Range:
min: 0
max: 100
system.mail:
type: config_object
label: 'Mail system'
constraints:
FullyValidatable: ~
mapping:
interface:
type: sequence
label: 'Interfaces'
sequence:
type: string
label: 'Interface'
constraints:
PluginExists:
manager: plugin.manager.mail
interface: 'Drupal\Core\Mail\MailInterface'
mailer_dsn:
type: mailer_dsn
label: 'Symfony mailer transport DSN'
system.theme.global:
type: theme_settings
label: 'Theme global settings'
system.advisories:
type: config_object
label: 'Security advisory settings'
constraints:
FullyValidatable: ~
mapping:
enabled:
type: boolean
label: 'Display critical security advisories'
interval_hours:
type: integer
label: 'How often to check for security advisories, in hours'
# Minimum can be set to 0 as it just means the advisories will be retrieved on every call.
# @see \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher::getSecurityAdvisories
constraints:
Range:
min: 0
block.settings.system_branding_block:
type: block_settings
label: 'Branding block'
constraints:
FullyValidatable: ~
mapping:
use_site_logo:
type: boolean
label: 'Use site logo'
use_site_name:
type: boolean
label: 'Use site name'
use_site_slogan:
type: boolean
label: 'Use site slogan'
block.settings.system_menu_block:*:
type: block_settings
label: 'Menu block'
constraints:
FullyValidatable: ~
mapping:
level:
type: integer
label: 'Starting level'
constraints:
MenuLinkDepth:
min: 0
depth:
type: integer
label: 'Maximum number of levels'
nullable: true
constraints:
MenuLinkDepth:
baseLevel: '[%parent.level]'
min: 1
expand_all_items:
type: boolean
label: 'Expand all items'
block.settings.local_tasks_block:
type: block_settings
label: 'Tabs block'
constraints:
FullyValidatable: ~
mapping:
primary:
type: boolean
label: 'Whether primary tabs are shown'
secondary:
type: boolean
label: 'Whether secondary tabs are shown'
condition.plugin.request_path:
type: condition.plugin
mapping:
pages:
type: string
condition.plugin.response_status:
type: condition.plugin
mapping:
status_codes:
type: sequence
sequence:
type: integer
system.feature_flags:
type: config_object
label: 'System Feature Flags'
constraints:
FullyValidatable: ~
mapping:
linkset_endpoint:
type: boolean
label: 'Enable the menu linkset endpoint'
condition.plugin.current_theme:
type: condition.plugin
mapping:
theme:
type: string
label: Theme

View File

@ -0,0 +1,32 @@
/**
* @file
* Alignment classes for text and block level elements.
*/
.text-align-left {
text-align: left;
}
.text-align-right {
text-align: right;
}
.text-align-center {
text-align: center;
}
.text-align-justify {
text-align: justify;
}
/**
* Alignment classes for block level elements (images, videos, blockquotes, etc.)
*/
.align-left {
float: left;
}
.align-right {
float: right;
}
.align-center {
display: block;
margin-right: auto;
margin-left: auto;
}

View File

@ -0,0 +1,15 @@
/**
* @file
* Float clearing.
*
* Based on the micro clearfix hack by Nicolas Gallagher, with the :before
* pseudo selector removed to allow normal top margin collapse.
*
* @see http://nicolasgallagher.com/micro-clearfix-hack
*/
.clearfix::after {
display: table;
clear: both;
content: "";
}

View File

@ -0,0 +1,16 @@
/**
* @file
* Inline items.
*/
.container-inline div,
.container-inline label {
display: inline-block;
}
/* Details contents always need to be rendered as block. */
.container-inline .details-wrapper {
display: block;
}
.container-inline .hidden {
display: none;
}

View File

@ -0,0 +1,53 @@
/**
* @file
* Utility classes to hide elements in different ways.
*/
/**
* Hide elements from all users.
*
* Used for elements which should not be immediately displayed to any user. An
* example would be collapsible details that will be expanded with a click
* from a user. The effect of this class can be toggled with the jQuery show()
* and hide() functions.
*/
.hidden {
display: none;
}
/**
* Hide elements visually, but keep them available for screen readers.
*
* Used for information required for screen reader users to understand and use
* the site where visual display is undesirable. Information provided in this
* manner should be kept concise, to avoid unnecessary burden on the user.
* "!important" is used to prevent unintentional overrides.
*/
.visually-hidden {
position: absolute !important;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
width: 1px;
height: 1px;
word-wrap: normal;
}
/**
* The .focusable class extends the .visually-hidden class to allow
* the element to be focusable when navigated to via the keyboard.
*/
.visually-hidden.focusable:active,
.visually-hidden.focusable:focus-within {
position: static !important;
overflow: visible;
clip: auto;
width: auto;
height: auto;
}
/**
* Hide visually and from screen readers, but maintain layout.
*/
.invisible {
visibility: hidden;
}

View File

@ -0,0 +1,19 @@
/**
* @file
* Styles for item list.
*/
.item-list__comma-list,
.item-list__comma-list li {
display: inline;
}
.item-list__comma-list {
margin: 0;
padding: 0;
}
.item-list__comma-list li::after {
content: ", ";
}
.item-list__comma-list li:last-child::after {
content: "";
}

View File

@ -0,0 +1,35 @@
/**
* @file
* Utility classes to assist with JavaScript functionality.
*/
/**
* For anything you want to hide on page load when JS is enabled, so
* that you can use the JS to control visibility and avoid flicker.
*/
.js .js-hide {
display: none;
}
/**
* For anything you want to show on page load only when JS is enabled.
*/
.js-show {
display: none;
}
.js .js-show {
display: block;
}
/**
* Use the scripting media features for modern browsers to reduce layout shifts.
*/
@media (scripting: enabled) {
/* Extra specificity to override previous selector. */
.js-hide.js-hide {
display: none;
}
.js-show {
display: block;
}
}

View File

@ -0,0 +1,8 @@
/*
* @file
* Contain positioned elements.
*/
.position-container {
position: relative;
}

View File

@ -0,0 +1,14 @@
/*
* @file
* Utility class to remove browser styles, especially for button.
*/
.reset-appearance {
margin: 0;
padding: 0;
border: 0 none;
background: transparent;
line-height: inherit;
-webkit-appearance: none;
appearance: none;
}

View File

@ -0,0 +1,30 @@
/**
* @file
* Styles for the system status counter component.
*/
.system-status-counter__status-icon {
display: inline-block;
width: 25px;
height: 25px;
vertical-align: middle;
}
.system-status-counter__status-icon::before {
display: block;
width: 100%;
height: 100%;
content: "";
background-repeat: no-repeat;
background-position: center 2px;
background-size: 16px;
}
.system-status-counter__status-icon--error::before {
background-image: url(../../../../misc/icons/e32700/error.svg);
}
.system-status-counter__status-icon--warning::before {
background-image: url(../../../../misc/icons/e29700/warning.svg);
}
.system-status-counter__status-icon--checked::before {
background-image: url(../../../../misc/icons/73b355/check.svg);
}

View File

@ -0,0 +1,27 @@
/**
* @file
* Styles for the system status report counters.
*/
.system-status-report-counters__item {
width: 100%;
margin-bottom: 0.5em;
padding: 0.5em 0;
text-align: center;
white-space: nowrap;
background-color: rgb(0, 0, 0, 0.063);
}
@media screen and (min-width: 60em) {
.system-status-report-counters {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.system-status-report-counters__item--half-width {
width: 49%;
}
.system-status-report-counters__item--third-width {
width: 33%;
}
}

View File

@ -0,0 +1,14 @@
/**
* @file
* Default styles for the System Status general info.
*/
.system-status-general-info__item {
margin-top: 1em;
padding: 0 1em 1em;
border: 1px solid #ccc;
}
.system-status-general-info__item-title {
border-bottom: 1px solid #ccc;
}

View File

@ -0,0 +1,408 @@
/**
* @file
* Styles for administration pages.
*/
/**
* Reusable layout styles.
*/
.layout-container {
margin: 0 1.5em;
}
.layout-container::after {
display: table;
clear: both;
content: "";
}
@media screen and (min-width: 38em) {
.layout-container {
margin: 0 2.5em;
}
.layout-column {
float: left; /* LTR */
box-sizing: border-box;
}
[dir="rtl"] .layout-column {
float: right;
}
.layout-column + .layout-column {
padding-left: 10px; /* LTR */
}
[dir="rtl"] .layout-column + .layout-column {
padding-right: 10px;
padding-left: 0;
}
.layout-column--half {
width: 50%;
}
.layout-column--quarter {
width: 25%;
}
.layout-column--three-quarter {
width: 75%;
}
}
/**
* Panel.
* Used to visually group items together.
*/
.panel {
padding: 5px 5px 15px;
}
.panel__description {
margin: 0 0 3px;
padding: 2px 0 3px 0;
}
/**
* System compact link: to toggle the display of description text.
*/
.compact-link {
margin: 0 0 0.5em 0;
}
/**
* Quick inline admin links.
*/
small .admin-link::before {
content: " [";
}
small .admin-link::after {
content: "]";
}
/**
* Modules page.
*/
.system-modules thead > tr {
border: 0;
}
.system-modules div.incompatible {
font-weight: bold;
}
.system-modules td.checkbox {
width: 4%;
min-width: 25px;
}
.system-modules td.module {
width: 25%;
}
.system-modules td {
vertical-align: top;
}
.system-modules label,
.system-modules-uninstall label {
color: #1d1d1d;
font-size: 1.15em;
}
.system-modules details {
color: #5c5c5b;
line-height: 20px;
}
.system-modules details[open] {
overflow: visible;
height: auto;
white-space: normal;
}
.system-modules details[open] summary .text {
text-transform: none;
-webkit-hyphens: auto;
hyphens: auto;
}
.system-modules td details a {
color: #5c5c5b;
border: 0;
}
.system-modules td details {
margin: 0;
border: 0;
}
.system-modules td details summary {
padding: 0;
cursor: default;
text-transform: none;
font-weight: normal;
}
.system-modules td {
padding-left: 0; /* LTR */
}
[dir="rtl"] .system-modules td {
padding-right: 0;
padding-left: 12px;
}
@media screen and (max-width: 40em) {
.system-modules td.name {
width: 20%;
}
.system-modules td.description {
width: 40%;
}
}
.system-modules .requirements {
max-width: 490px;
padding: 5px 0;
}
.system-modules .links {
overflow: hidden; /* prevents collapse */
}
.system-modules .checkbox {
margin: 0 5px;
}
.system-modules .checkbox .form-item {
margin-bottom: 0;
}
.admin-requirements,
.admin-required {
color: #666;
font-size: 0.9em;
}
.admin-enabled {
color: #080;
}
.admin-missing {
color: #f00;
}
.module-link {
display: block;
float: left; /* LTR */
margin-top: 2px;
padding: 2px 20px;
white-space: nowrap;
}
[dir="rtl"] .module-link {
float: right;
}
.module-link-help {
background: url(../../../misc/icons/787878/questionmark-disc.svg) 0 50% no-repeat; /* LTR */
}
[dir="rtl"] .module-link-help {
background-position: top 50% right 0;
}
.module-link-permissions {
background: url(../../../misc/icons/787878/key.svg) 0 50% no-repeat; /* LTR */
}
[dir="rtl"] .module-link-permissions {
background-position: top 50% right 0;
}
.module-link-configure {
background: url(../../../misc/icons/787878/cog.svg) 0 50% no-repeat; /* LTR */
}
[dir="rtl"] .module-link-configure {
background-position: top 50% right 0;
}
.module-link--non-stable {
padding-left: 18px;
background: url(../../../misc/icons/e29700/warning.svg) 0 50% no-repeat; /* LTR */
}
[dir="rtl"] .module-link--non-stable {
padding-right: 18px;
padding-left: 0;
background-position: top 50% right 0;
}
/* Status report. */
.system-status-report__status-title {
position: relative;
box-sizing: border-box;
width: 100%;
padding: 10px 6px 10px 40px; /* LTR */
vertical-align: top;
background-color: transparent;
font-weight: normal;
}
[dir="rtl"] .system-status-report__status-title {
padding: 10px 40px 10px 6px;
}
.system-status-report__status-icon::before {
position: absolute;
top: 12px;
left: 12px; /* LTR */
display: block;
width: 16px;
height: 16px;
content: "";
background-repeat: no-repeat;
}
[dir="rtl"] .system-status-report__status-icon::before {
right: 12px;
left: auto;
}
.system-status-report__status-icon--error::before {
background-image: url(../../../misc/icons/e32700/error.svg);
}
.system-status-report__status-icon--warning::before {
background-image: url(../../../misc/icons/e29700/warning.svg);
}
.system-status-report__entry__value {
padding: 1em 0.5em;
}
/**
* Appearance page.
*/
.theme-info__header {
margin-bottom: 0;
font-weight: normal;
}
.theme-default .theme-info__header {
font-weight: bold;
}
.theme-info__description {
margin-top: 0;
}
.theme-link--non-stable {
padding-left: 18px;
background: url(../../../misc/icons/e29700/warning.svg) 0 50% no-repeat; /* LTR */
}
.system-themes-list {
margin-bottom: 20px;
}
.system-themes-list-uninstalled {
padding-top: 20px;
border-top: 1px solid #cdcdcd;
}
.system-themes-list__header {
margin: 0;
}
.theme-selector {
padding-top: 20px;
}
.theme-selector .screenshot,
.theme-selector .no-screenshot {
max-width: 100%;
height: auto;
padding: 2px;
text-align: center;
vertical-align: bottom;
border: 1px solid #e0e0d8;
}
.theme-default .screenshot {
border: 1px solid #aaa;
}
.system-themes-list-uninstalled .screenshot,
.system-themes-list-uninstalled .no-screenshot {
max-width: 194px;
height: auto;
}
/**
* Theme display without vertical toolbar.
*/
@media screen and (min-width: 45em) {
body:not(.toolbar-vertical) .system-themes-list-installed .screenshot,
body:not(.toolbar-vertical) .system-themes-list-installed .no-screenshot {
float: left; /* LTR */
width: 294px;
margin: 0 20px 0 0; /* LTR */
}
[dir="rtl"] body:not(.toolbar-vertical) .system-themes-list-installed .screenshot,
[dir="rtl"] body:not(.toolbar-vertical) .system-themes-list-installed .no-screenshot {
float: right;
margin: 0 0 0 20px;
}
body:not(.toolbar-vertical) .system-themes-list-installed .system-themes-list__header {
margin-top: 0;
}
body:not(.toolbar-vertical) .system-themes-list-uninstalled .theme-selector {
float: left; /* LTR */
box-sizing: border-box;
width: 31.25%;
padding: 20px 20px 20px 0; /* LTR */
}
[dir="rtl"] body:not(.toolbar-vertical) .system-themes-list-uninstalled .theme-selector {
float: right;
padding: 20px 0 20px 20px;
}
body:not(.toolbar-vertical) .system-themes-list-uninstalled .theme-info {
min-height: 170px;
}
}
/**
* Theme display with vertical toolbar.
*/
@media screen and (min-width: 60em) {
.toolbar-vertical .system-themes-list-installed .screenshot,
.toolbar-vertical .system-themes-list-installed .no-screenshot {
float: left; /* LTR */
width: 294px;
margin: 0 20px 0 0; /* LTR */
}
[dir="rtl"] .toolbar-vertical .system-themes-list-installed .screenshot,
[dir="rtl"] .toolbar-vertical .system-themes-list-installed .no-screenshot {
float: right;
margin: 0 0 0 20px;
}
.toolbar-vertical .system-themes-list-installed .theme-info__header {
margin-top: 0;
}
.toolbar-vertical .system-themes-list-uninstalled .theme-selector {
float: left; /* LTR */
box-sizing: border-box;
width: 31.25%;
padding: 20px 20px 20px 0; /* LTR */
}
[dir="rtl"] .toolbar-vertical .system-themes-list-uninstalled .theme-selector {
float: right;
padding: 20px 0 20px 20px;
}
.toolbar-vertical .system-themes-list-uninstalled .theme-info {
min-height: 170px;
}
}
.system-themes-list-installed .theme-info {
max-width: 940px;
}
.theme-selector .incompatible {
margin-top: 10px;
font-weight: bold;
}
.theme-selector .operations {
margin: 10px 0 0 0;
padding: 0;
}
.theme-selector .operations li {
float: left; /* LTR */
margin: 0;
padding: 0 0.7em;
list-style-type: none;
border-right: 1px solid #cdcdcd; /* LTR */
}
[dir="rtl"] .theme-selector .operations li {
float: right;
border-right: none;
border-left: 1px solid #cdcdcd;
}
.theme-selector .operations li:last-child {
padding: 0 0 0 0.7em; /* LTR */
border-right: none; /* LTR */
}
[dir="rtl"] .theme-selector .operations li:last-child {
padding: 0 0.7em 0 0;
border-left: none;
}
.theme-selector .operations li:first-child {
padding: 0 0.7em 0 0; /* LTR */
}
[dir="rtl"] .theme-selector .operations li:first-child {
padding: 0 0 0 0.7em;
}
.system-themes-admin-form {
clear: left; /* LTR */
}
[dir="rtl"] .system-themes-admin-form {
clear: right;
}
.cron-description__run-cron {
display: block;
}
.system-cron-settings__link {
overflow-wrap: break-word;
word-wrap: break-word;
}

View File

@ -0,0 +1,41 @@
/**
* Traditional split diff theming
*/
table.diff {
width: 100%;
margin-bottom: 20px;
border-spacing: 4px;
}
table.diff .diff-context {
background-color: #fafafa;
}
table.diff .diff-deletedline {
width: 50%;
background-color: #ffa;
}
table.diff .diff-addedline {
width: 50%;
background-color: #afa;
}
table.diff .diffchange {
color: #f00;
font-weight: bold;
}
table.diff .diff-marker {
width: 1.4em;
}
table.diff th {
padding-right: inherit; /* LTR */
}
[dir="rtl"] table.diff th {
padding-right: 0;
padding-left: inherit;
}
table.diff td div {
overflow: auto;
padding: 0.1ex 0.5em;
word-wrap: break-word;
}
table.diff td {
padding: 0.1ex 0.4em;
}

View File

@ -0,0 +1,56 @@
/**
* Update styles
*/
.update-results {
margin-top: 3em;
padding: 0.25em;
border: 1px solid #ccc;
background: #eee;
font-size: smaller;
}
.update-results h2 {
margin-top: 0.25em;
}
.update-results h4 {
margin-bottom: 0.25em;
}
.update-results .none {
color: #888;
font-style: italic;
}
.update-results .failure strong {
color: #b63300;
}
/**
* Authorize.php styles
*/
#edit-submit-connection {
clear: both;
}
#edit-submit-process,
.filetransfer {
display: none;
clear: both;
}
.js #edit-submit-connection {
display: none;
}
.js #edit-submit-process {
display: block;
}
#edit-connection-settings-change-connection-type {
margin: 2.6em 0.5em 0 1em; /* LTR */
}
[dir="rtl"] #edit-connection-settings-change-connection-type {
margin-right: 1em;
margin-left: 0.5em;
}
/**
* Theme maintenance styles
*/
.authorize-results__failure {
font-weight: bold;
}

View File

@ -0,0 +1,20 @@
---
label: 'Clearing the site cache'
related:
- core.maintenance
---
{% set performance_link_text %}{% trans %}Performance{% endtrans %}{% endset %}
{% set performance_link = render_var(help_route_link(performance_link_text, 'system.performance_settings')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Clear the data in the site cache.{% endtrans %}</p>
<h2>{% trans %}What is the cache?{% endtrans %}</h2>
<p>{% trans %}Some of the calculations that are done when your site loads a page take a long time to run. To save time when these calculations would need to be done again, their results can be <em>cached</em> in your site's database. There are internal mechanisms to <em>clear</em> cached data when the conditions or assumptions that went into the calculation have changed, but you can also clear cached data manually. When your site is misbehaving, a good first step is to clear the cache and see if the problem goes away.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em>{{ performance_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Clear all caches</em>. Your site's cached data will be cleared.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/prevent-cache.html">Concept: Cache (Drupal User Guide)</a>{% endtrans %}</li>
</ul>

View File

@ -0,0 +1,30 @@
---
label: 'Changing basic site settings'
top_level: true
related:
- user.security_account_settings
---
{% set regional_link_text %}{% trans %}Regional settings{% endtrans %}{% endset %}
{% set regional_link = render_var(help_route_link(regional_link_text, 'system.regional_settings')) %}
{% set information_link_text %}{% trans %}Basic site settings{% endtrans %}{% endset %}
{% set information_link = render_var(help_route_link(information_link_text, 'system.site_information_settings')) %}
{% set datetime_link_text %}{% trans %}Date and time formats{% endtrans %}{% endset %}
{% set datetime_link = render_var(help_route_link(datetime_link_text, 'entity.date_format.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure the basic settings of your site, including the site name, slogan, main email address, default time zone, default country, and the date formats to use.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>System</em> &gt; <em>{{ information_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Enter the site name, slogan, and main email address for your site. {% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Region and language</em> &gt; <em>{{ regional_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Select the default country and default time zone for your site.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Region and language</em> &gt; <em>{{ datetime_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Look at the <em>Patterns</em> for the Default long, medium, and short date formats. If any of them does not match the date format you want to use on your site, click <em>Edit</em> in that row to edit the format.{% endtrans %}</li>
<li>{% trans %}Adjust the <em>Format string</em> until the <em>Displayed</em> format matches what you want. (Date format strings are composed of PHP date format codes.){% endtrans %}</li>
<li>{% trans %}Click <em>Save format</em>. You should see a message indicating that the format was saved.{% endtrans %}</li>
<li>{% trans %}Repeat the previous three steps for any other date formats that need to be changed.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<p>{% trans %}<a href="https://www.php.net/manual/datetime.format.php#refsect1-datetime.format-parameters">PHP date format codes reference</a>{% endtrans %}</p>

View File

@ -0,0 +1,26 @@
---
label: 'Configuring error responses, including 403/404 pages'
related:
- system.config_basic
- core.maintenance
---
{% set log_settings_link_text %}{% trans %}Logging and errors{% endtrans %}{% endset %}
{% set log_settings_link = render_var(help_route_link(log_settings_link_text, 'system.logging_settings')) %}
{% set information_link_text %}{% trans %}Basic site settings{% endtrans %}{% endset %}
{% set information_link = render_var(help_route_link(information_link_text, 'system.site_information_settings')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Set up your site to respond appropriately to site errors, including 403 and 404 page responses.{% endtrans %}</p>
<h2>{% trans %}What are 403 and 404 responses?{% endtrans %}</h2>
<p>{% trans %}When a user visits a web page, the web server sends a response code in addition to the page content. A normal, non-error response has code 200. If the page does not exist on the site, the response code is 404. If the page exists, but the user is not authorized to visit the page, the response code is 403. The core software provides default responses for both 403 and 404 codes, but if you prefer, you can create your own pages for each.{% endtrans %}</p>
<h2>{% trans %}What other errors can occur?{% endtrans %}</h2>
<p>{% trans %}Under some situations, your site can generate error messages. These can be due to user errors (such as entering invalid values in a form, or incorrect configuration), PHP runtime errors, or software bugs. Some errors may result in a <em>white screen of death</em> (a totally blank web page response); less drastic errors will generate error messages. You can configure what happens when an error message is generated.{% endtrans %}</p>
<h2>{% trans %}Steps {% endtrans %}</h2>
<ol>
<li>{% trans %}If desired, create pages to use for 403 and 404 responses. Note the URLs for these pages.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>System</em> &gt; <em>{{ information_link }}</em>.{% endtrans %}</li>
<li>{% trans %}In the <em>Error pages</em> section, enter the URL for your 403/404 pages, starting after the site home page URL. For example, if your site URL is <em>https://example.com</em> and your 404 page is <em>https://example.com/not-found</em>, you would enter <em>/not-found</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em>{{ log_settings_link }}</em>.{% endtrans %}</li>
<li>{% trans %}For a production site, select <em>None</em> under <em>Error messages to display</em>. For a site that is in development, select one of the other options, so that you are more aware of the errors the site is generating.{% endtrans %}</li>
<li>{% trans %}Click <em>Save configuration</em>. You should see a message indicating that the settings were saved.{% endtrans %}</li>
</ol>

View File

@ -0,0 +1,22 @@
---
label: 'Enabling and disabling maintenance mode'
related:
- core.maintenance
- system.cache
---
{% set cache_topic = render_var(help_topic_link('system.cache')) %}
{% set maintenance_link_text %}{% trans %}Maintenance mode{% endtrans %}{% endset %}
{% set maintenance_link = render_var(help_route_link(maintenance_link_text, 'system.site_maintenance_mode')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Put your site in maintenance mode to perform maintenance operations, and then return to normal mode when finished.{% endtrans %}</p>
<h2>{% trans %}What is maintenance mode?{% endtrans %}</h2>
<p>{% trans %}When your site is in maintenance mode, most site visitors will see a simple maintenance mode message page, rather than being able to use the full functionality of the site. Users with <em>Use the site in maintenance mode</em> permission who are already logged in will be able to use the full site, and the log in page at <em>/user</em> will also be accessible to anyone.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em>{{ maintenance_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Check <em>Put site into maintenance mode</em>, optionally change the <em>Message to display when in maintenance mode</em>, and click <em>Save configuration</em>. Your site will be in maintenance mode.{% endtrans %}</li>
<li>{% trans %}Perform your maintenance operations.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>Development</em> &gt; <em>{{ maintenance_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Uncheck <em>Put site into maintenance mode</em> and click <em>Save configuration</em>. Your site will be back in normal operation mode.{% endtrans %}</li>
<li>{% trans %}Clear the site cache. See {{ cache_topic }} for instructions.{% endtrans %}</li>
</ol>

View File

@ -0,0 +1,18 @@
---
label: 'Installing a module'
related:
- core.extending
- system.module_uninstall
---
{% set extend_link_text %}{% trans %}Extend{% endtrans %}{% endset %}
{% set extend_link = render_var(help_route_link(extend_link_text, 'system.modules_list')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Install a core module, or a contributed module that has already been downloaded.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>{{ extend_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Enter a word from the module name or description into the filter box, to make the list of modules smaller. Locate the module you want to install.{% endtrans %}</li>
<li>{% trans %}Check the box next to the name of the module you want to install; you can also check more than one box to install multiple modules at the same time. If the checkbox is disabled for the module you are trying to install, expand the information to see why -- you may need to download an additional module that your module requires.{% endtrans %}</li>
<li>{% trans %}Click <em>Install</em> at the bottom of the page. If you chose to install a module with dependencies that were not already installed, or if you chose an Experimental module, confirm your choice on the next page.{% endtrans %}</li>
<li>{% trans %}Wait for the module (or modules) to be installed. You should be returned to the <em>Extend</em> page with a message saying the module or modules were installed.{% endtrans %}</li>
</ol>

View File

@ -0,0 +1,21 @@
---
label: 'Uninstalling a module'
related:
- core.extending
- system.module_install
- system.maintenance_mode
---
{% set uninstall_link_text %}{% trans %}Uninstall{% endtrans %}{% endset %}
{% set uninstall_link = render_var(help_route_link(uninstall_link_text, 'system.modules_uninstall')) %}
{% set maintenance_topic = render_var(help_topic_link('system.maintenance_mode')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Uninstall a module. Your site should be in <em>maintenance mode</em> when you uninstall modules. See {{ maintenance_topic }} for details.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Extend</em> &gt; <em>{{ uninstall_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Enter a word from the module name or description into the filter box, to make the list of modules smaller. Locate the module you want to uninstall.{% endtrans %}</li>
<li>{% trans %}In the <em>Description</em> column, see if there are reasons that this module cannot be uninstalled. For example, you may have created content using this module (which you would need to delete first), or there may be another module installed that requires this module to be installed (you would need to uninstall the other module first).{% endtrans %}</li>
<li>{% trans %}If there are no reasons listed, the module can be uninstalled. Check the box in the <em>Uninstall</em> column, next to the module's name.{% endtrans %}</li>
<li>{% trans %}Click <em>Uninstall</em> at the bottom of the page. Verify the list of modules to be uninstalled and configuration to be deleted on the confirmation page, and click <em>Uninstall</em>.{% endtrans %}</li>
<li>{% trans %}Wait for the module to be uninstalled. You should be returned to the <em>Uninstall</em> page with a message saying the module was uninstalled.{% endtrans %}</li>
</ol>

View File

@ -0,0 +1,21 @@
---
label: 'Running reports on your site'
related:
- core.maintenance
- core.security
- system.config_error
---
{% set status_link_text %}{% trans %}Status report{% endtrans %}{% endset %}
{% set status_link = render_var(help_route_link(status_link_text, 'system.status')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Run reports to learn about the status and health of your site.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Reports</em> &gt; <em>{{ status_link }}</em> to see a report that summarizes the health and status of your site. If there are any warnings or errors, you will need to fix them. Take note of any upcoming highly critical security releases that may impact your site.{% endtrans %}</li>
<li>{% trans %}If you have the core Database Logging module installed, in the <em>Manage</em> administrative menu, navigate to <em>Reports</em> &gt; <em>Recent log messages</em> to see a report of the error and informational messages your site has generated. You can filter the report by <em>Severity</em> to see only the most critical messages, if desired.{% endtrans %}</li>
<li>{% trans %}If you have the core Update Status module installed, in the <em>Manage</em> administrative menu, navigate to <em>Reports</em> &gt; <em>Available updates</em> to see a report of the updates that are available for your site software. If <em>Last checked</em> is far in the past, click <em>Check manually</em> to update the report. Scan the report; if the core software or any modules or themes have security updates available, you should update them as soon as possible.{% endtrans %}</li>
</ol>
<h2>{% trans %}Additional resources{% endtrans %}</h2>
<ul>
<li>{% trans %}<a href="https://www.drupal.org/docs/user_guide/en/security-chapter.html">Security and Maintenance (Drupal User Guide)</a>, which includes information on how to update your site's core software, modules, and themes.{% endtrans %}</li>
</ul>

View File

@ -0,0 +1,19 @@
---
label: 'Installing a theme and setting default themes'
related:
- core.appearance
- system.theme_uninstall
---
{% set themes_link_text %}{% trans %}Appearance{% endtrans %}{% endset %}
{% set themes_link = render_var(help_route_link(themes_link_text, 'system.themes_page')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Install a core theme, or a contributed theme that has already been downloaded. Choose the default themes to use for the site and for administrative pages.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>{{ themes_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Locate the themes that you want to use as the site default theme and for administrative pages.{% endtrans %}</li>
<li>{% trans %}For each of these themes, if the theme is in the <em>Uninstalled themes</em> section, click the <em>Install</em> link to install the theme. Wait for the theme to be installed (translations might be downloaded). You should be returned to the <em>Appearance</em> page.{% endtrans %}</li>
<li>{% trans %}Locate the theme that you want to be your default theme, which should now be in the <em>Installed themes</em> section. If it is not already labeled as the <em>default theme</em>, click the <em>Set as default</em> link.{% endtrans %}</li>
<li>{% trans %}At the bottom of the page, select the <em>Administration theme</em> that you want to use on administrative pages. Click <em>Save configuration</em> if you selected a new theme.{% endtrans %}</li>
<li>{% trans %}If you changed the default theme for your site, visit the site home page or another page on the non-administration part of your site and verify that the site is using the new theme. If you changed the administration theme, verify that the new theme is used on administrative pages.{% endtrans %}</li>
</ol>

View File

@ -0,0 +1,16 @@
---
label: 'Uninstalling an unused theme'
related:
- core.appearance
- system.theme_install
---
{% set themes_link_text %}{% trans %}Appearance{% endtrans %}{% endset %}
{% set themes_link = render_var(help_route_link(themes_link_text, 'system.themes_page')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Uninstall a theme that was previously installed, but is no longer being used on the site.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>{{ themes_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Locate the theme that you want to uninstall, in the <em>Installed themes</em> section.{% endtrans %}</li>
<li>{% trans %}Click the <em>Uninstall</em> link to uninstall the theme. If there is not an <em>Uninstall</em> link, the theme cannot be uninstalled because it is either being used as the site default theme, being used as the <em>Administration theme</em>, or is the base theme for another installed theme.{% endtrans %}</li>
</ol>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,69 @@
/**
* @file
* Provides date format preview feature.
*/
(function ($, Drupal, drupalSettings) {
const dateFormats = drupalSettings.dateFormats;
/**
* Display the preview for date format entered.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attach behavior for previewing date formats on input elements.
*/
Drupal.behaviors.dateFormat = {
attach(context) {
const source = once(
'dateFormat',
'[data-drupal-date-formatter="source"]',
context,
);
const target = once(
'dateFormat',
'[data-drupal-date-formatter="preview"]',
context,
);
// All elements have to exist.
if (!source.length || !target.length) {
return;
}
/**
* Event handler that replaces date characters with value.
*
* @param {jQuery.Event} e
* The jQuery event triggered.
*/
function dateFormatHandler(e) {
const baseValue = e.target.value || '';
const dateString = baseValue.replace(/\\?(.?)/gi, (key, value) =>
dateFormats[key] ? dateFormats[key] : value,
);
// Set date preview.
target.forEach((item) => {
item.querySelectorAll('em').forEach((em) => {
em.textContent = dateString;
});
});
$(target).toggleClass('js-hide', !dateString.length);
}
/**
* On given event triggers the date character replacement.
*/
$(source)
.on(
'keyup.dateFormat change.dateFormat input.dateFormat',
dateFormatHandler,
)
// Initialize preview.
.trigger('keyup');
},
};
})(jQuery, Drupal, drupalSettings);

View File

@ -0,0 +1,82 @@
/**
* @file
* System behaviors.
*/
(function ($, Drupal, drupalSettings) {
// Cache IDs in an array for ease of use.
const ids = [];
/**
* Attaches field copy behavior from input fields to other input fields.
*
* When a field is filled out, apply its value to other fields that will
* likely use the same value. In the installer this is used to populate the
* administrator email address with the same value as the site email address.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the field copy behavior to an input field.
*/
Drupal.behaviors.copyFieldValue = {
attach(context) {
// List of fields IDs on which to bind the event listener.
// Create an array of IDs to use with jQuery.
Object.keys(drupalSettings.copyFieldValue || {}).forEach((element) => {
ids.push(element);
});
if (ids.length) {
// Listen to value:copy events on all dependent fields.
// We have to use body and not document because of the way jQuery events
// bubble up the DOM tree.
$(once('copy-field-values', 'body')).on(
'value:copy',
this.valueTargetCopyHandler,
);
// Listen on all source elements.
$(once('copy-field-values', `#${ids.join(', #')}`)).on(
'blur',
this.valueSourceBlurHandler,
);
}
},
detach(context, settings, trigger) {
if (trigger === 'unload' && ids.length) {
$(once.remove('copy-field-values', 'body')).off('value:copy');
$(once.remove('copy-field-values', `#${ids.join(', #')}`)).off('blur');
}
},
/**
* Event handler that fill the target element with the specified value.
*
* @param {jQuery.Event} e
* Event object.
* @param {string} value
* Custom value from jQuery trigger.
*/
valueTargetCopyHandler(e, value) {
const { target } = e;
if (target.value === '') {
target.value = value;
}
},
/**
* Handler for a Blur event on a source field.
*
* This event handler will trigger a 'value:copy' event on all dependent
* fields.
*
* @param {jQuery.Event} e
* The event triggered.
*/
valueSourceBlurHandler(e) {
const { value } = e.target;
const targetIds = drupalSettings.copyFieldValue[e.target.id];
$(`#${targetIds.join(', #')}`).trigger('value:copy', value);
},
};
})(jQuery, Drupal, drupalSettings);

View File

@ -0,0 +1,112 @@
/**
* @file
* Module page behaviors.
*/
(function ($, Drupal, debounce) {
/**
* Filters the module list table by a text input search string.
*
* Additionally accounts for multiple tables being wrapped in "package" details
* elements.
*
* Text search input: input.table-filter-text
* Target table: input.table-filter-text[data-table]
* Source text: .table-filter-text-source, .module-name, .module-description
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.tableFilterByText = {
attach(context, settings) {
const [input] = once('table-filter-text', 'input.table-filter-text');
if (!input) {
return;
}
const $table = $(input.getAttribute('data-table'));
let $rowsAndDetails;
let $rows;
let $details;
let searching = false;
function hidePackageDetails(index, element) {
const $packDetails = $(element);
const $visibleRows = $packDetails.find('tbody tr:visible');
$packDetails.toggle($visibleRows.length > 0);
}
function filterModuleList(e) {
const query = e.target.value;
// Case insensitive expression to find query at the beginning of a word.
const re = new RegExp(`\\b${query}`, 'i');
function showModuleRow(index, row) {
const sources = row.querySelectorAll(
'.table-filter-text-source, .module-name, .module-description',
);
let sourcesConcat = '';
// Concatenate the textContent of the elements in the row, with a
// space in between.
sources.forEach((item) => {
sourcesConcat += ` ${item.textContent}`;
});
const textMatch = sourcesConcat.search(re) !== -1;
$(row).closest('tr').toggle(textMatch);
}
// Search over all rows and packages.
$rowsAndDetails.show();
// Filter if the length of the query is at least 2 characters.
if (query.length >= 2) {
searching = true;
$rows.each(showModuleRow);
// Note that we first open all <details> to be able to use ':visible'.
// Mark the <details> elements that were closed before filtering, so
// they can be closed again when filtering is removed.
$details
.not('[open]')
.attr('data-drupal-system-state', 'forced-open');
// Hide the package <details> if they don't have any visible rows.
// Note that we first show() all <details> to be able to use ':visible'.
$details.attr('open', true).each(hidePackageDetails);
Drupal.announce(
Drupal.formatPlural(
$rowsAndDetails.filter('tbody tr:visible').length,
'1 module is available in the modified list.',
'@count modules are available in the modified list.',
),
);
} else if (searching) {
searching = false;
$rowsAndDetails.show();
// Return <details> elements that had been closed before filtering
// to a closed state.
$details
.filter('[data-drupal-system-state="forced-open"]')
.removeAttr('data-drupal-system-state')
.attr('open', false);
}
}
function preventEnterKey(event) {
if (event.which === 13) {
event.preventDefault();
event.stopPropagation();
}
}
if ($table.length) {
$rowsAndDetails = $table.find('tr, details');
$rows = $table.find('tbody tr');
$details = $rowsAndDetails.filter('.package-listing');
$(input).on({
input: debounce(filterModuleList, 200),
keydown: preventEnterKey,
});
}
},
};
})(jQuery, Drupal, Drupal.debounce);

View File

@ -0,0 +1,20 @@
id: action_settings
label: Action configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- actions_max_stack
source_module: system
process:
recursion_limit:
plugin: skip_on_empty
method: row
source: empty
destination:
plugin: config
config_name: null
destination_module: system

View File

@ -0,0 +1,50 @@
# cspell:ignore imagecache
id: d6_action
label: Actions
migration_tags:
- Drupal 6
- Configuration
source:
plugin: action
process:
id:
-
plugin: machine_name
source: aid
label: description
type: type
plugin:
-
plugin: static_map
source: callback
map:
system_goto_action: action_goto_action
system_send_email_action: action_send_email_action
system_message_action: action_message_action
user_block_ip_action: 0
imagecache_flush_action: 0
imagecache_generate_all_action: 0
imagecache_generate_action: 0
comment_publish_action: entity:publish_action:comment
comment_unpublish_action: entity:unpublish_action:comment
comment_save_action: entity:save_action:comment
node_publish_action: entity:publish_action:node
node_unpublish_action: entity:unpublish_action:node
node_save_action: entity:save_action:node
comment_unpublish_by_keyword_action: 0
node_unpublish_by_keyword_action: 0
node_assign_owner_action: 0
bypass: true
-
plugin: skip_on_empty
method: row
configuration:
-
plugin: default_value
source: parameters
default_value: "a:0:{}"
-
plugin: callback
callable: unserialize
destination:
plugin: entity:action

View File

@ -0,0 +1,24 @@
# cspell:ignore multirow
id: d6_date_formats
label: Date format configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: variable_multirow
variables:
- date_format_long
- date_format_medium
- date_format_short
source_module: system
process:
id:
plugin: static_map
source: name
map:
date_format_long: long
date_format_short: short
date_format_medium: medium
pattern: value
destination:
plugin: entity:date_format

View File

@ -0,0 +1,14 @@
# The menu_settings migration is in the menu_ui module.
id: d6_menu
label: Menus
migration_tags:
- Drupal 6
- Configuration
source:
plugin: menu
process:
id: menu_name
label: title
description: description
destination:
plugin: entity:menu

View File

@ -0,0 +1,18 @@
id: d6_system_cron
label: Cron settings
migration_tags:
- Drupal 6
- Configuration
source:
plugin: variable
variables:
- cron_threshold_warning
- cron_threshold_error
- cron_last
source_module: system
process:
'threshold/requirements_warning': cron_threshold_warning
'threshold/requirements_error': cron_threshold_error
destination:
plugin: config
config_name: system.cron

View File

@ -0,0 +1,21 @@
id: d6_system_date
label: System date configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: variable
variables:
- configurable_timezones
- date_first_day
- date_default_timezone
source_module: system
process:
'timezone/user/configurable': configurable_timezones
first_day: date_first_day
'timezone/default':
plugin: timezone
source: date_default_timezone
destination:
plugin: config
config_name: system.date

View File

@ -0,0 +1,20 @@
id: d6_system_file
label: File system configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: variable
variables:
- allow_insecure_uploads
source_module: system
process:
allow_insecure_uploads:
plugin: static_map
source: allow_insecure_uploads
map:
0: FALSE
1: TRUE
destination:
plugin: config
config_name: system.file

View File

@ -0,0 +1,21 @@
id: d6_system_performance
label: Performance configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: variable
variables:
- preprocess_css
- preprocess_js
- cache_lifetime
- cache
- page_compression
source_module: system
process:
'css/preprocess': preprocess_css
'js/preprocess': preprocess_js
'cache/page/max_age': cache_lifetime
destination:
plugin: config
config_name: system.performance

View File

@ -0,0 +1,46 @@
id: d7_action
label: Actions
migration_tags:
- Drupal 7
- Configuration
source:
plugin: action
process:
id:
-
plugin: machine_name
source: aid
label: label
type: type
plugin:
-
plugin: static_map
source: callback
map:
system_goto_action: action_goto_action
system_send_email_action: action_send_email_action
system_message_action: action_message_action
system_block_ip_action: 0
comment_publish_action: entity:publish_action:comment
comment_unpublish_action: entity:unpublish_action:comment
comment_save_action: entity:save_action:comment
node_publish_action: entity:publish_action:node
node_unpublish_action: entity:unpublish_action:node
node_save_action: entity:save_action:node
comment_unpublish_by_keyword_action: 0
node_unpublish_by_keyword_action: 0
node_assign_owner_action: 0
bypass: true
-
plugin: skip_on_empty
method: row
configuration:
-
plugin: default_value
source: parameters
default_value: "a:0:{}"
-
plugin: callback
callable: unserialize
destination:
plugin: entity:action

View File

@ -0,0 +1,31 @@
id: d7_global_theme_settings
label: D7 global theme settings
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- theme_settings
source_module: system
process:
'features/logo': theme_settings/toggle_logo
'features/name': theme_settings/toggle_name
'features/slogan': theme_settings/toggle_slogan
'features/node_user_picture': theme_settings/toggle_node_user_picture
'features/comment_user_picture': theme_settings/toggle_comment_user_picture
'features/comment_user_verification': theme_settings/toggle_comment_user_verification
'features/favicon': theme_settings/toggle_favicon
'logo/use_default': theme_settings/default_logo
'logo/path': theme_settings/logo_path
'favicon/use_default': theme_settings/default_favicon
'favicon/path': theme_settings/favicon_path
'favicon/mimetype': theme_settings/favicon_mimetype
# Ignore settings not present in Drupal 8
# theme_settings/logo_upload
# theme_settings/favicon_upload
# theme_settings/toggle_main_menu
# theme_settings/toggle_secondary_menu
destination:
plugin: config
config_name: system.theme.global

View File

@ -0,0 +1,25 @@
id: d7_menu
label: Menus
migration_tags:
- Drupal 7
- Configuration
source:
plugin: menu
process:
id:
plugin: static_map
bypass: true
source: menu_name
map:
main-menu: main
management: admin
navigation: tools
user-menu: account
label: title
description: description
langcode:
plugin: default_value
source: language
default_value: en
destination:
plugin: entity:menu

View File

@ -0,0 +1,19 @@
id: d7_system_authorize
label: Drupal 7 file transfer authorize configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- authorize_filetransfer_default
source_module: system
process:
filetransfer_default:
plugin: skip_on_empty
method: row
source: empty
destination:
plugin: config
config_name: null
destination_module: system

View File

@ -0,0 +1,17 @@
id: d7_system_cron
label: Drupal 7 cron settings
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- cron_threshold_warning
- cron_threshold_error
source_module: system
process:
'threshold/requirements_warning': cron_threshold_warning
'threshold/requirements_error': cron_threshold_error
destination:
plugin: config
config_name: system.cron

View File

@ -0,0 +1,25 @@
id: d7_system_date
label: Drupal 7 system date configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- site_default_country
- date_first_day
- date_default_timezone
- configurable_timezones
- empty_timezone_message
- user_default_timezone
source_module: system
process:
'country/default': site_default_country
first_day: date_first_day
'timezone/default': date_default_timezone
'timezone/user/configurable': configurable_timezones
'timezone/user/warn': empty_timezone_message
'timezone/user/default': user_default_timezone
destination:
plugin: config
config_name: system.date

View File

@ -0,0 +1,20 @@
id: d7_system_file
label: Drupal 7 file system configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- allow_insecure_uploads
source_module: system
process:
allow_insecure_uploads:
plugin: static_map
source: allow_insecure_uploads
map:
0: FALSE
1: TRUE
destination:
plugin: config
config_name: system.file

View File

@ -0,0 +1,38 @@
id: d7_system_mail
label: Drupal 7 system mail configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables_no_row_if_missing:
- mail_system
source_module: system
process:
'interface/default':
plugin: static_map
source: 'mail_system/default-system'
map:
DefaultMailSystem: php_mail
MailTestCase: test_mail_collector
'mailer_dsn':
plugin: static_map
source: 'mail_system/default-system'
map:
DefaultMailSystem:
scheme: 'sendmail'
host: 'default'
user: null
password: null
port: null
options: []
MailTestCase:
scheme: 'null'
host: 'null'
user: null
password: null
port: null
options: []
destination:
plugin: config
config_name: system.mail

View File

@ -0,0 +1,20 @@
id: d7_system_performance
label: Drupal 7 performance configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- preprocess_css
- preprocess_js
- cache_lifetime
- page_compression
source_module: system
process:
'css/preprocess': preprocess_css
'js/preprocess': preprocess_js
'cache/page/max_age': cache_lifetime
destination:
plugin: config
config_name: system.performance

View File

@ -0,0 +1,52 @@
id: d7_theme_settings
label: D7 theme settings
migration_tags:
- Drupal 7
- Configuration
source:
plugin: d7_theme_settings
constants:
config_suffix: '.settings'
process:
# Build the configuration name from the variable name, i.e.
# theme_bartik_settings becomes bartik.settings.
legacy_theme_name:
-
plugin: explode
source: name
delimiter: _
-
plugin: extract
index:
- 1
theme_name:
plugin: static_map
source: '@legacy_theme_name'
bypass: true
map:
bartik: olivero
seven: claro
configuration_name:
plugin: concat
source:
- '@theme_name'
- constants/config_suffix
toggle_logo: theme_settings/toggle_logo
toggle_name: value/toggle_name
toggle_slogan: value/toggle_slogan
toggle_node_user_picture: value/toggle_node_user_picture
toggle_comment_user_picture: value/toggle_comment_user_picture
toggle_comment_user_verification: value/toggle_comment_user_verification
toggle_favicon: value/toggle_favicon
default_logo: value/default_logo
logo_path: value/logo_path
logo_upload: value/logo_upload
default_favicon: value/default_favicon
favicon_path: value/favicon_path
favicon_mimetype: value/favicon_mimetype
# Ignore settings not present in Drupal 8.
# value/favicon_upload
# value/toggle_main_menu
# value/toggle_secondary_menu
destination:
plugin: d7_theme_settings

View File

@ -0,0 +1,15 @@
finished:
6:
menu:
- system
- menu_link_content
- menu_ui
system: system
# An upgrade path is not needed for jquery_ui.
jquery_ui: core
7:
menu:
- system
- menu_link_content
- menu_ui
system: system

View File

@ -0,0 +1,16 @@
id: system_image
label: Image toolkit configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- image_toolkit
source_module: system
process:
toolkit: image_toolkit
destination:
plugin: config
config_name: system.image

View File

@ -0,0 +1,16 @@
id: system_image_gd
label: Image quality configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- image_jpeg_quality
source_module: system
process:
jpeg_quality: image_jpeg_quality
destination:
plugin: config
config_name: system.image.gd

View File

@ -0,0 +1,24 @@
id: system_logging
label: System logging
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- error_level
source_module: system
process:
error_level:
plugin: static_map
source: error_level
default_value: all
map:
0: hide
1: some
2: all
3: verbose
destination:
plugin: config
config_name: system.logging

View File

@ -0,0 +1,26 @@
id: system_maintenance
label: Maintenance page configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- maintenance_mode_message
- site_offline_message
source_module: system
process:
message:
-
plugin: callback
callable: array_filter
source:
- maintenance_mode_message
- site_offline_message
-
plugin: callback
callable: current
destination:
plugin: config
config_name: system.maintenance

View File

@ -0,0 +1,16 @@
id: system_rss
label: RSS configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- feed_item_length
source_module: system
process:
'items/view_mode': feed_item_length
destination:
plugin: config
config_name: system.rss

View File

@ -0,0 +1,70 @@
id: system_site
label: Site configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
constants:
slash: '/'
variables:
- site_name
- site_mail
- site_slogan
- site_frontpage
- site_403
- site_404
- drupal_weight_select_max
- admin_compact_mode
source_module: system
process:
name: site_name
mail: site_mail
slogan: site_slogan
'page/front':
-
plugin: concat
source:
- constants/slash
- site_frontpage
-
plugin: static_map
map:
# Drupal 6 and Drupal 7 default site_frontpage is 'node'. If this
# variable is set to 'node', to an empty string, or it is completely
# missing, we want to migrate the equivalent Drupal 9 value, which is
# '/node'.
'/': '/node'
bypass: true
'page/403':
-
plugin: concat
source:
- constants/slash
- site_403
-
plugin: static_map
map:
'/': ''
bypass: true
'page/404':
-
plugin: concat
source:
- constants/slash
- site_404
-
plugin: static_map
map:
'/': ''
bypass: true
weight_select_max:
plugin: default_value
source: drupal_weight_select_max
strict: true
default_value: 100
admin_compact_mode: admin_compact_mode
destination:
plugin: config
config_name: system.site

View File

@ -0,0 +1,34 @@
<?php
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,38 @@
<?php
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 \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
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,122 @@
<?php
namespace Drupal\system\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\AccessAwareRouter;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Access check for routes implementing _access_admin_menu_block_page.
*
* @see \Drupal\system\EventSubscriber\AccessRouteAlterSubscriber
* @see \Drupal\system\Controller\SystemController::systemAdminMenuBlockPage()
*/
class SystemAdminMenuBlockAccessCheck implements AccessInterface {
/**
* Constructs a new SystemAdminMenuBlockAccessCheck.
*
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menuLinkTree
* The menu link tree service.
* @param \Drupal\Core\Routing\AccessAwareRouter $router
* The router service.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
* The menu link manager service.
*/
public function __construct(
private readonly MenuLinkTreeInterface $menuLinkTree,
private readonly AccessAwareRouter $router,
private readonly MenuLinkManagerInterface $menuLinkManager,
) {
}
/**
* Checks access.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The cron key.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account): AccessResultInterface {
$parameters = $route_match->getParameters()->all();
$route = $route_match->getRouteObject();
// Load links in the 'admin' menu matching this route. First, try to find
// the menu link using all specified parameters.
$links = $this->menuLinkManager->loadLinksByRoute($route_match->getRouteName(), $parameters, 'admin');
// If the menu link was not found, try finding it without the parameters
// that match the route defaults. Depending on whether the parameter is
// specified in the menu item with a value matching the default, or not
// specified at all, will change how it is stored in the menu_tree table. In
// both cases the route match parameters will always include the default
// parameters. This fallback method of finding the menu item is needed so
// that menu items will work in either case.
// @todo Remove this fallback in https://drupal.org/i/3359511.
if (empty($links)) {
$parameters_without_defaults = array_filter($parameters, fn ($key) => !$route->hasDefault($key) || $route->getDefault($key) !== $parameters[$key], ARRAY_FILTER_USE_KEY);
$links = $this->menuLinkManager->loadLinksByRoute($route_match->getRouteName(), $parameters_without_defaults, 'admin');
}
if (empty($links)) {
// If we did not find a link then we have no opinion on access.
return AccessResult::neutral();
}
return $this->hasAccessToChildMenuItems(reset($links), $account)->cachePerPermissions();
}
/**
* Check that the given route has access to child routes.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $link
* The menu link.
* @param \Drupal\Core\Session\AccountInterface $account
* The account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function hasAccessToChildMenuItems(MenuLinkInterface $link, AccountInterface $account): AccessResultInterface {
$parameters = new MenuTreeParameters();
$parameters->setRoot($link->getPluginId())
->excludeRoot()
->setTopLevelOnly()
->onlyEnabledLinks();
$link_url = $link->getUrlObject();
if (!$link_url->isRouted()) {
// If the link is not routed, we cannot check access to it.
return AccessResult::neutral();
}
$route = $this->router->getRouteCollection()->get($link_url->getRouteName());
if ($route && empty($route->getRequirement('_access_admin_menu_block_page')) && empty($route->getRequirement('_access_admin_overview_page'))) {
return AccessResult::allowed();
}
foreach ($this->menuLinkTree->load(NULL, $parameters) as $element) {
// Skip the link if the user does not have access.
if (!$element->link->getUrlObject()->access($account)) {
continue;
}
// If access is allowed to this element in the tree, check for access to
// any of its own children.
if ($this->hasAccessToChildMenuItems($element->link, $account)->isAllowed()) {
return AccessResult::allowed();
}
}
return AccessResult::neutral();
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Drupal\system;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining an action entity.
*/
interface ActionConfigEntityInterface extends ConfigEntityInterface {
/**
* Returns whether or not this action is configurable.
*
* @return bool
* TRUE if the action is configurable, FALSE otherwise.
*/
public function isConfigurable();
/**
* Returns the operation type.
*
* The operation type can be NULL if no type is specified.
*
* @return string|null
* The operation type, or NULL if no type is specified.
*/
public function getType();
/**
* Returns the operation plugin.
*
* @return \Drupal\Core\Action\ActionInterface
* The action plugin instance.
*/
public function getPlugin();
}

View File

@ -0,0 +1,66 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\system\ModuleAdminLinksHelper;
use Drupal\user\ModulePermissionsLinkHelper;
/**
* Controller for admin section.
*/
class AdminController extends ControllerBase {
/**
* AdminController constructor.
*/
public function __construct(
protected ModuleExtensionList $moduleExtensionList,
protected ModuleAdminLinksHelper $moduleAdminLinks,
protected ModulePermissionsLinkHelper $modulePermissionsLinks,
) {
}
/**
* Prints a listing of admin tasks, organized by module.
*
* @return array
* A render array containing the listing.
*/
public function index() {
$extensions = array_intersect_key($this->moduleExtensionList->getList(), $this->moduleHandler()->getModuleList());
uasort($extensions, [ModuleExtensionList::class, 'sortByName']);
$menu_items = [];
foreach ($extensions as $module => $extension) {
// Only display a section if there are any available tasks.
$admin_tasks = $this->moduleAdminLinks->getModuleAdminLinks($module);
if ($module_permissions_link = $this->modulePermissionsLinks->getModulePermissionsLink($module, $extension->info['name'])) {
$admin_tasks["user.admin_permissions.{$module}"] = $module_permissions_link;
}
if (!empty($admin_tasks)) {
// Sort links by title.
uasort($admin_tasks, ['\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[$extension->info['name']] = [$extension->info['description'], $admin_tasks];
}
}
$output = [
'#theme' => 'system_admin_index',
'#menu_items' => $menu_items,
];
return $output;
}
}

View File

@ -0,0 +1,255 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Asset\AssetCollectionGrouperInterface;
use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
use Drupal\Core\Asset\AssetDumperUriInterface;
use Drupal\Core\Asset\AssetGroupSetHashTrait;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Asset\LibraryDependencyResolverInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\Core\Theme\ThemeInitializationInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\system\FileDownloadController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Defines a controller to serve asset aggregates.
*/
abstract class AssetControllerBase extends FileDownloadController {
use AssetGroupSetHashTrait;
/**
* The asset type.
*
* @var string
*/
protected string $assetType;
/**
* The aggregate file extension.
*
* @var string
*/
protected string $fileExtension;
/**
* The asset aggregate content type to send as Content-Type header.
*
* @var string
*/
protected string $contentType;
/**
* The cache control header to use.
*
* Headers sent from PHP can never perfectly match those sent when the
* file is served by the filesystem, so ensure this request does not get
* cached in either the browser or reverse proxies. Subsequent requests
* for the file will be served from disk and be cached. This is done to
* avoid situations such as where one CDN endpoint is serving a version
* cached from PHP, while another is serving a version cached from disk.
* Should there be any discrepancy in behavior between those files, this
* can make debugging very difficult.
*/
protected const CACHE_CONTROL = 'private, no-store';
/**
* Constructs an object derived from AssetControllerBase.
*
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
* The stream wrapper manager.
* @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $libraryDependencyResolver
* The library dependency resolver.
* @param \Drupal\Core\Asset\AssetResolverInterface $assetResolver
* The asset resolver.
* @param \Drupal\Core\Theme\ThemeInitializationInterface $themeInitialization
* The theme initializer.
* @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
* The theme manager.
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
* The asset grouper.
* @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $optimizer
* The asset collection optimizer.
* @param \Drupal\Core\Asset\AssetDumperUriInterface $dumper
* The asset dumper.
*/
public function __construct(
StreamWrapperManagerInterface $streamWrapperManager,
protected readonly LibraryDependencyResolverInterface $libraryDependencyResolver,
protected readonly AssetResolverInterface $assetResolver,
protected readonly ThemeInitializationInterface $themeInitialization,
protected readonly ThemeManagerInterface $themeManager,
protected readonly AssetCollectionGrouperInterface $grouper,
protected readonly AssetCollectionOptimizerInterface $optimizer,
protected readonly AssetDumperUriInterface $dumper,
) {
parent::__construct($streamWrapperManager);
$this->fileExtension = $this->assetType;
}
/**
* Generates an aggregate, given a filename.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param string $file_name
* The file to deliver.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
* The transferred file as response.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the filename is invalid or an invalid query argument is
* supplied.
*/
public function deliver(Request $request, string $file_name) {
$uri = 'assets://' . $this->assetType . '/' . $file_name;
// Check to see whether a file matching the $uri already exists, this can
// happen if it was created while this request was in progress.
if (file_exists($uri)) {
return new BinaryFileResponse($uri, 200, [
'Cache-control' => static::CACHE_CONTROL,
]);
}
// First validate that the request is valid enough to produce an asset group
// aggregate. The theme must be passed as a query parameter, since assets
// always depend on the current theme.
if (!$request->query->has('theme')) {
throw new BadRequestHttpException('The theme must be passed as a query argument');
}
if (!$request->query->has('delta') || !is_numeric($request->query->get('delta'))) {
throw new BadRequestHttpException('The numeric delta must be passed as a query argument');
}
if (!$request->query->has('language')) {
throw new BadRequestHttpException('The language must be passed as a query argument');
}
if (!$request->query->has('include')) {
throw new BadRequestHttpException('The libraries to include must be passed as a query argument');
}
$file_parts = explode('_', basename($file_name, '.' . $this->fileExtension), 2);
// Ensure the filename is correctly prefixed.
if ($file_parts[0] !== $this->fileExtension) {
throw new BadRequestHttpException('The filename prefix must match the file extension');
}
// The hash is the second segment of the filename.
if (!isset($file_parts[1])) {
throw new BadRequestHttpException('Invalid filename');
}
$received_hash = $file_parts[1];
// Now build the asset groups based on the libraries. It requires the full
// set of asset groups to extract and build the aggregate for the group we
// want, since libraries may be split across different asset groups.
$theme = $request->query->get('theme');
$active_theme = $this->themeInitialization->initTheme($theme);
$this->themeManager->setActiveTheme($active_theme);
$attached_assets = new AttachedAssets();
$include_libraries = explode(',', UrlHelper::uncompressQueryParameter($request->query->get('include')));
// Check that library names are in the correct format.
$validate = function ($libraries_to_check) {
foreach ($libraries_to_check as $library) {
if (substr_count($library, '/') === 0) {
throw new BadRequestHttpException(sprintf('The "%s" library name must include at least one slash.', $library));
}
}
};
$validate($include_libraries);
$attached_assets->setLibraries($include_libraries);
if ($request->query->has('exclude')) {
$exclude_libraries = explode(',', UrlHelper::uncompressQueryParameter($request->query->get('exclude')));
$validate($exclude_libraries);
$attached_assets->setAlreadyLoadedLibraries($exclude_libraries);
}
$groups = $this->getGroups($attached_assets, $request);
$group = $this->getGroup($groups, $request->query->get('delta'));
// Generate a hash based on the asset group, this uses the same method as
// the collection optimizer does to create the filename, so it should match.
$generated_hash = $this->generateHash($group);
$data = $this->optimizer->optimizeGroup($group);
$response = new Response($data, 200, [
'Cache-control' => static::CACHE_CONTROL,
'Content-Type' => $this->contentType,
]);
// However, the hash from the library definitions in code may not match the
// hash from the URL. This can be for three reasons:
// 1. Someone has requested an outdated URL, i.e. from a cached page, which
// matches a different version of the code base.
// 2. Someone has requested an outdated URL during a deployment. This is
// the same case as #1 but a much shorter window.
// 3. Someone is attempting to craft an invalid URL in order to conduct a
// denial of service attack on the site.
// Dump the optimized group into an aggregate file, but only if the
// received hash and generated hash match. This prevents invalid filenames
// from filling the disk, while still serving aggregates that may be
// referenced in cached HTML.
if (hash_equals($generated_hash, $received_hash)) {
$this->dumper->dumpToUri($data, $this->assetType, $uri);
}
else {
$expected_filename = $this->fileExtension . '_' . $generated_hash . '.' . $this->fileExtension;
$response = new RedirectResponse(
str_replace($file_name, $expected_filename, $request->getRequestUri()),
301,
['Cache-Control' => 'public, max-age=3600, must-revalidate'],
);
}
return $response;
}
/**
* Gets a group.
*
* @param array $groups
* An array of asset groups.
* @param int $group_delta
* The group delta.
*
* @return array
* The correct asset group matching $group_delta.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the filename is invalid.
*/
protected function getGroup(array $groups, int $group_delta): array {
if (isset($groups[$group_delta])) {
return $groups[$group_delta];
}
throw new BadRequestHttpException('Invalid filename.');
}
/**
* Get grouped assets.
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $attached_assets
* The attached assets.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* The grouped assets.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the query argument is omitted.
*/
abstract protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request): array;
}

View File

@ -0,0 +1,107 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Batch\BatchStorageInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
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 {
use StringTranslationTrait;
/**
* Constructs a new BatchController.
*/
public function __construct(
protected string $root,
protected BatchStorageInterface $batchStorage,
) {
require_once $this->root . '/core/includes/batch.inc';
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->getParameter('app.root'),
$container->get('batch.storage'),
);
}
/**
* 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) {
$output = _batch_page($request);
if ($output === FALSE) {
throw new AccessDeniedHttpException();
}
elseif ($output instanceof Response) {
return $output;
}
elseif (isset($output)) {
$title = $output['#title'] ?? NULL;
$page = [
'#type' => 'page',
'#title' => $title,
'#show_messages' => FALSE,
'content' => $output,
];
// Also inject title as a page header (if available).
if ($title) {
$page['header'] = [
'#type' => 'page_title',
'#title' => $title,
];
}
return $page;
}
}
/**
* The _title_callback for the system.batch_page.html route.
*
* @return string
* The page title.
*/
public function batchPageTitle(Request $request) {
$batch = &batch_get();
if (!($request_id = $request->query->get('id'))) {
return '';
}
// Retrieve the current state of the batch.
if (!$batch) {
$batch = $this->batchStorage->load($request_id);
}
if (!$batch) {
return '';
}
$current_set = _batch_current_set();
return !empty($current_set['title']) ? $current_set['title'] : '';
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Access\CsrfRequestHeaderAccessCheck;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Returns responses for CSRF token routes.
*/
class CsrfTokenController implements ContainerInjectionInterface {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $tokenGenerator;
/**
* Constructs a new CsrfTokenController object.
*
* @param \Drupal\Core\Access\CsrfTokenGenerator $token_generator
* The CSRF token generator.
*/
public function __construct(CsrfTokenGenerator $token_generator) {
$this->tokenGenerator = $token_generator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('csrf_token')
);
}
/**
* Returns a CSRF protecting session token.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function csrfToken() {
return new Response($this->tokenGenerator->get(CsrfRequestHeaderAccessCheck::TOKEN_KEY), 200, ['Content-Type' => 'text/plain']);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Drupal\system\Controller;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Asset\AssetGroupSetHashTrait;
/**
* Defines a controller to serve CSS aggregates.
*/
class CssAssetController extends AssetControllerBase {
use AssetGroupSetHashTrait;
/**
* {@inheritdoc}
*/
protected string $contentType = 'text/css';
/**
* {@inheritdoc}
*/
protected string $assetType = 'css';
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('stream_wrapper_manager'),
$container->get('library.dependency_resolver'),
$container->get('asset.resolver'),
$container->get('theme.initialization'),
$container->get('theme.manager'),
$container->get('asset.css.collection_grouper'),
$container->get('asset.css.collection_optimizer'),
$container->get('asset.css.dumper'),
);
}
/**
* {@inheritdoc}
*/
protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request): array {
$language = $this->languageManager()->getLanguage($request->get('language'));
$assets = $this->assetResolver->getCssAssets($attached_assets, FALSE, $language);
return $this->grouper->group($assets);
}
}

View File

@ -0,0 +1,739 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Asset\AssetQueryStringInterface;
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\Requirement\RequirementSeverity;
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\Update\UpdateRegistry;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* 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 bare HTML page renderer.
*
* @var \Drupal\Core\Render\BareHtmlPageRendererInterface
*/
protected $bareHtmlPageRenderer;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* The post update registry.
*
* @var \Drupal\Core\Update\UpdateRegistry
*/
protected $postUpdateRegistry;
/**
* 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\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
* The bare HTML page renderer.
* @param \Drupal\Core\Update\UpdateRegistry $post_update_registry
* The post update registry.
* @param \Drupal\Core\Asset\AssetQueryStringInterface $assetQueryString
* The asset query string.
*/
public function __construct(
$root,
KeyValueExpirableFactoryInterface $key_value_expirable_factory,
CacheBackendInterface $cache,
StateInterface $state,
ModuleHandlerInterface $module_handler,
AccountInterface $account,
BareHtmlPageRendererInterface $bare_html_page_renderer,
UpdateRegistry $post_update_registry,
protected AssetQueryStringInterface $assetQueryString,
) {
$this->root = $root;
$this->keyValueExpirableFactory = $key_value_expirable_factory;
$this->cache = $cache;
$this->state = $state;
$this->moduleHandler = $module_handler;
$this->account = $account;
$this->bareHtmlPageRenderer = $bare_html_page_renderer;
$this->postUpdateRegistry = $post_update_registry;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->getParameter('app.root'),
$container->get('keyvalue.expirable'),
$container->get('cache.default'),
$container->get('state'),
$container->get('module_handler'),
$container->get('current_user'),
$container->get('bare_html_page_renderer'),
$container->get('update.post_update_registry'),
$container->get('asset.query_string')
);
}
/**
* 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.
*/
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();
if ($request->query->get('continue')) {
$request->getSession()->set('update_ignore_warnings', TRUE);
}
$regions = [];
$requirements = update_check_requirements();
$severity = RequirementSeverity::maxSeverityFromRequirements($requirements);
if ($severity === RequirementSeverity::Error || ($severity === RequirementSeverity::Warning && !$request->getSession()->has('update_ignore_warnings'))) {
$regions['sidebar_first'] = $this->updateTasksList('requirements');
$output = $this->requirements($severity, $requirements, $request);
}
else {
switch ($op) {
case 'selection':
$regions['sidebar_first'] = $this->updateTasksList('selection');
$output = $this->selection($request);
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($request);
break;
case 'results':
$regions['sidebar_first'] = $this->updateTasksList('results');
$output = $this->results($request);
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 = $output['#title'] ?? $this->t('Drupal database update');
return $this->bareHtmlPageRenderer->renderBarePage($output, $title, 'maintenance_page', $regions);
}
/**
* Returns the info database update page.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array.
*/
protected function info(Request $request) {
// Change query-strings on css/js files to enforce reload for all users.
$this->assetQueryString->reset();
// 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'] = [
'#markup' => '<p>' . $this->t('Use this utility to update your database whenever a module, theme, or the core software is updated.') . '</p><p>' . $this->t('For more detailed information, see the <a href="https://www.drupal.org/docs/updating-drupal">Updating Drupal guide</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.");
// @todo Simplify with https://www.drupal.org/node/2548095
$base_url = str_replace('/update.php', '', $request->getBaseUrl());
$info[] = $this->t('Put your site into <a href=":url">maintenance mode</a>.', [
':url' => Url::fromRoute('system.site_maintenance_mode')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl(),
]);
$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('Update your files (as described in the handbook page linked above).');
$build['info'] = [
'#theme' => 'item_list',
'#list_type' => 'ol',
'#items' => $info,
];
$build['info_footer'] = [
'#markup' => '<p>' . $this->t('When you have performed the steps above, you may proceed.') . '</p>',
];
$build['link'] = [
'#type' => 'link',
'#title' => $this->t('Continue'),
'#attributes' => ['class' => ['button', 'button--primary']],
// @todo Revisit once https://www.drupal.org/node/2548095 is in.
'#url' => Url::fromUri('base://selection'),
];
return $build;
}
/**
* Renders a list of available database updates.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array.
*/
protected function selection(Request $request) {
// Make sure there is no stale theme registry.
$this->cache->deleteAll();
$count = 0;
$incompatible_count = 0;
$build['start'] = [
'#tree' => TRUE,
'#type' => 'details',
];
// Ensure system.module's updates appear first.
$build['start']['system'] = [];
$starting_updates = [];
$incompatible_updates_exist = FALSE;
$updates_per_extension = [];
foreach (['update', 'post_update'] as $update_type) {
switch ($update_type) {
case 'update':
$updates = update_get_update_list();
break;
case 'post_update':
$updates = $this->postUpdateRegistry->getPendingUpdateInformation();
break;
}
foreach ($updates as $extension => $update) {
if (!isset($update['start'])) {
$build['start'][$extension] = [
'#type' => 'item',
'#title' => $extension . ($this->moduleHandler->moduleExists($extension) ? ' module' : ' theme'),
'#markup' => $update['warning'],
'#prefix' => '<div class="messages messages--warning">',
'#suffix' => '</div>',
];
$incompatible_updates_exist = TRUE;
continue;
}
if (!empty($update['pending'])) {
$updates_per_extension += [$extension => []];
$updates_per_extension[$extension] = array_merge($updates_per_extension[$extension], $update['pending']);
$build['start'][$extension] = [
'#type' => 'hidden',
'#value' => $update['start'],
];
// Store the previous items in order to merge normal updates and
// post_update functions together.
$build['start'][$extension] = [
'#theme' => 'item_list',
'#items' => $updates_per_extension[$extension],
'#title' => $extension . ($this->moduleHandler->moduleExists($extension) ? ' module' : ' theme'),
];
if ($update_type === 'update') {
$starting_updates[$extension] = $update['start'];
}
}
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'] = [$module_update_key => $build['start'][$module_update_key]] + $build['start'];
}
}
// Warn the user if any updates were incompatible.
if ($incompatible_updates_exist) {
$this->messenger()->addWarning($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'));
}
if (empty($count)) {
$this->messenger()->addStatus($this->t('No pending updates.'));
unset($build);
$build['links'] = [
'#theme' => 'links',
'#links' => $this->helpfulLinks($request),
];
// No updates to run, so caches won't get flushed later. Clear them now.
drupal_flush_all_caches();
}
else {
$build['help'] = [
'#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)',
['@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count]
);
}
else {
$build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates');
}
// @todo Simplify with https://www.drupal.org/node/2548095
$base_url = str_replace('/update.php', '', $request->getBaseUrl());
$url = (new Url('system.db_update', ['op' => 'run']))->setOption('base_url', $base_url);
$build['link'] = [
'#type' => 'link',
'#title' => $this->t('Apply pending updates'),
'#attributes' => ['class' => ['button', 'button--primary']],
'#weight' => 5,
'#url' => $url,
'#access' => $url->access($this->currentUser()),
];
}
return $build;
}
/**
* Displays results of the update script with any accompanying errors.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array.
*/
protected function results(Request $request) {
// @todo Simplify with https://www.drupal.org/node/2548095
$base_url = str_replace('/update.php', '', $request->getBaseUrl());
// Retrieve and remove session information.
$session = $request->getSession();
$update_results = $session->remove('update_results');
$update_success = $session->remove('update_success');
$session->remove('update_ignore_warnings');
// 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>.', [
':url' => Url::fromRoute('dblog.overview')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl(),
]);
}
else {
$log_message = $this->t('All errors have been logged.');
}
if ($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.', [':url' => Url::fromRoute('<front>')->setOption('base_url', $base_url)->toString(TRUE)->getGeneratedUrl()]) . ' ' . $log_message . '</p>';
}
else {
$last = $session->get('updates_remaining');
$last = reset($last);
[$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>.', [
'@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'] = [
'#markup' => $message,
];
$build['links'] = [
'#theme' => 'links',
'#links' => $this->helpfulLinks($request),
];
// Output a list of info messages.
if (!empty($update_results)) {
$all_messages = [];
foreach ($update_results as $extension => $updates) {
if ($extension != '#abort') {
$extension_has_message = FALSE;
$info_messages = [];
foreach ($updates as $name => $queries) {
$messages = [];
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[] = [
'#wrapper_attributes' => ['class' => ['success']],
'#markup' => $query['query'],
];
}
else {
$messages[] = [
'#wrapper_attributes' => ['class' => ['failure']],
'#markup' => '<strong>' . $this->t('Failed:') . '</strong> ' . $query['query'],
];
}
}
if ($messages) {
$extension_has_message = TRUE;
if (is_numeric($name)) {
$title = $this->t('Update #@count', ['@count' => $name]);
}
else {
$title = $this->t('Update @name', ['@name' => trim($name, '_')]);
}
$info_messages[] = [
'#theme' => 'item_list',
'#items' => $messages,
'#title' => $title,
];
}
}
// If there were any messages then prefix them with the extension name
// and add it to the global message list.
if ($extension_has_message) {
$header = $this->moduleHandler->moduleExists($extension) ?
$this->t('@module module', ['@module' => $extension]) :
$this->t('@theme theme', ['@theme' => $extension]);
$all_messages[] = [
'#type' => 'container',
'#prefix' => '<h3>' . $header . '</h3>',
'#children' => $info_messages,
];
}
}
}
if ($all_messages) {
$build['query_messages'] = [
'#type' => 'container',
'#children' => $all_messages,
'#attributes' => ['class' => ['update-results']],
'#prefix' => '<h2>' . $this->t('The following updates returned messages:') . '</h2>',
];
}
}
return $build;
}
/**
* Renders a list of requirement errors or warnings.
*
* @param int $severity
* The severity of the message, as per RFC 3164.
* @param array $requirements
* The array of requirement values.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* A render array.
*/
public function requirements($severity, array $requirements, Request $request) {
$options = $severity === RequirementSeverity::Warning ? ['continue' => 1] : [];
// @todo Revisit once https://www.drupal.org/node/2548095 is in. Something
// like Url::fromRoute('system.db_update')->setOptions() should then be
// possible.
$try_again_url = Url::fromUri($request->getUriForPath(''))->setOptions(['query' => $options])->toString(TRUE)->getGeneratedUrl();
$build['status_report'] = [
'#type' => 'status_report',
'#requirements' => $requirements,
'#suffix' => $this->t('Check the messages and <a href=":url">try again</a>.', [':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 = [
'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 = [
'#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) {
$maintenance_mode = $this->state->get('system.maintenance_mode', FALSE);
// Store the current maintenance mode status in the session so that it can
// be restored at the end of the batch.
$request->getSession()->set('maintenance_mode', $maintenance_mode);
// During the update, always put the site into maintenance mode so that
// in-progress schema changes do not affect visiting users.
if (empty($maintenance_mode)) {
$this->state->set('system.maintenance_mode', TRUE);
}
/** @var \Drupal\Core\Batch\BatchBuilder $batch_builder */
$batch_builder = (new BatchBuilder())
->setTitle($this->t('Updating'))
->setInitMessage($this->t('Starting updates'))
->setErrorMessage($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.'))
->setFinishCallback([DbUpdateController::class, 'batchFinished']);
// 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 = [];
foreach ($updates as $function => $update) {
$dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : [];
}
// Determine updates to be performed.
foreach ($updates as $function => $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::service('update.update_hook_registry')->setInstalledVersion($update['module'], $update['number'] - 1);
unset($start[$update['module']]);
}
$batch_builder->addOperation('update_do_one', [$update['module'], $update['number'], $dependency_map[$function]]);
}
}
$post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions();
if ($post_updates) {
// Now we rebuild all caches and after that execute the hook_post_update()
// functions.
$batch_builder->addOperation('drupal_flush_all_caches', []);
foreach ($post_updates as $function) {
$batch_builder->addOperation('update_invoke_post_update', [$function]);
}
}
batch_set($batch_builder->toArray());
// @todo Revisit once https://www.drupal.org/node/2548095 is in.
return batch_process(Url::fromUri('base://results'), Url::fromUri('base://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 bool $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 = \Drupal::request()->getSession();
$session->set('update_results', $results);
$session->set('update_success', $success);
$session->set('updates_remaining', $operations);
// Now that the update is done, we can put the site back online if it was
// previously not in maintenance mode.
if (!$session->remove('maintenance_mode')) {
\Drupal::state()->set('system.maintenance_mode', FALSE);
}
}
/**
* Provides links to the homepage and administration pages.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array
* An array of links.
*/
protected function helpfulLinks(Request $request) {
// @todo Simplify with https://www.drupal.org/node/2548095
$base_url = str_replace('/update.php', '', $request->getBaseUrl());
$links['front'] = [
'title' => $this->t('Front page'),
'url' => Url::fromRoute('<front>')->setOption('base_url', $base_url),
];
if ($this->account->hasPermission('access administration pages')) {
$links['admin-pages'] = [
'title' => $this->t('Administration pages'),
'url' => Url::fromRoute('system.admin')->setOption('base_url', $base_url),
];
}
if ($this->account->hasPermission('administer site configuration')) {
$links['status-report'] = [
'title' => $this->t('Status report'),
'url' => Url::fromRoute('system.status')->setOption('base_url', $base_url),
];
}
return $links;
}
/**
* Retrieves module updates.
*
* @return array
* The module updates that can be performed.
*/
protected function getModuleUpdates() {
$return = [];
$updates = update_get_update_list();
foreach ($updates as $module => $update) {
$return[$module] = $update['start'];
}
return $return;
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Tags;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityAutocompleteMatcherInterface;
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\EntityAutocompleteMatcherInterface
*/
protected $matcher;
/**
* The key value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValue;
/**
* Constructs an EntityAutocompleteController object.
*
* @param \Drupal\Core\Entity\EntityAutocompleteMatcherInterface $matcher
* The autocomplete matcher for entity references.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value
* The key value factory.
*/
public function __construct(EntityAutocompleteMatcherInterface $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 = [];
// Get the typed string from the URL, if it exists.
$input = $request->query->get('q');
// Check this string for emptiness, but allow any non-empty string.
if (is_string($input) && strlen($input)) {
$tag_list = Tags::explode($input);
$typed_string = !empty($tag_list) ? mb_strtolower(array_pop($tag_list)) : '';
// 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 (!hash_equals($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();
}
$entity_type_id = $request->query->get('entity_type');
if ($entity_type_id && $this->entityTypeManager()->hasDefinition($entity_type_id)) {
$entity_id = $request->query->get('entity_id');
if ($entity_id) {
$entity = $this->entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
if ($entity->access('update')) {
$selection_settings['entity'] = $entity;
}
}
}
$matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string);
}
return new JsonResponse($matches);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller for default HTTP 4xx responses.
*/
class Http4xxController extends ControllerBase {
/**
* The default 4xx error content.
*
* @return array
* A render array containing the message to display for 4xx errors.
*/
public function on4xx() {
return [
'#markup' => $this->t('A client error happened'),
];
}
/**
* The default 401 content.
*
* @return array
* A render array containing the message to display for 401 pages.
*/
public function on401() {
return [
'#markup' => $this->t('Log in to access this page.'),
];
}
/**
* The default 403 content.
*
* @return array
* A render array containing the message to display for 403 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,64 @@
<?php
namespace Drupal\system\Controller;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Asset\AssetGroupSetHashTrait;
/**
* Defines a controller to serve Javascript aggregates.
*/
class JsAssetController extends AssetControllerBase {
use AssetGroupSetHashTrait;
/**
* {@inheritdoc}
*/
protected string $contentType = 'text/javascript';
/**
* {@inheritdoc}
*/
protected string $assetType = 'js';
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('stream_wrapper_manager'),
$container->get('library.dependency_resolver'),
$container->get('asset.resolver'),
$container->get('theme.initialization'),
$container->get('theme.manager'),
$container->get('asset.js.collection_grouper'),
$container->get('asset.js.collection_optimizer'),
$container->get('asset.js.dumper'),
);
}
/**
* {@inheritdoc}
*/
protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request): array {
// The header and footer scripts are two distinct sets of asset groups. The
// $group_key is not sufficient to find the group, we also need to locate it
// within either the header or footer set.
$language = $this->languageManager()->getLanguage($request->get('language'));
[$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($attached_assets, FALSE, $language);
$scope = $request->get('scope');
if (!isset($scope)) {
throw new BadRequestHttpException('The URL must have a scope query argument.');
}
$assets = $scope === 'header' ? $js_assets_header : $js_assets_footer;
// While the asset resolver will find settings, these are never aggregated,
// so filter them out.
unset($assets['drupalSettings']);
return $this->grouper->group($assets);
}
}

View File

@ -0,0 +1,282 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\system\MenuInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Linkset controller.
*
* Provides a menu endpoint.
*
* @internal
* This class's API is internal and it is not intended for extension.
*/
final class LinksetController extends ControllerBase {
/**
* Linkset constructor.
*
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menuTree
* The menu tree loader service. This is used to load a menu's link
* elements so that they can be serialized into a linkset response.
*/
public function __construct(protected readonly MenuLinkTreeInterface $menuTree) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('menu.link_tree'));
}
/**
* Serve linkset requests.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* An HTTP request.
* @param \Drupal\system\MenuInterface $menu
* A menu for which to produce a linkset.
*
* @return \Drupal\Core\Cache\CacheableJsonResponse
* A linkset response.
*/
public function process(Request $request, MenuInterface $menu) {
// Load the given menu's tree of elements.
$tree = $this->loadMenuTree($menu);
// Get the incoming request URI and parse it so the linkset can use a
// relative URL for the linkset anchor.
['path' => $path, 'query' => $query] = parse_url($request->getUri()) + ['query' => FALSE];
// Construct a relative URL.
$anchor = $path . (!empty($query) ? '?' . $query : '');
$cacheability = CacheableMetadata::createFromObject($menu);
// Encode the menu tree as links in the application/linkset+json media type
// and add the machine name of the menu to which they belong.
$menu_id = $menu->id();
$links = $this->toLinkTargetObjects($tree, $cacheability);
foreach ($links as $rel => $target_objects) {
$links[$rel] = array_map(function (array $target) use ($menu_id) {
// According to the Linkset specification, this member must be an array
// since the "machine-name" target attribute is non-standard.
// See https://tools.ietf.org/html/draft-ietf-httpapi-linkset-08#section-4.2.4.3
return $target + ['machine-name' => [$menu_id]];
}, $target_objects);
}
$linkset = !empty($tree)
? [['anchor' => $anchor] + $links]
: [];
$data = ['linkset' => $linkset];
// Set the response content-type header.
$headers = ['content-type' => 'application/linkset+json'];
$response = new CacheableJsonResponse($data, 200, $headers);
// Attach cacheability metadata to the response.
$response->addCacheableDependency($cacheability);
return $response;
}
/**
* Encode a menu tree as link items and capture any cacheability metadata.
*
* This method recursively traverses the given menu tree to produce a flat
* array of link items encoded according the application/linkset+json
* media type.
*
* To preserve hierarchical information, the target attribute contains a
* `hierarchy` member. Its value is an array containing the position of a link
* within a particular sub-tree prepended by the positions of its ancestors,
* and can be used to reconstruct a hierarchical data structure.
*
* The reason that a `hierarchy` member is used instead of a `parent` or
* `children` member is because it is more compact, more suited to the linkset
* media type, and because it simplifies many menu operations. Specifically:
*
* 1. Creating a `parent` member would require each link to have an `id`
* in order to have something referenceable by the `parent` member. Reusing
* the link plugin IDs would not be viable because it would leak
* information about which modules are installed on the site. Therefore,
* this ID would have to be invented and would probably end up looking a
* lot like the `hierarchy` value. Finally, link IDs would encourage
* clients to hardcode the ID instead of using link relation types
* appropriately.
* 2. The linkset media type is not itself hierarchical. This means that
* `children` is infeasible without inventing our own Drupal-specific media
* type.
* 3. The `hierarchy` member can be used to efficiently perform tree
* operations that would otherwise be more complicated to implement. For
* example, by comparing the first X amount of hierarchy levels, you can
* find any subtree without writing recursive logic or complicated loops.
* Visit the URL below for more examples.
*
* The structure of a `hierarchy` value is defined below.
*
* A link which is a child of another link will always be prefixed by the
* exact value of their parent's hierarchy member. For example, if a link /bar
* is a child of a link /foo and /foo has a hierarchy member with the value
* ["1"], then the link /bar might have a hierarchy member with the value
* ["1", "0"]. The link /foo can be said to have depth 1, while the link
* /bar can be said to have depth 2.
*
* Links which have the same parent (or no parent) have their relative order
* preserved in the final component of the hierarchy value.
*
* According to the Linkset specification, each value in the hierarchy array
* must be a string. See
* https://tools.ietf.org/html/draft-ietf-httpapi-linkset-08#section-4.2.4.3
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* A tree of menu elements.
* @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability
* An object to capture any cacheability metadata.
* @param array $hierarchy_ancestors
* (Internal use only) The hierarchy value of the parent element
* if $tree is a subtree. Do not pass this value.
*
* @return array
* An array which can be JSON-encoded to represent the given link tree.
*
* @see https://www.drupal.org/project/decoupled_menus/issues/3204132#comment-14439385
*/
protected function toLinkTargetObjects(array $tree, RefinableCacheableDependencyInterface $cacheability, $hierarchy_ancestors = []): array {
$links = [];
// Calling array_values() discards any key names so that $index will be
// numerical.
foreach (array_values($tree) as $index => $element) {
// Extract and preserve the access cacheability metadata.
$element_access = $element->access;
assert($element_access instanceof AccessResultInterface);
$cacheability->addCacheableDependency($element_access);
// If an element is not accessible, it should not be encoded. Its
// cacheability should be preserved regardless, which is why that is done
// outside of this conditional.
if ($element_access->isAllowed()) {
// Get and generate the URL of the link's target. This can create
// cacheability metadata also.
$url = $element->link->getUrlObject();
$generated_url = $url->toString(TRUE);
$cacheability = $cacheability->addCacheableDependency($generated_url);
// Take the hierarchy value for the current element and append it
// to the link element parent's hierarchy value. See this method's
// docblock for more context on why this value is the way it is.
$hierarchy = $hierarchy_ancestors;
array_push($hierarchy, strval($index));
$link_options = $element->link->getOptions();
$link_attributes = ($link_options['attributes'] ?? []);
$link_rel = $link_attributes['rel'] ?? 'item';
// Encode the link.
$link = [
'href' => $generated_url->getGeneratedUrl(),
// @todo should this use the "title*" key if it is internationalized?
// Follow up issue:
// https://www.drupal.org/project/decoupled_menus/issues/3280735
'title' => $element->link->getTitle(),
'hierarchy' => $hierarchy,
];
$this->processCustomLinkAttributes($link, $link_attributes);
$links[$link_rel][] = $link;
// Recurse into the element's subtree.
if (!empty($element->subtree)) {
// Recursion!
$links = array_merge_recursive($links, $this->toLinkTargetObjects($element->subtree, $cacheability, $hierarchy));
}
}
}
return $links;
}
/**
* Process custom link parameters.
*
* Since the values for attributes are dynamic and we can't
* guarantee that they adhere to the linkset specification,
* we do some custom processing as follows,
* 1. Transform all of them into an array if
* they are not already an array.
* 2. Transform all non-string values into strings
* (e.g. ["42"] instead of [42])
* 3. Ignore (for now) any keys that are already specified.
* Namely: hreflang, media, type, title, and title*.
* 4. Ensure that custom names do not contain an
* asterisk and ignore them if they do.
* 5. These attributes require special handling. For instance,
* these parameters must be strings instead of an array of strings.
*
* NOTE: Values which are not object/array are cast to string.
*
* @param array $link
* Link structure.
* @param array $attributes
* Attributes available for the link.
*/
private function processCustomLinkAttributes(array &$link, array $attributes = []) {
$attribute_keys_to_ignore = [
'hreflang',
'media',
'type',
'title',
'title*',
];
foreach ($attributes as $key => $value) {
if (in_array($key, $attribute_keys_to_ignore, TRUE)) {
continue;
}
// Skip the attribute key if it has an asterisk (*).
if (str_contains($key, '*')) {
continue;
}
// Skip the value if it is an object.
if (is_object($value)) {
continue;
}
// See https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-linkset-03#section-4.2.4.3
// Values for custom attributes must follow these rules,
// - Values MUST be array.
// - Each item in the array MUST be a string.
if (is_array($value)) {
$link[$key] = [];
foreach ($value as $val) {
if (is_object($val) || is_array($val)) {
continue;
}
$link[$key][] = (string) $val;
}
}
else {
$link[$key] = [(string) $value];
}
}
}
/**
* Loads a menu tree.
*
* @param \Drupal\system\MenuInterface $menu
* A menu for which a tree should be loaded.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* A menu link tree.
*/
protected function loadMenuTree(MenuInterface $menu) : array {
$parameters = new MenuTreeParameters();
$parameters->onlyEnabledLinks();
$parameters->setMinDepth(0);
$tree = $this->menuTree->load($menu->id(), $parameters);
$manipulators = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
];
return $this->menuTree->transform($tree, $manipulators);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\system\Form\ClearCacheForm;
use Drupal\system\Form\PerformanceForm;
/**
* Controller for performance admin.
*/
class PerformanceController extends ControllerBase {
/**
* Displays the system performance page.
*
* @return array
* A render array containing the cache-clear form and performance
* configuration form.
*/
public function build(): array {
// Load the cache form and embed it in a details element.
$cache_clear = $this->formBuilder()->getForm(ClearCacheForm::class);
$cache_clear['clear_cache'] = [
'#type' => 'details',
'#title' => $this->t('Clear cache'),
'#open' => TRUE,
'clear' => $cache_clear['clear'],
];
unset($cache_clear['clear']);
return [
'cache_clear' => $cache_clear,
'performance' => $this->formBuilder()->getForm(PerformanceForm::class),
];
}
}

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