first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

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,10 @@
/**
* @file
* Collapsible details.
*
* @see collapse.js
*/
.js details:not([open]) .details-wrapper {
display: none;
}

View File

@@ -0,0 +1,9 @@
/**
* @file
* Fieldgroup border reset.
*/
.fieldgroup {
padding: 0;
border-width: 0;
}

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 {
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
* Utility class to prevent text wrapping.
*/
.nowrap {
white-space: nowrap;
}

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,21 @@
/**
* @file
* Resizable textareas.
*/
.resize-none {
resize: none;
}
.resize-vertical {
min-height: 2em;
resize: vertical;
}
.resize-horizontal {
max-width: 100%;
resize: horizontal;
}
.resize-both {
max-width: 100%;
min-height: 2em;
resize: both;
}

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: rgba(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,19 @@
/**
* @file
* Table sort indicator.
*
* @see tablesort-indicator.html.twig
*/
.tablesort {
display: inline-block;
width: 16px;
height: 16px;
background-size: 100%;
}
.tablesort--asc {
background-image: url(../../../../misc/icons/787878/twistie-down.svg);
}
.tablesort--desc {
background-image: url(../../../../misc/icons/787878/twistie-up.svg);
}

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 Manager 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,119 @@
<?php
namespace Drupal\system\Access;
use Drupal\Core\Access\AccessManagerInterface;
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\Access\AccessManagerInterface $accessManager
* The access manager.
* @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 AccessManagerInterface $accessManager,
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();
$route = $this->router->getRouteCollection()->get($link->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) {
if (!$this->accessManager->checkNamedRoute($element->link->getRouteName(), $element->link->getRouteParameters(), $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,33 @@
<?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
*/
public function isConfigurable();
/**
* Returns the operation type.
*
* @return string
*/
public function getType();
/**
* Returns the operation plugin.
*
* @return \Drupal\Core\Action\ActionInterface
*/
public function getPlugin();
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\system\ModuleAdminLinksHelper;
use Drupal\user\ModulePermissionsLinkHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controller for admin section.
*/
class AdminController extends ControllerBase {
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* The module admin links service.
*
* @var \Drupal\system\ModuleAdminLinksHelper
*/
protected ModuleAdminLinksHelper $moduleAdminLinks;
/**
* The module permissions link service.
*
* @var \Drupal\user\ModulePermissionsLinkHelper
*/
protected ModulePermissionsLinkHelper $modulePermissionsLinks;
/**
* AdminController constructor.
*
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
* The module extension list.
* @param \Drupal\system\ModuleAdminLinksHelper|null $module_admin_links
* The module admin links.
* @param \Drupal\user\ModulePermissionsLinkHelper|null $module_permissions_link
* The module permission link.
*/
public function __construct(ModuleExtensionList $extension_list_module, ?ModuleAdminLinksHelper $module_admin_links = NULL, ?ModulePermissionsLinkHelper $module_permissions_link = NULL) {
$this->moduleExtensionList = $extension_list_module;
if (!isset($module_admin_links)) {
@trigger_error('Calling ' . __METHOD__ . '() without the $module_admin_tasks_helper argument is deprecated in drupal:10.2.0 and the $module_admin_tasks_helper argument will be required in drupal:11.0.0. See https://www.drupal.org/node/3038972', E_USER_DEPRECATED);
$module_admin_links = \Drupal::service('system.module_admin_links_helper');
}
$this->moduleAdminLinks = $module_admin_links;
if (!isset($module_permissions_link)) {
@trigger_error('Calling ' . __METHOD__ . ' without the $module_permissions_link argument is deprecated in drupal:10.2.0 and the $module_permissions_link argument will be required in drupal:11.0.0. See https://www.drupal.org/node/3038972', E_USER_DEPRECATED);
$module_permissions_link = \Drupal::service('user.module_permissions_link_helper');
}
$this->modulePermissionsLinks = $module_permissions_link;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('extension.list.module'),
$container->get('system.module_admin_links_helper'),
$container->get('user.module_permissions_link_helper')
);
}
/**
* 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,244 @@
<?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\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);
// 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);
}
return new Response($data, 200, [
'Cache-control' => static::CACHE_CONTROL,
'Content-Type' => $this->contentType,
]);
}
/**
* 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,726 @@
<?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\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 = NULL) {
$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;
if ($this->assetQueryString === NULL) {
$this->assetQueryString = \Drupal::service('asset.query_string');
@trigger_error('Calling' . __METHOD__ . '() without the $assetQueryString argument is deprecated in drupal:10.2.0 and is required in drupal:11.0.0. See https://www.drupal.org/node/3358337', E_USER_DEPRECATED);
}
}
/**
* {@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 = drupal_requirements_severity($requirements);
if ($severity == REQUIREMENT_ERROR || ($severity == REQUIREMENT_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/upgrade">upgrading handbook</a>. If you are unsure what these terms mean you should probably contact your hosting provider.') . '</p>',
];
$info[] = $this->t("<strong>Back up your code</strong>. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.");
// @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 $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 == REQUIREMENT_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 $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),
];
}
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,281 @@
<?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,28 @@
<?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 {
return [
'cache_clear' => $this->formBuilder()->getForm(ClearCacheForm::class),
'performance' => $this->formBuilder()->getForm(PerformanceForm::class),
];
}
}

View File

@@ -0,0 +1,408 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Extension\ModuleDependencyMessageTrait;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Link;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Theme\ThemeAccessCheck;
use Drupal\Core\Url;
use Drupal\system\SystemManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for System routes.
*/
class SystemController extends ControllerBase {
use ModuleDependencyMessageTrait;
/**
* System Manager Service.
*
* @var \Drupal\system\SystemManager
*/
protected $systemManager;
/**
* The theme access checker service.
*
* @var \Drupal\Core\Theme\ThemeAccessCheck
*/
protected $themeAccess;
/**
* The form builder service.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuLinkTree;
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* The theme extension list.
*
* @var \Drupal\Core\Extension\ThemeExtensionList
*/
protected ThemeExtensionList $themeExtensionList;
/**
* Constructs a new SystemController.
*
* @param \Drupal\system\SystemManager $systemManager
* System manager service.
* @param \Drupal\Core\Theme\ThemeAccessCheck $theme_access
* The theme access checker service.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
* @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list
* The module extension list.
* @param \Drupal\Core\Extension\ThemeExtensionList $theme_extension_list
* The theme extension list.
*/
public function __construct(SystemManager $systemManager, ThemeAccessCheck $theme_access, FormBuilderInterface $form_builder, MenuLinkTreeInterface $menu_link_tree, ModuleExtensionList $module_extension_list, ThemeExtensionList $theme_extension_list) {
$this->systemManager = $systemManager;
$this->themeAccess = $theme_access;
$this->formBuilder = $form_builder;
$this->menuLinkTree = $menu_link_tree;
$this->moduleExtensionList = $module_extension_list;
$this->themeExtensionList = $theme_extension_list;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('system.manager'),
$container->get('access_check.theme'),
$container->get('form_builder'),
$container->get('menu.link_tree'),
$container->get('extension.list.module'),
$container->get('extension.list.theme'),
);
}
/**
* Provide the administration overview page.
*
* This will render child links two levels below the specified link ID,
* grouped by the child links one level below.
*
* @param string $link_id
* The ID of the administrative path link for which to display child links.
*
* @return array
* A renderable array of the administration overview page.
*/
public function overview($link_id) {
// Check for status report errors.
if ($this->currentUser()->hasPermission('administer site configuration') && $this->systemManager->checkRequirements()) {
$this->messenger()->addError($this->t('One or more problems were detected with your Drupal installation. Check the <a href=":status">status report</a> for more information.', [':status' => Url::fromRoute('system.status')->toString()]));
}
// Load all menu links below it.
$parameters = new MenuTreeParameters();
$parameters->setRoot($link_id)->excludeRoot()->setTopLevelOnly()->onlyEnabledLinks();
$tree = $this->menuLinkTree->load(NULL, $parameters);
$manipulators = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
];
$tree = $this->menuLinkTree->transform($tree, $manipulators);
$tree_access_cacheability = new CacheableMetadata();
$blocks = [];
foreach ($tree as $key => $element) {
$tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
// Only render accessible links.
if (!$element->access->isAllowed()) {
continue;
}
$link = $element->link;
$block['title'] = $link->getTitle();
$block['description'] = $link->getDescription();
$block['content'] = [
'#theme' => 'admin_block_content',
'#content' => $this->systemManager->getAdminBlock($link),
];
if (!empty($block['content']['#content'])) {
$blocks[$key] = $block;
}
}
if ($blocks) {
ksort($blocks);
$build = [
'#theme' => 'admin_page',
'#blocks' => $blocks,
];
$tree_access_cacheability->applyTo($build);
return $build;
}
else {
$build = [
'#markup' => $this->t('You do not have any administrative items.'),
];
$tree_access_cacheability->applyTo($build);
return $build;
}
}
/**
* Sets whether the admin menu is in compact mode or not.
*
* @param string $mode
* Valid values are 'on' and 'off'.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function compactPage($mode) {
user_cookie_save(['admin_compact_mode' => ($mode == 'on')]);
return $this->redirect('<front>');
}
/**
* Provides a single block from the administration menu as a page.
*/
public function systemAdminMenuBlockPage() {
return $this->systemManager->getBlockContents();
}
/**
* Returns a theme listing which excludes obsolete themes.
*
* @return string
* An HTML string of the theme listing page.
*
* @todo Move into ThemeController.
*/
public function themesPage() {
$config = $this->config('system.theme');
// Get all available themes.
$themes = $this->themeExtensionList->reset()->getList();
// Remove obsolete themes.
$themes = array_filter($themes, function ($theme) {
return !$theme->isObsolete();
});
uasort($themes, [ThemeExtensionList::class, 'sortByName']);
$theme_default = $config->get('default');
$theme_groups = ['installed' => [], 'uninstalled' => []];
$admin_theme = $config->get('admin');
$admin_theme_options = [];
$incompatible_installed = FALSE;
foreach ($themes as &$theme) {
if (!empty($theme->info['hidden'])) {
continue;
}
if (!$incompatible_installed && $theme->info['core_incompatible'] && $theme->status) {
$incompatible_installed = TRUE;
$this->messenger()->addWarning($this->t(
'There are errors with some installed themes. Visit the <a href=":link">status report page</a> for more information.',
[':link' => Url::fromRoute('system.status')->toString()]
));
}
$theme->is_default = ($theme->getName() == $theme_default);
$theme->is_admin = ($theme->getName() == $admin_theme || ($theme->is_default && empty($admin_theme)));
// Identify theme screenshot.
$theme->screenshot = NULL;
// Create a list which includes the current theme and all its base themes.
if (isset($themes[$theme->getName()]->base_themes)) {
$theme_keys = array_keys($themes[$theme->getName()]->base_themes);
$theme_keys[] = $theme->getName();
}
else {
$theme_keys = [$theme->getName()];
}
// Look for a screenshot in the current theme or in its closest ancestor.
foreach (array_reverse($theme_keys) as $theme_key) {
if (isset($themes[$theme_key]) && file_exists($themes[$theme_key]->info['screenshot'])) {
$theme->screenshot = [
'uri' => $themes[$theme_key]->info['screenshot'],
'alt' => $this->t('Screenshot for @theme theme', ['@theme' => $theme->info['name']]),
'title' => $this->t('Screenshot for @theme theme', ['@theme' => $theme->info['name']]),
'attributes' => ['class' => ['screenshot']],
];
break;
}
}
if (empty($theme->status)) {
// Require the 'content' region to make sure the main page
// content has a common place in all themes.
$theme->incompatible_region = !isset($theme->info['regions']['content']);
$theme->incompatible_php = version_compare(phpversion(), $theme->info['php']) < 0;
// Confirm that all base themes are available.
$theme->incompatible_base = (isset($theme->info['base theme']) && !($theme->base_themes === array_filter($theme->base_themes)));
// Confirm that the theme engine is available.
$theme->incompatible_engine = isset($theme->info['engine']) && !isset($theme->owner);
// Confirm that module dependencies are available.
$theme->incompatible_module = FALSE;
// Confirm that the user has permission to enable modules.
$theme->insufficient_module_permissions = FALSE;
}
// Check module dependencies.
if ($theme->module_dependencies) {
$modules = $this->moduleExtensionList->getList();
foreach ($theme->module_dependencies as $dependency => $dependency_object) {
if ($incompatible = $this->checkDependencyMessage($modules, $dependency, $dependency_object)) {
$theme->module_dependencies_list[$dependency] = $incompatible;
$theme->incompatible_module = TRUE;
continue;
}
// @todo Add logic for not displaying hidden modules in
// https://drupal.org/node/3117829.
$module_name = $modules[$dependency]->info['name'];
$theme->module_dependencies_list[$dependency] = $modules[$dependency]->status ? $this->t('@module_name', ['@module_name' => $module_name]) : $this->t('@module_name (<span class="admin-disabled">disabled</span>)', ['@module_name' => $module_name]);
// Create an additional property that contains only disabled module
// dependencies. This will determine if it is possible to install the
// theme, or if modules must first be enabled.
if (!$modules[$dependency]->status) {
$theme->module_dependencies_disabled[$dependency] = $module_name;
if (!$this->currentUser()->hasPermission('administer modules')) {
$theme->insufficient_module_permissions = TRUE;
}
}
}
}
$theme->operations = [];
if (!empty($theme->status) || !$theme->info['core_incompatible'] && !$theme->incompatible_php && !$theme->incompatible_base && !$theme->incompatible_engine && !$theme->incompatible_module && empty($theme->module_dependencies_disabled)) {
// Create the operations links.
$query['theme'] = $theme->getName();
if ($this->themeAccess->checkAccess($theme->getName())) {
$theme->operations[] = [
'title' => $this->t('Settings'),
'url' => Url::fromRoute('system.theme_settings_theme', ['theme' => $theme->getName()]),
'attributes' => ['title' => $this->t('Settings for @theme theme', ['@theme' => $theme->info['name']])],
];
}
if (!empty($theme->status)) {
if (!$theme->is_default) {
$theme_uninstallable = TRUE;
if ($theme->getName() == $admin_theme) {
$theme_uninstallable = FALSE;
}
// Check it isn't the base of theme of an installed theme.
foreach ($theme->required_by as $themename => $dependency) {
if (!empty($themes[$themename]->status)) {
$theme_uninstallable = FALSE;
}
}
if ($theme_uninstallable) {
$theme->operations[] = [
'title' => $this->t('Uninstall'),
'url' => Url::fromRoute('system.theme_uninstall'),
'query' => $query,
'attributes' => ['title' => $this->t('Uninstall @theme theme', ['@theme' => $theme->info['name']])],
];
}
$theme->operations[] = [
'title' => $this->t('Set as default'),
'url' => Url::fromRoute('system.theme_set_default'),
'query' => $query,
'attributes' => ['title' => $this->t('Set @theme as default theme', ['@theme' => $theme->info['name']])],
];
}
$admin_theme_options[$theme->getName()] = $theme->info['name'] . ($theme->isExperimental() ? ' (' . t('Experimental') . ')' : '');
}
else {
$theme->operations[] = [
'title' => $this->t('Install'),
'url' => Url::fromRoute('system.theme_install'),
'query' => $query,
'attributes' => ['title' => $this->t('Install @theme theme', ['@theme' => $theme->info['name']])],
];
$theme->operations[] = [
'title' => $this->t('Install and set as default'),
'url' => Url::fromRoute('system.theme_set_default'),
'query' => $query,
'attributes' => ['title' => $this->t('Install @theme as default theme', ['@theme' => $theme->info['name']])],
];
}
}
// Add notes to default theme, administration theme and non-stable themes.
$theme->notes = [];
if ($theme->is_default) {
$theme->notes[] = $this->t('default theme');
}
if ($theme->is_admin) {
$theme->notes[] = $this->t('administration theme');
}
$lifecycle = $theme->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER];
if (!empty($theme->info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER])) {
$theme->notes[] = Link::fromTextAndUrl($this->t('@lifecycle', ['@lifecycle' => ucfirst($lifecycle)]),
Url::fromUri($theme->info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER], [
'attributes' =>
[
'class' => 'theme-link--non-stable',
'aria-label' => $this->t('View information on the @lifecycle status of the theme @theme', [
'@lifecycle' => ucfirst($lifecycle),
'@theme' => $theme->info['name'],
]),
],
])
)->toString();
}
if ($theme->isExperimental() && empty($theme->info[ExtensionLifecycle::LIFECYCLE_LINK_IDENTIFIER])) {
$theme->notes[] = $this->t('experimental theme');
}
// Sort installed and uninstalled themes into their own groups.
$theme_groups[$theme->status ? 'installed' : 'uninstalled'][] = $theme;
}
// There are two possible theme groups.
$theme_group_titles = [
'installed' => $this->formatPlural(count($theme_groups['installed']), 'Installed theme', 'Installed themes'),
];
if (!empty($theme_groups['uninstalled'])) {
$theme_group_titles['uninstalled'] = $this->formatPlural(count($theme_groups['uninstalled']), 'Uninstalled theme', 'Uninstalled themes');
}
uasort($theme_groups['installed'], 'system_sort_themes');
$this->moduleHandler()->alter('system_themes_page', $theme_groups);
$build = [];
$build[] = [
'#theme' => 'system_themes_page',
'#theme_groups' => $theme_groups,
'#theme_group_titles' => $theme_group_titles,
];
$build[] = $this->formBuilder->getForm('Drupal\system\Form\ThemeAdminForm', $admin_theme_options);
return $build;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Site\Settings;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\system\SystemManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Returns responses for System Info routes.
*/
class SystemInfoController implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* System Manager Service.
*
* @var \Drupal\system\SystemManager
*/
protected $systemManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('system.manager')
);
}
/**
* Constructs a SystemInfoController object.
*
* @param \Drupal\system\SystemManager $systemManager
* System manager service.
*/
public function __construct(SystemManager $systemManager) {
$this->systemManager = $systemManager;
}
/**
* Displays the site status report.
*
* @return array
* A render array containing a list of system requirements for the Drupal
* installation and whether this installation meets the requirements.
*/
public function status() {
$requirements = $this->systemManager->listRequirements();
return ['#type' => 'status_report_page', '#requirements' => $requirements];
}
/**
* Returns the contents of phpinfo().
*
* @return \Symfony\Component\HttpFoundation\Response
* A response object to be sent to the client.
*/
public function php() {
if (function_exists('phpinfo')) {
ob_start();
$phpinfo_flags = Settings::get('sa_core_2023_004_phpinfo_flags', ~ (INFO_VARIABLES | INFO_ENVIRONMENT));
phpinfo($phpinfo_flags);
$output = ob_get_clean();
}
else {
$output = $this->t('The phpinfo() function is disabled. For more information, visit the <a href=":phpinfo">Enabling and disabling phpinfo()</a> handbook page.', [':phpinfo' => 'https://www.drupal.org/node/243993']);
}
return new Response($output);
}
}

View File

@@ -0,0 +1,243 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\MissingDependencyException;
use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Extension\ThemeInstallerInterface;
use Drupal\system\Form\ThemeExperimentalConfirmForm;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Controller for theme handling.
*/
class ThemeController extends ControllerBase {
/**
* The theme handler service.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* An extension discovery instance.
*
* @var \Drupal\Core\Extension\ThemeExtensionList
*/
protected $themeList;
/**
* The theme installer service.
*
* @var \Drupal\Core\Extension\ThemeInstallerInterface
*/
protected $themeInstaller;
/**
* Constructs a new ThemeController.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Extension\ThemeExtensionList $theme_list
* The theme extension list.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer
* The theme installer.
*/
public function __construct(ThemeHandlerInterface $theme_handler, ThemeExtensionList $theme_list, ConfigFactoryInterface $config_factory, ThemeInstallerInterface $theme_installer) {
$this->themeHandler = $theme_handler;
$this->themeList = $theme_list;
$this->configFactory = $config_factory;
$this->themeInstaller = $theme_installer;
}
/**
* Uninstalls a theme.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object containing a theme name and a valid token.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Redirects back to the appearance admin page.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Throws access denied when no theme or token is set in the request or when
* the token is invalid.
*/
public function uninstall(Request $request) {
$theme = $request->query->get('theme');
$config = $this->config('system.theme');
if (isset($theme)) {
// Get current list of themes.
$themes = $this->themeHandler->listInfo();
// Check if the specified theme is one recognized by the system.
if (!empty($themes[$theme])) {
// Do not uninstall the default or admin theme.
if ($theme === $config->get('default') || $theme === $config->get('admin')) {
$this->messenger()->addError($this->t('%theme is the default theme and cannot be uninstalled.', ['%theme' => $themes[$theme]->info['name']]));
}
else {
$this->themeInstaller->uninstall([$theme]);
$this->messenger()->addStatus($this->t('The %theme theme has been uninstalled.', ['%theme' => $themes[$theme]->info['name']]));
}
}
else {
$this->messenger()->addError($this->t('The %theme theme was not found.', ['%theme' => $theme]));
}
return $this->redirect('system.themes_page');
}
throw new AccessDeniedHttpException();
}
/**
* Installs a theme.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object containing a theme name and a valid token.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|array
* Redirects back to the appearance admin page or the confirmation form
* if an experimental theme will be installed.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Throws access denied when no theme or token is set in the request or when
* the token is invalid.
*/
public function install(Request $request) {
$theme = $request->query->get('theme');
if (isset($theme)) {
// Display confirmation form in case of experimental theme.
if ($this->willInstallExperimentalTheme($theme)) {
return $this->formBuilder()->getForm(ThemeExperimentalConfirmForm::class, $theme);
}
try {
if ($this->themeInstaller->install([$theme])) {
$themes = $this->themeHandler->listInfo();
$this->messenger()->addStatus($this->t('The %theme theme has been installed.', ['%theme' => $themes[$theme]->info['name']]));
}
else {
$this->messenger()->addError($this->t('The %theme theme was not found.', ['%theme' => $theme]));
}
}
catch (PreExistingConfigException $e) {
$config_objects = $e->flattenConfigObjects($e->getConfigObjects());
$this->messenger()->addError(
$this->formatPlural(
count($config_objects),
'Unable to install @extension, %config_names already exists in active configuration.',
'Unable to install @extension, %config_names already exist in active configuration.',
[
'%config_names' => implode(', ', $config_objects),
'@extension' => $theme,
])
);
}
catch (UnmetDependenciesException $e) {
$this->messenger()->addError($e->getTranslatedMessage($this->getStringTranslation(), $theme));
}
catch (MissingDependencyException $e) {
$this->messenger()->addError($this->t('Unable to install @theme due to missing module dependencies.', ['@theme' => $theme]));
}
return $this->redirect('system.themes_page');
}
throw new AccessDeniedHttpException();
}
/**
* Checks if the given theme requires the installation of experimental themes.
*
* @param string $theme
* The name of the theme to check.
*
* @return bool
* Whether experimental themes will be installed.
*/
protected function willInstallExperimentalTheme($theme) {
$all_themes = $this->themeList->getList();
$dependencies = array_keys($all_themes[$theme]->requires);
$themes_to_enable = array_merge([$theme], $dependencies);
foreach ($themes_to_enable as $name) {
if (isset($all_themes[$name]) && $all_themes[$name]->isExperimental() && $all_themes[$name]->status === 0) {
return TRUE;
}
}
return FALSE;
}
/**
* Set the default theme.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object containing a theme name.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|array
* Redirects back to the appearance admin page or the confirmation form
* if an experimental theme will be installed.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Throws access denied when no theme is set in the request.
*/
public function setDefaultTheme(Request $request) {
$config = $this->configFactory->getEditable('system.theme');
$theme = $request->query->get('theme');
if (isset($theme)) {
// Get current list of themes.
$themes = $this->themeHandler->listInfo();
// Display confirmation form if an experimental theme is being installed.
if ($this->willInstallExperimentalTheme($theme)) {
return $this->formBuilder()->getForm(ThemeExperimentalConfirmForm::class, $theme, TRUE);
}
// Check if the specified theme is one recognized by the system.
// Or try to install the theme.
if (isset($themes[$theme]) || $this->themeInstaller->install([$theme])) {
$themes = $this->themeHandler->listInfo();
// Set the default theme.
$config->set('default', $theme)->save();
// The status message depends on whether an admin theme is currently in
// use: a value of 0 means the admin theme is set to be the default
// theme.
$admin_theme = $config->get('admin');
if (!empty($admin_theme) && $admin_theme != $theme) {
$this->messenger()
->addStatus($this->t('Note that the administration theme is still set to the %admin_theme theme; consequently, the theme on this page remains unchanged. All non-administrative sections of the site, however, will show the selected %selected_theme theme by default.', [
'%admin_theme' => $themes[$admin_theme]->info['name'],
'%selected_theme' => $themes[$theme]->info['name'],
]));
}
else {
$this->messenger()->addStatus($this->t('%theme is now the default theme.', ['%theme' => $themes[$theme]->info['name']]));
}
}
else {
$this->messenger()->addError($this->t('The %theme theme was not found.', ['%theme' => $theme]));
}
return $this->redirect('system.themes_page');
}
throw new AccessDeniedHttpException();
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\system\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Provides a callback for finding a time zone identifier.
*/
class TimezoneController {
/**
* Returns a time zone identifier given a time zone abbreviation.
*
* @param string $abbreviation
* Time zone abbreviation.
* @param int $offset
* Offset from GMT in seconds. Defaults to -1 which means that first found
* time zone corresponding to abbreviation is returned. Otherwise exact
* offset is searched and only if not found then the first time zone with
* any offset is returned.
* @param null|int $is_daylight_saving_time
* Daylight saving time indicator. If abbreviation does not exist then the
* time zone is searched solely by offset and is DST.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The time zone identifier or 'false' in JsonResponse object.
*/
public function getTimezone($abbreviation = '', $offset = -1, $is_daylight_saving_time = NULL) {
$offset = intval($offset);
// Out of bounds check for offset. Offset +/- UTC is typically no
// smaller/larger than -12/+14.
if ($offset < -60000 || $offset > 60000) {
return new JsonResponse(FALSE);
}
if (isset($is_daylight_saving_time)) {
$original = intval($is_daylight_saving_time);
$is_daylight_saving_time = min(1, max(-1, intval($is_daylight_saving_time)));
// Catch if out of boundary.
if ($original !== $is_daylight_saving_time) {
return new JsonResponse(FALSE);
}
}
// An abbreviation of "0" passed in the callback arguments should be
// interpreted as the empty string.
$abbreviation = $abbreviation ? $abbreviation : '';
$timezone = timezone_name_from_abbr($abbreviation, $offset, $is_daylight_saving_time);
return new JsonResponse($timezone);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,79 @@
<?php
namespace Drupal\system;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of date format entities.
*
* @see \Drupal\system\Entity\DateFormat
*/
class DateFormatListBuilder extends ConfigEntityListBuilder {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* Constructs a new DateFormatListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter, protected ?TimeInterface $time = NULL) {
parent::__construct($entity_type, $storage);
$this->dateFormatter = $date_formatter;
if ($this->time === NULL) {
@trigger_error('Calling ' . __METHOD__ . ' without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3112298', E_USER_DEPRECATED);
$this->time = \Drupal::service('datetime.time');
}
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('date.formatter'),
$container->get('datetime.time'),
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = t('Name');
$header['pattern'] = t('Pattern');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
$row['pattern'] = $this->dateFormatter->format($this->time->getRequestTime(), $entity->id());
return $row + parent::buildRow($entity);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Drupal\system\Element;
use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\Render\Element\StatusReport;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
/**
* Creates status report page element.
*/
#[RenderElement('status_report_page')]
class StatusReportPage extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = static::class;
return [
'#theme' => 'status_report_page',
'#pre_render' => [
[$class, 'preRenderCounters'],
[$class, 'preRenderGeneralInfo'],
[$class, 'preRenderRequirements'],
],
];
}
/**
* #pre_render callback to get general info out of requirements.
*/
public static function preRenderGeneralInfo($element) {
$element['#general_info'] = [
'#theme' => 'status_report_general_info',
];
// Loop through requirements and pull out items.
foreach ($element['#requirements'] as $key => $requirement) {
switch ($key) {
case 'cron':
foreach ($requirement['description'] as &$description_elements) {
foreach ($description_elements as &$description_element) {
if (isset($description_element['#url']) && $description_element['#url']->getRouteName() == 'system.run_cron') {
$description_element['#attributes']['class'][] = 'button';
$description_element['#attributes']['class'][] = 'button--small';
$description_element['#attributes']['class'][] = 'button--primary';
$description_element['#attributes']['class'][] = 'system-status-general-info__run-cron';
}
}
}
// Intentional fall-through.
case 'drupal':
case 'webserver':
case 'database_system':
case 'database_system_version':
case 'php':
case 'php_memory_limit':
$element['#general_info']['#' . $key] = $requirement;
if (isset($requirement['severity']) && $requirement['severity'] < REQUIREMENT_WARNING) {
if (empty($requirement['severity']) || $requirement['severity'] == REQUIREMENT_OK) {
unset($element['#requirements'][$key]);
}
}
break;
}
}
return $element;
}
/**
* #pre_render callback to create counter elements.
*/
public static function preRenderCounters($element) {
// Count number of items with different severity for summary.
$counters = [
'error' => [
'amount' => 0,
'text' => t('Error'),
'text_plural' => t('Errors'),
],
'warning' => [
'amount' => 0,
'text' => t('Warning'),
'text_plural' => t('Warnings'),
],
'checked' => [
'amount' => 0,
'text' => t('Checked', [], ['context' => 'Examined']),
'text_plural' => t('Checked', [], ['context' => 'Examined']),
],
];
$severities = StatusReport::getSeverities();
foreach ($element['#requirements'] as $key => &$requirement) {
$severity = $severities[REQUIREMENT_INFO];
if (isset($requirement['severity'])) {
$severity = $severities[(int) $requirement['severity']];
}
elseif (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'install') {
$severity = $severities[REQUIREMENT_OK];
}
if (isset($counters[$severity['status']])) {
$counters[$severity['status']]['amount']++;
}
}
foreach ($counters as $key => $counter) {
if ($counter['amount'] === 0) {
continue;
}
$text = new PluralTranslatableMarkup($counter['amount'], $counter['text'], $counter['text_plural']);
$element['#counters'][$key] = [
'#theme' => 'status_report_counter',
'#amount' => $counter['amount'],
'#text' => $text,
'#severity' => $key,
];
}
return $element;
}
/**
* #pre_render callback to create status report requirements.
*/
public static function preRenderRequirements($element) {
$element['#requirements'] = [
'#type' => 'status_report',
'#requirements' => $element['#requirements'],
];
return $element;
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace Drupal\system\Entity;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\system\ActionConfigEntityInterface;
use Drupal\Core\Action\ActionPluginCollection;
/**
* Defines the configured action entity.
*
* @ConfigEntityType(
* id = "action",
* label = @Translation("Action"),
* label_collection = @Translation("Actions"),
* label_singular = @Translation("action"),
* label_plural = @Translation("actions"),
* label_count = @PluralTranslation(
* singular = "@count action",
* plural = "@count actions",
* ),
* admin_permission = "administer actions",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "type",
* "plugin",
* "configuration",
* }
* )
*/
class Action extends ConfigEntityBase implements ActionConfigEntityInterface, EntityWithPluginCollectionInterface {
/**
* The name (plugin ID) of the action.
*
* @var string
*/
protected $id;
/**
* The label of the action.
*
* @var string
*/
protected $label;
/**
* The action type.
*
* @var string
*/
protected $type;
/**
* The configuration of the action.
*
* @var array
*/
protected $configuration = [];
/**
* The plugin ID of the action.
*
* @var string
*/
protected $plugin;
/**
* The plugin collection that stores action plugins.
*
* @var \Drupal\Core\Action\ActionPluginCollection
*/
protected $pluginCollection;
/**
* {@inheritdoc}
*/
public static function create(array $values = []) {
// When no label is specified for this action config entity, default to the
// label of the used action plugin.
if (!array_key_exists('label', $values) && array_key_exists('plugin', $values)) {
try {
$action_plugin_manager = \Drupal::service('plugin.manager.action');
assert($action_plugin_manager instanceof PluginManagerInterface);
$action_plugin_definition = $action_plugin_manager->getDefinition($values['plugin']);
// @see \Drupal\Core\Annotation\Action::$label
assert(array_key_exists('label', $action_plugin_definition));
$values['label'] = $action_plugin_definition['label'];
}
catch (PluginNotFoundException) {
}
}
return parent::create($values);
}
/**
* Encapsulates the creation of the action's LazyPluginCollection.
*
* @return \Drupal\Component\Plugin\LazyPluginCollection
* The action's plugin collection.
*/
protected function getPluginCollection() {
if (!$this->pluginCollection) {
$this->pluginCollection = new ActionPluginCollection(\Drupal::service('plugin.manager.action'), $this->plugin, $this->configuration);
}
return $this->pluginCollection;
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
return ['configuration' => $this->getPluginCollection()];
}
/**
* {@inheritdoc}
*/
public function getPlugin() {
return $this->getPluginCollection()->get($this->plugin);
}
/**
* {@inheritdoc}
*/
public function setPlugin($plugin_id) {
$this->plugin = $plugin_id;
$this->getPluginCollection()->addInstanceId($plugin_id);
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition() {
return $this->getPlugin()->getPluginDefinition();
}
/**
* {@inheritdoc}
*/
public function execute(array $entities) {
return $this->getPlugin()->executeMultiple($entities);
}
/**
* {@inheritdoc}
*/
public function isConfigurable() {
return $this->getPlugin() instanceof ConfigurableInterface;
}
/**
* {@inheritdoc}
*/
public function getType() {
return $this->type;
}
/**
* {@inheritdoc}
*/
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
/** @var \Drupal\system\ActionConfigEntityInterface $a */
/** @var \Drupal\system\ActionConfigEntityInterface $b */
$a_type = $a->getType();
$b_type = $b->getType();
if ($a_type != $b_type) {
return strnatcasecmp($a_type, $b_type);
}
return parent::sort($a, $b);
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Drupal\system\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\system\MenuInterface;
/**
* Defines the Menu configuration entity class.
*
* @ConfigEntityType(
* id = "menu",
* label = @Translation("Menu"),
* label_collection = @Translation("Menus"),
* label_singular = @Translation("menu"),
* label_plural = @Translation("menus"),
* label_count = @PluralTranslation(
* singular = "@count menu",
* plural = "@count menus",
* ),
* handlers = {
* "access" = "Drupal\system\MenuAccessControlHandler",
* "storage" = "Drupal\system\MenuStorage",
* },
* admin_permission = "administer menu",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "description",
* "locked",
* }
* )
*/
class Menu extends ConfigEntityBase implements MenuInterface {
/**
* The menu machine name.
*
* @var string
*/
protected $id;
/**
* The human-readable name of the menu entity.
*
* @var string
*/
protected $label;
/**
* The menu description.
*
* @var string
*/
protected $description;
/**
* The locked status of this menu.
*
* @var bool
*/
protected $locked = FALSE;
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function isLocked() {
return (bool) $this->locked;
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
foreach ($entities as $menu) {
// Delete all links from the menu.
$menu_link_manager->deleteLinksInMenu($menu->id());
}
}
/**
* {@inheritdoc}
*/
public function save() {
$return = parent::save();
\Drupal::cache('menu')->invalidateAll();
// Invalidate the block cache to update menu-based derivatives.
if (\Drupal::moduleHandler()->moduleExists('block')) {
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
return $return;
}
/**
* {@inheritdoc}
*/
public function delete() {
parent::delete();
\Drupal::cache('menu')->invalidateAll();
// Invalidate the block cache to update menu-based derivatives.
if (\Drupal::moduleHandler()->moduleExists('block')) {
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\system\EventSubscriber;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Alters routes to add necessary requirements.
*
* @see \Drupal\system\Access\SystemAdminMenuBlockAccessCheck
* @see \Drupal\system\Controller\SystemController::systemAdminMenuBlockPage()
*/
class AccessRouteAlterSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[RoutingEvents::ALTER][] = 'accessAdminMenuBlockPage';
return $events;
}
/**
* Adds _access_admin_menu_block_page requirement to routes pointing to SystemController::systemAdminMenuBlockPage.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The event to process.
*/
public function accessAdminMenuBlockPage(RouteBuildEvent $event) {
$routes = $event->getRouteCollection();
foreach ($routes as $route) {
// Do not use a leading slash when comparing to the _controller string
// because the leading slash in a fully-qualified method name is optional.
if ($route->hasDefault('_controller')) {
switch (ltrim($route->getDefault('_controller'), '\\')) {
case 'Drupal\system\Controller\SystemController::systemAdminMenuBlockPage':
$route->setRequirement('_access_admin_menu_block_page', 'TRUE');
break;
case 'Drupal\system\Controller\SystemController::overview':
$route->setRequirement('_access_admin_overview_page', 'TRUE');
break;
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Drupal\system\EventSubscriber;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Adds the _admin_route option to each admin HTML route.
*/
class AdminRouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($collection->all() as $route) {
$path = $route->getPath();
if (($path == '/admin' || str_starts_with($path, '/admin/')) && !$route->hasOption('_admin_route') && static::isHtmlRoute($route)) {
$route->setOption('_admin_route', TRUE);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = parent::getSubscribedEvents();
// Use a lower priority than \Drupal\field_ui\Routing\RouteSubscriber or
// \Drupal\views\EventSubscriber\RouteSubscriber to ensure we add the option
// to their routes.
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -200];
return $events;
}
/**
* Determines whether the given route is an HTML route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to analyze.
*
* @return bool
* TRUE if HTML is a valid format for this route.
*/
protected static function isHtmlRoute(Route $route) {
// If a route has no explicit format, then HTML is valid.
$format = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : ['html'];
return in_array('html', $format, TRUE);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Drupal\system\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Defines a config subscriber for changes to 'system.advisories'.
*/
class AdvisoriesConfigSubscriber implements EventSubscriberInterface {
/**
* The security advisory fetcher service.
*
* @var \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher
*/
protected $securityAdvisoriesFetcher;
/**
* Constructs a new ConfigSubscriber object.
*
* @param \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher $security_advisories_fetcher
* The security advisory fetcher service.
*/
public function __construct(SecurityAdvisoriesFetcher $security_advisories_fetcher) {
$this->securityAdvisoriesFetcher = $security_advisories_fetcher;
}
/**
* Deletes the stored response from the security advisories feed, if needed.
*
* The stored response will only be deleted if the 'interval_hours' config
* setting is reduced from the previous value.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigSave(ConfigCrudEvent $event): void {
$saved_config = $event->getConfig();
if ($saved_config->getName() === 'system.advisories' && $event->isChanged('interval_hours')) {
$original_interval = $saved_config->getOriginal('interval_hours');
if ($original_interval && $saved_config->get('interval_hours') < $original_interval) {
// If the new interval is less than the original interval, delete the
// stored results.
$this->securityAdvisoriesFetcher->deleteStoredResponse();
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[ConfigEvents::SAVE][] = ['onConfigSave'];
return $events;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Drupal\system\EventSubscriber;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Theme\Registry;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A subscriber invalidating cache tags when system config objects are saved.
*/
class ConfigCacheTag implements EventSubscriberInterface {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The cache tags invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagsInvalidator;
/**
* Constructs a ConfigCacheTag object.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* The cache tags invalidator.
* @param \Drupal\Core\Theme\Registry|null $themeRegistry
* The theme registry.
*/
public function __construct(ThemeHandlerInterface $theme_handler, CacheTagsInvalidatorInterface $cache_tags_invalidator, protected ?Registry $themeRegistry = NULL) {
$this->themeHandler = $theme_handler;
$this->cacheTagsInvalidator = $cache_tags_invalidator;
if ($this->themeRegistry === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $themeRegistry argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3355227', E_USER_DEPRECATED);
$this->themeRegistry = \Drupal::service('theme.registry');
}
}
/**
* Invalidate cache tags when particular system config objects are saved.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The Event to process.
*/
public function onSave(ConfigCrudEvent $event) {
$config_name = $event->getConfig()->getName();
// Changing the site settings may mean a different route is selected for the
// front page. Additionally a change to the site name or similar must
// invalidate the render cache since this could be used anywhere.
if ($config_name === 'system.site') {
$this->cacheTagsInvalidator->invalidateTags(['route_match', 'rendered']);
}
// Theme configuration and global theme settings.
if (in_array($config_name, ['system.theme', 'system.theme.global'], TRUE)) {
$this->cacheTagsInvalidator->invalidateTags(['rendered']);
}
// Library and template overrides potentially change for the default theme
// when the admin theme is changed.
if ($config_name === 'system.theme' && $event->isChanged('admin')) {
$this->themeRegistry->reset();
$this->cacheTagsInvalidator->invalidateTags(['library_info']);
}
// Theme-specific settings, check if this matches a theme settings
// configuration object (THEME_NAME.settings), in that case, clear the
// rendered cache tag.
if (preg_match('/^([^\.]*)\.settings$/', $config_name, $matches)) {
if ($this->themeHandler->themeExists($matches[1])) {
$this->cacheTagsInvalidator->invalidateTags(['rendered']);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[ConfigEvents::SAVE][] = ['onSave'];
return $events;
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Drupal\system\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\Event\FileUploadSanitizeNameEvent;
use Drupal\Core\File\FileSystemInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* The final subscriber to 'file.upload.sanitize.name'.
*
* This prevents insecure filenames.
*/
class SecurityFileUploadEventSubscriber implements EventSubscriberInterface {
/**
* Constructs a new file event listener.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
*/
public function __construct(
protected ConfigFactoryInterface $configFactory,
) {}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// This event must be run last to ensure the filename obeys the security
// rules.
$events[FileUploadSanitizeNameEvent::class][] = [
'sanitizeName',
PHP_INT_MIN,
];
return $events;
}
/**
* Sanitizes the upload's filename to make it secure.
*
* @param \Drupal\Core\File\Event\FileUploadSanitizeNameEvent $event
* File upload sanitize name event.
*/
public function sanitizeName(FileUploadSanitizeNameEvent $event): void {
$filename = $event->getFilename();
// Dot files are renamed regardless of security settings.
$filename = trim($filename, '.');
// Remove any null bytes. See
// http://php.net/manual/security.filesystem.nullbytes.php
$filename = str_replace(chr(0), '', $filename);
// Split up the filename by periods. The first part becomes the basename,
// the last part the final extension.
$filename_parts = explode('.', $filename);
// Remove file basename.
$filename = array_shift($filename_parts);
// Remove final extension.
$final_extension = (string) array_pop($filename_parts);
// Check if we're dealing with a dot file that is also an insecure extension
// e.g. .htaccess. In this scenario there is only one 'part' and the
// extension becomes the filename. We use the original filename from the
// event rather than the trimmed version above.
$insecure_uploads = $this->configFactory->get('system.file')->get('allow_insecure_uploads');
if (!$insecure_uploads && $final_extension === '' && str_contains($event->getFilename(), '.') && in_array(strtolower($filename), FileSystemInterface::INSECURE_EXTENSIONS, TRUE)) {
$final_extension = $filename;
$filename = '';
}
$extensions = $event->getAllowedExtensions();
if (!empty($extensions) && !in_array(strtolower($final_extension), $extensions, TRUE)) {
// This upload will be rejected by FileExtension constraint anyway so do
// not make any alterations to the filename. This prevents a file named
// 'example.php' being renamed to 'example.php_.txt' and uploaded if the
// .txt extension is allowed but .php is not. It is the responsibility of
// the function that dispatched the event to ensure
// FileValidator::validate() is called with 'FileExtension' in the list of
// validators if $extensions is not empty.
return;
}
if (!$insecure_uploads && in_array(strtolower($final_extension), FileSystemInterface::INSECURE_EXTENSIONS, TRUE)) {
if (empty($extensions) || in_array('txt', $extensions, TRUE)) {
// Add .txt to potentially executable files prior to munging to help prevent
// exploits. This results in a filenames like filename.php being changed to
// filename.php.txt prior to munging.
$filename_parts[] = $final_extension;
$final_extension = 'txt';
}
else {
// Since .txt is not an allowed extension do not rename the file. The
// file will be rejected by FileValidator::validate().
return;
}
}
// If there are any insecure extensions in the filename munge all the
// internal extensions.
$munge_everything = !empty(array_intersect(array_map('strtolower', $filename_parts), FileSystemInterface::INSECURE_EXTENSIONS));
// Munge the filename to protect against possible malicious extension hiding
// within an unknown file type (i.e. filename.html.foo). This was introduced
// as part of SA-2006-006 to fix Apache's risky fallback behavior.
// Loop through the middle parts of the name and add an underscore to the
// end of each section that could be a file extension but isn't in the
// list of allowed extensions.
foreach ($filename_parts as $filename_part) {
$filename .= '.' . $filename_part;
if ($munge_everything) {
$filename .= '_';
}
elseif (!empty($extensions) && !in_array(strtolower($filename_part), $extensions) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
$filename .= '_';
}
}
if ($final_extension !== '') {
$filename .= '.' . $final_extension;
}
if ($filename !== $event->getFilename()) {
$event->setFilename($filename)->setSecurityRename();
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Drupal\system;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* System file controller.
*/
class FileDownloadController extends ControllerBase {
/**
* The stream wrapper manager.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* FileDownloadController constructor.
*
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
* The stream wrapper manager.
*/
public function __construct(StreamWrapperManagerInterface $streamWrapperManager) {
$this->streamWrapperManager = $streamWrapperManager;
}
/**
* Handles private file transfers.
*
* Call modules that implement hook_file_download() to find out if a file is
* accessible and what headers it should be transferred with. If one or more
* modules returned headers the download will start with the returned headers.
* If a module returns -1 an AccessDeniedHttpException will be thrown. If the
* file exists but no modules responded an AccessDeniedHttpException will be
* thrown. If the file does not exist a NotFoundHttpException will be thrown.
*
* @see hook_file_download()
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param string $scheme
* The file scheme, defaults to 'private'.
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
* The transferred file as response.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown when the requested file does not exist.
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Thrown when the user does not have access to the file.
*/
public function download(Request $request, $scheme = 'private') {
$target = $request->query->get('file');
// Merge remaining path arguments into relative file path.
$uri = $this->streamWrapperManager->normalizeUri($scheme . '://' . $target);
if ($this->streamWrapperManager->isValidScheme($scheme) && is_file($uri)) {
// Let other modules provide headers and controls access to the file.
$headers = $this->moduleHandler()->invokeAll('file_download', [$uri]);
foreach ($headers as $result) {
if ($result == -1) {
throw new AccessDeniedHttpException();
}
}
if (count($headers)) {
// \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond()
// sets response as not cacheable if the Cache-Control header is not
// already modified. We pass in FALSE for non-private schemes for the
// $public parameter to make sure we don't change the headers.
return new BinaryFileResponse($uri, 200, $headers, $scheme !== 'private');
}
throw new AccessDeniedHttpException();
}
throw new NotFoundHttpException();
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\system\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Clear caches for this site.
*
* @internal
*/
class ClearCacheForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_clear_cache';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['clear_cache'] = [
'#type' => 'details',
'#title' => $this->t('Clear cache'),
'#open' => TRUE,
];
$form['clear_cache']['clear'] = [
'#type' => 'submit',
'#value' => $this->t('Clear all caches'),
];
return $form;
}
/**
* Clears the caches.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
drupal_flush_all_caches();
$this->messenger()->addStatus($this->t('Caches cleared.'));
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\CronInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Form\ConfigFormBaseTrait;
/**
* Configure cron settings for this site.
*
* @internal
*/
class CronForm extends FormBase {
use ConfigFormBaseTrait;
/**
* Stores the state storage service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The cron service.
*
* @var \Drupal\Core\CronInterface
*/
protected $cron;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a CronForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\State\StateInterface $state
* The state key value store.
* @param \Drupal\Core\CronInterface $cron
* The cron service.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
*/
public function __construct(ConfigFactoryInterface $config_factory, StateInterface $state, CronInterface $cron, DateFormatterInterface $date_formatter, ModuleHandlerInterface $module_handler) {
$this->configFactory = $config_factory;
$this->state = $state;
$this->cron = $cron;
$this->dateFormatter = $date_formatter;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.cron'];
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('state'),
$container->get('cron'),
$container->get('date.formatter'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'system_cron_settings';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['description'] = [
'#markup' => '<p>' . $this->t('Cron takes care of running periodic tasks like checking for updates and indexing content for search.') . '</p>',
];
$form['run'] = [
'#type' => 'submit',
'#value' => $this->t('Run cron'),
'#submit' => ['::runCron'],
];
$status = '<p>' . $this->t('Last run: %time ago.', ['%time' => $this->dateFormatter->formatTimeDiffSince($this->state->get('system.cron_last'))]) . '</p>';
$form['status'] = [
'#markup' => $status,
];
$cron_url = Url::fromRoute('system.cron', ['key' => $this->state->get('system.cron_key')], ['absolute' => TRUE])->toString();
$form['cron_url'] = [
'#markup' => '<p>' . $this->t('To run cron from outside the site, go to <a href=":cron" class="system-cron-settings__link">@cron</a>', [':cron' => $cron_url, '@cron' => $cron_url]) . '</p>',
];
if (!$this->moduleHandler->moduleExists('automated_cron')) {
$form['automated_cron'] = [
'#markup' => $this->t('Install the <em>Automated Cron</em> module to allow cron execution at the end of a server response.'),
];
}
$form['cron'] = [
'#title' => $this->t('Cron settings'),
'#type' => 'details',
'#open' => TRUE,
];
$form['cron']['logging'] = [
'#type' => 'checkbox',
'#title' => $this->t('Detailed cron logging'),
'#default_value' => $this->config('system.cron')->get('logging'),
'#description' => $this->t('Run times of individual cron jobs will be written to watchdog'),
];
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save configuration'),
'#button_type' => 'primary',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.cron')
->set('logging', $form_state->getValue('logging'))
->save();
$this->messenger()->addStatus($this->t('The configuration options have been saved.'));
}
/**
* Form submission handler for running cron manually.
*/
public function runCron(array &$form, FormStateInterface $form_state) {
if ($this->cron->run()) {
$this->messenger()->addStatus($this->t('Cron ran successfully.'));
}
else {
$this->messenger()->addError($this->t('Cron run failed.'));
}
}
}

View File

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

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\system\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityDeleteForm;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Builds a form to delete a date format.
*
* @internal
*/
class DateFormatDeleteForm extends EntityDeleteForm {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* Constructs a DateFormatDeleteForm object.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(DateFormatterInterface $date_formatter, protected ?TimeInterface $time = NULL) {
$this->dateFormatter = $date_formatter;
if ($this->time === NULL) {
@trigger_error('Calling ' . __METHOD__ . ' without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3112298', E_USER_DEPRECATED);
$this->time = \Drupal::service('datetime.time');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter'),
$container->get('datetime.time'),
);
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete the format %name : %format?', [
'%name' => $this->entity->label(),
'%format' => $this->dateFormatter->format($this->time->getRequestTime(), $this->entity->id()),
]);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Drupal\system\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form for editing a date format.
*
* @internal
*/
class DateFormatEditForm extends DateFormatFormBase {
/**
* Constructs a DateFormatEditForm object.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date service.
* @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $date_format_storage
* The date format storage.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(DateFormatterInterface $date_formatter, ConfigEntityStorageInterface $date_format_storage, protected ?TimeInterface $time = NULL) {
parent::__construct($date_formatter, $date_format_storage);
if ($this->time === NULL) {
@trigger_error('Calling ' . __METHOD__ . ' without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3112298', E_USER_DEPRECATED);
$this->time = \Drupal::service('datetime.time');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter'),
$container->get('entity_type.manager')->getStorage('date_format'),
$container->get('datetime.time'),
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$now = t('Displayed as %date', ['%date' => $this->dateFormatter->format($this->time->getRequestTime(), $this->entity->id())]);
$form['date_format_pattern']['#field_suffix'] = ' <small data-drupal-date-formatter="preview">' . $now . '</small>';
$form['date_format_pattern']['#default_value'] = $this->entity->getPattern();
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save format');
unset($actions['delete']);
return $actions;
}
}

View File

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

View File

@@ -0,0 +1,155 @@
<?php
namespace Drupal\system\Form;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure development settings for this site.
*
* @internal
*/
class DevelopmentSettingsForm extends FormBase {
/**
* Constructs a new development settings form.
*
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $keyValueFactory
* The key value factory.
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The Drupal kernel.
*/
public function __construct(
protected KeyValueFactoryInterface $keyValueFactory,
protected DrupalKernelInterface $kernel,
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = new static(
$container->get('keyvalue'),
$container->get('kernel')
);
$instance->setMessenger($container->get('messenger'));
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'development_settings_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$development_settings = $this->keyValueFactory->get('development_settings');
$form['description'] = [
'#plain_text' => $this->t('These settings should only be enabled on development environments and never on production.'),
];
$twig_debug = $development_settings->get('twig_debug', FALSE);
$twig_cache_disable = $development_settings->get('twig_cache_disable', FALSE);
$twig_development_state_conditions = [
'input[data-drupal-selector="edit-twig-development-mode"]' => [
'checked' => TRUE,
],
];
$form['disable_rendered_output_cache_bins'] = [
'#type' => 'checkbox',
'#title' => $this->t('Do not cache markup'),
'#description' => $this->t('Disables render cache, dynamic page cache, and page cache.'),
'#default_value' => $development_settings->get('disable_rendered_output_cache_bins', FALSE),
];
$form['twig_development_mode'] = [
'#type' => 'checkbox',
'#title' => $this->t('Twig development mode'),
'#description' => $this->t('Exposes Twig development settings.'),
'#default_value' => $twig_debug || $twig_cache_disable,
];
$form['twig_development'] = [
'#type' => 'fieldset',
'#title' => $this->t('Twig development mode'),
'#states' => [
'visible' => $twig_development_state_conditions,
],
];
$form['twig_development']['twig_debug'] = [
'#type' => 'checkbox',
'#title' => $this->t('Twig debug mode'),
'#description' => $this->t("Provides Twig's <code>dump()</code> function for debugging, outputs template suggestions to HTML comments, and automatically recompile Twig templates after changes."),
'#default_value' => $twig_debug,
];
$form['twig_development']['twig_cache_disable'] = [
'#type' => 'checkbox',
'#title' => $this->t('Disable Twig cache'),
'#description' => $this->t('Twig templates are not cached and are always compiled when rendered.'),
'#default_value' => $twig_cache_disable,
];
if (!$twig_debug && !$twig_cache_disable) {
$form['twig_development']['twig_debug']['#states'] = [
'checked' => $twig_development_state_conditions,
];
$form['twig_development']['twig_cache_disable']['#states'] = [
'checked' => $twig_development_state_conditions,
];
}
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save settings'),
'#button_type' => 'primary',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$development_settings = $this->keyValueFactory->get('development_settings');
$disable_rendered_output_cache_bins_previous = $development_settings->get('disable_rendered_output_cache_bins', FALSE);
$disable_rendered_output_cache_bins = (bool) $form_state->getValue('disable_rendered_output_cache_bins');
if ($disable_rendered_output_cache_bins) {
$development_settings->set('disable_rendered_output_cache_bins', TRUE);
}
else {
$development_settings->delete('disable_rendered_output_cache_bins');
}
$twig_development_mode = (bool) $form_state->getValue('twig_development_mode');
$twig_development_previous = $development_settings->getMultiple(['twig_debug', 'twig_cache_disable']);
$twig_development = [
'twig_debug' => (bool) $form_state->getValue('twig_debug'),
'twig_cache_disable' => (bool) $form_state->getValue('twig_cache_disable'),
];
if ($twig_development_mode) {
$invalidate_container = $twig_development_previous !== $twig_development;
$development_settings->setMultiple($twig_development);
}
else {
$invalidate_container = TRUE;
$development_settings->deleteMultiple(array_keys($twig_development));
}
if ($invalidate_container || $disable_rendered_output_cache_bins_previous !== $disable_rendered_output_cache_bins) {
$this->kernel->invalidateContainer();
}
$this->messenger()->addStatus($this->t('The settings have been saved.'));
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,85 @@
<?php
namespace Drupal\system\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure System settings for this site.
*/
class MenuLinksetSettingsForm extends ConfigFormBase {
/**
* Constructs a MenuLinksetSettingsForm object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
* The typed config manager.
* @param \Drupal\Core\Routing\RouteBuilderInterface $routerBuilder
* The router builder service.
*/
public function __construct(
ConfigFactoryInterface $config_factory,
TypedConfigManagerInterface $typedConfigManager,
protected readonly RouteBuilderInterface $routerBuilder,
) {
parent::__construct($config_factory, $typedConfigManager);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('config.typed'),
$container->get('router.builder')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'menu_linkset_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['system.feature_flags'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['linkset']['enable_endpoint'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable the menu linkset endpoint'),
'#description' => $this->t('See the <a href="@docs-link">decoupled menus documentation</a> for more information.', [
'@docs-link' => 'https://www.drupal.org/docs/develop/decoupled-drupal/decoupled-menus',
]),
'#default_value' => $this->config('system.feature_flags')->get('linkset_endpoint'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('system.feature_flags')
->set('linkset_endpoint', $form_state->getValue('enable_endpoint'))
->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Drupal\system\Form;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
/**
* Provides helpers for enabling modules.
*
* @internal
*/
trait ModulesEnabledTrait {
use StringTranslationTrait;
/**
* Gets the current user.
*
* @return \Drupal\Core\Session\AccountInterface
* The current user.
*/
abstract protected function currentUser();
/**
* Provides a confirmation message after modules have been enabled.
*
* @param string[] $modules
* Enabled module names, keyed by machine names.
*
* @return \Drupal\Core\StringTranslation\PluralTranslatableMarkup
* A confirmation message. If any of the enabled modules have permissions
* that the current user can manage, then include a link to the permissions
* page for those modules.
*/
protected function modulesEnabledConfirmationMessage(array $modules): PluralTranslatableMarkup {
$machine_names = implode(',', array_keys($modules));
$url = Url::fromRoute('user.admin_permissions.module', ['modules' => $machine_names]);
$module_names = implode(', ', array_values($modules));
$t_args = ['%name' => $module_names, '%names' => $module_names];
if ($url->access($this->currentUser())) {
return $this->formatPlural(
count($modules),
'Module %name has been installed. Configure <a href=":link">related permissions</a>.',
'@count modules have been installed: %names. Configure <a href=":link">related permissions</a>.',
$t_args + [':link' => $url->toString()]
);
}
return $this->formatPlural(
count($modules),
'Module %name has been installed.',
'@count modules have been installed: %names.',
$t_args
);
}
/**
* Provides a fail message after attempt to install a module.
*
* @param string[] $modules
* Enabled module names, keyed by machine names.
* @param \Drupal\Core\Config\PreExistingConfigException $exception
* Exception thrown if configuration with the same name already exists.
*
* @return \Drupal\Core\StringTranslation\PluralTranslatableMarkup
* A confirmation message. If any of the enabled modules have permissions
* that the current user can manage, then include a link to the permissions
* page for those modules.
*/
protected function modulesFailToEnableMessage(array $modules, PreExistingConfigException $exception): PluralTranslatableMarkup {
$config_objects = $exception->flattenConfigObjects($exception->getConfigObjects());
return $this->formatPlural(
count($config_objects),
'Unable to install @extension, %config_names already exists in active configuration.',
'Unable to install @extension, %config_names already exist in active configuration.',
[
'%config_names' => implode(', ', $config_objects),
'@extension' => $modules['install'][$exception->getExtension()],
]);
}
}

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