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,22 @@
/**
* @file
* Admin styling for the User module.
*/
/* Permissions page */
.permissions .module {
font-weight: bold;
}
.permissions .permission {
padding-left: 1.5em; /* LTR */
}
[dir="rtl"] .permissions .permission {
padding-right: 1.5em;
padding-left: 0;
}
/* Account settings */
.user-admin-settings .details-description {
padding-bottom: 0.5em;
font-size: 0.85em;
}

View File

@@ -0,0 +1,24 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/**
* @file
* Styling for the user module icons.
*/
/**
* Toolbar tab icon.
*/
.toolbar-bar .toolbar-icon-user::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23bebebe' d='M1 15c-.275 0-.498-.225-.496-.5 0 0 .007-.746 1.071-1.512 1.138-.818 2.347-.969 3.308-1.498s.954-1.299.925-2.115c-.019-.543-.112-.657-.489-1.392-.556-1.084-.709-2.021-.791-2.823-.127-1.252.011-3.035.86-4.001.847-.964 2.114-1.104 2.114-1.104l.5-.055.498.055s1.266.14 2.113 1.104c.85.966.988 2.75.859 4.001-.08.802-.234 1.739-.791 2.823-.377.734-.476.849-.488 1.392-.029.816-.035 1.586.926 2.115s2.17.68 3.307 1.498c1.064.766 1.072 1.512 1.072 1.512.002.275-.221.5-.496.5h-14.002z'/%3e%3c/svg%3e");
}
.toolbar-bar .toolbar-icon-user:active::before,
.toolbar-bar .toolbar-icon-user.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23ffffff' d='M1 15c-.275 0-.498-.225-.496-.5 0 0 .007-.746 1.071-1.512 1.138-.818 2.347-.969 3.308-1.498s.954-1.299.925-2.115c-.019-.543-.112-.657-.489-1.392-.556-1.084-.709-2.021-.791-2.823-.127-1.252.011-3.035.86-4.001.847-.964 2.114-1.104 2.114-1.104l.5-.055.498.055s1.266.14 2.113 1.104c.85.966.988 2.75.859 4.001-.08.802-.234 1.739-.791 2.823-.377.734-.476.849-.488 1.392-.029.816-.035 1.586.926 2.115s2.17.68 3.307 1.498c1.064.766 1.072 1.512 1.072 1.512.002.275-.221.5-.496.5h-14.002z'/%3e%3c/svg%3e");
}

View File

@@ -0,0 +1,15 @@
/**
* @file
* Styling for the user module icons.
*/
/**
* Toolbar tab icon.
*/
.toolbar-bar .toolbar-icon-user::before {
background-image: url(../../../misc/icons/bebebe/person.svg);
}
.toolbar-bar .toolbar-icon-user:active::before,
.toolbar-bar .toolbar-icon-user.is-active::before {
background-image: url(../../../misc/icons/ffffff/person.svg);
}

View File

@@ -0,0 +1,21 @@
/**
* @file
* Module styling for user module.
*/
.password-strength__title,
.password-strength__text {
display: inline;
}
.password-strength__meter {
height: 0.75em;
margin-top: 0.5em;
background-color: lightgray;
}
.password-strength__indicator {
width: 0;
height: 100%;
background-color: gray;
}
.password-confirm-match {
visibility: hidden;
}

View File

@@ -0,0 +1,21 @@
---
label: 'Creating a user account'
related:
- user.security_account_settings
- user.overview
---
{% set people_link_text %}{% trans %}People{% endtrans %}{% endset %}
{% set people_link = render_var(help_route_link(people_link_text, 'entity.user.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a new user account.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>{{ people_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add user</em>.{% endtrans %}</li>
<li>{% trans %}Enter the <em>Email address</em>, <em>Username</em>, and <em>Password</em> (twice) for the new user.{% endtrans %}</li>
<li>{% trans %}Verify that the <em>Roles</em> checked for the new user are correct.{% endtrans %}</li>
<li>{% trans %}If you want the new user to receive an email message notifying them of the new account, check <em>Notify user of new account</em>.{% endtrans %}</li>
<li>{% trans %}Optionally, change other settings on the form.{% endtrans %}</li>
<li>{% trans %}Click <em>Create new account</em>.{% endtrans %}</li>
<li>{% trans %}You will be left on the <em>Add user</em> page; repeat these steps if you have more user accounts to create.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,17 @@
---
label: 'Adding a new role'
related:
- user.overview
- user.permissions
---
{% set roles_link_text %}{% trans %}Roles{% endtrans %}{% endset %}
{% set roles_link = render_var(help_route_link(roles_link_text, 'entity.user_role.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a new role.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>People</em> &gt; <em>{{ roles_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Add role</em>.{% endtrans %}</li>
<li>{% trans %}Enter the desired <em>Role name</em>. If desired, click <em>Edit</em> to change the <em>Machine name</em> for the role.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. You should be returned to the <em>Roles</em> page and your new role should be in the role list.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,12 @@
---
label: 'Managing user accounts and site visitors'
top_level: true
---
<h2>{% trans %}What is a user?{% endtrans %}</h2>
<p>{% trans %}A user is anyone accessing or viewing your site. <em>Anonymous</em> users are users who are not logged in, and <em>Authenticated</em> users are users who are logged in.{% endtrans %}</p>
<h2>{% trans %}What is a role?{% endtrans %}</h2>
<p>{% trans %}<em>Roles</em> are used to group and classify users; each user can be assigned one or more roles. There are also special roles for all anonymous and all authenticated users.{% endtrans %}</p>
<h2>{% trans %}What is a permission?{% endtrans %}</h2>
<p>{% trans %}Granting a <em>permission</em> to a role allows users who have been assigned that role to perform an action on the site, such as viewing content, editing or creating a particular type of content, administering settings for a particular module, or using a particular function of the site (such as search).{% endtrans %}</p>
<h2>{% trans %}Overview of managing user accounts and visitors{% endtrans %}</h2>
<p>{% trans %}The core User module allows users to register, log in, and log out, and administrators to manage user roles and permissions. The core Ban module allows administrators to ban certain IP addresses from accessing the site. Depending on which modules you have installed on your site, the related topics below will help you with tasks related to managing user accounts and visitors.{% endtrans %}</p>

View File

@@ -0,0 +1,17 @@
---
label: 'Modifying the permissions for a role'
related:
- user.overview
- user.new_role
- core.security
---
{% set permissions_link_text %}{% trans %}Permissions{% endtrans %}{% endset %}
{% set permissions_link = render_var(help_route_link(permissions_link_text, 'user.admin_permissions')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Modify the permissions for an existing role.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>People</em> &gt; <em>{{ permissions_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Review the permissions for the role, paying particular attention to the permissions marked with <em>Warning: Give to trusted roles only; this permission has security implications.</em> Uncheck permissions that this role should not have, in the row of the permission and the column of the role; check permissions that this role should have.{% endtrans %}</li>
<li>{% trans %}Click <em>Save permissions</em>.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,36 @@
---
label: 'Configuring how user accounts are created and deleted'
related:
- core.security
- user.overview
---
{% set account_settings_link_text %}{% trans %}Account settings{% endtrans %}{% endset %}
{% set account_settings_link = render_var(help_route_link(account_settings_link_text, 'entity.user.admin_form')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure settings related to how user accounts are created and deleted.{% endtrans %}</p>
<h2>{% trans %}What are the settings related to user account creation and deletion?{% endtrans %}</h2>
<ul>
<li>{% trans %}You can make it possible for new users to register themselves for accounts, with or without email verification or administrative approval. Or, you can make it so only administrators with <em>Administer users</em> permission can register new users.{% endtrans %}</li>
<li>{% trans %}You can configure what happens to content that a user created, if their account is <em>canceled</em> (deleted).{% endtrans %}</li>
<li>{% trans %}You can edit the email messages that are sent to users when their accounts are pending, approved, created, blocked, or canceled, or when they request a password reset.{% endtrans %}</li>
</ul>
<h2>{% trans %}What are variables in email message text?{% endtrans %}</h2>
<p>{% trans %}<em>Variables</em> are short text strings, enclosed in square brackets [], that you can insert into configured email message text. When an individual message is generated, data from your site is substituted for the variables. Some commonly-used variables are:{% endtrans %}</p>
<ul>
<li>{% trans %}[site:name]: The name of your website.{% endtrans %}</li>
<li>{% trans %}[site:url]: The URL of your website.{% endtrans %}</li>
<li>{% trans %}[site:login-url]: The URL where users can log in to your site.{% endtrans %}</li>
<li>{% trans %}[user:display-name]: The user's displayed name.{% endtrans %}</li>
<li>{% trans %}[user:account-name]: The user's account name.{% endtrans %}</li>
<li>{% trans %}[user:mail]: The user's email alias.{% endtrans %}</li>
<li>{% trans %}[user:one-time-login-url]: An expiring URL that a user can use to log in once, if they need to reset their password.{% endtrans %}</li>
</ul>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Configuration</em> &gt; <em>People</em> &gt; <em>{{ account_settings_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Select the method you want to use for creating user accounts, and check or uncheck the box that requires email verification, to match the settings you want for your site.{% endtrans %}</li>
<li>{% trans %}Select the desired option for what happens to content that a user created if their account is canceled.{% endtrans %}</li>
<li>{% trans %}Optionally, edit the text of email messages related to user accounts.{% endtrans %}</li>
<li>{% trans %}Verify that the other settings are correct.{% 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,18 @@
---
label: 'Modifying or deleting a user account'
related:
- user.security_account_settings
- user.overview
---
{% set people_link_text %}{% trans %}People{% endtrans %}{% endset %}
{% set people_link = render_var(help_route_link(people_link_text, 'entity.user.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Update or delete an existing user account.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>{{ people_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Enter all or part of the user name or email address of the user account you want to update or delete, and click <em>Filter</em>. A short list of user accounts, including the account of interest, should be shown in the table; if not, modify the filter text until you can find the account of interest.{% endtrans %}</li>
<li>{% trans %}Click <em>Edit</em> in the <em>Operations</em> area of the account of interest.{% endtrans %}</li>
<li>{% trans %}To delete the user account, scroll to the bottom and click <em>Cancel account</em>. Select what you want to happen to the user's content on the next screen, and click <em>Cancel account</em>.{% endtrans %}</li>
<li>{% trans %}To update the user account, enter new values in the form and click <em>Save</em>.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,18 @@
id: d6_profile_values
label: Profile values
class: Drupal\user\Plugin\migrate\ProfileValues
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_profile_field_values
process:
uid: uid
destination:
plugin: entity:user
migration_dependencies:
required:
- d6_user
- user_profile_field_instance
- user_profile_entity_display
- user_profile_entity_form_display

View File

@@ -0,0 +1,56 @@
id: d6_user
label: User accounts
audit: true
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_user
process:
# If you are using this file to build a custom migration consider removing
# the uid field to allow incremental migrations.
uid: uid
name: name
pass: pass
mail: mail
created: created
access: access
login: login
status: status
timezone:
plugin: user_update_7002
source: timezone
langcode:
plugin: user_langcode
source: language
fallback_to_site_default: false
preferred_langcode:
plugin: user_langcode
source: language
fallback_to_site_default: true
preferred_admin_langcode:
plugin: user_langcode
source: language
fallback_to_site_default: true
init: init
roles:
plugin: migration_lookup
migration: d6_user_role
source: roles
user_picture:
plugin: migration_lookup
migration: d6_user_picture_file
source: uid
no_stub: true
destination:
plugin: entity:user
md5_passwords: true
migration_dependencies:
required:
- d6_user_role
optional:
- language
- default_language
- d6_user_picture_file
- user_picture_entity_display
- user_picture_entity_form_display

View File

@@ -0,0 +1,24 @@
id: d6_user_contact_settings
label: User contact settings
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_user
constants:
key: contact
module: contact
process:
uid: uid
key: 'constants/key'
module: 'constants/module'
settings:
plugin: skip_row_if_not_set
index: contact
source: data
destination:
plugin: user_data
migration_dependencies:
required:
- d6_user

View File

@@ -0,0 +1,69 @@
id: d6_user_mail
label: User mail configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: variable
variables:
- user_mail_status_activated_subject
- user_mail_status_activated_body
- user_mail_password_reset_subject
- user_mail_password_reset_body
- user_mail_status_deleted_subject
- user_mail_status_deleted_body
- user_mail_register_admin_created_subject
- user_mail_register_admin_created_body
- user_mail_register_no_approval_required_subject
- user_mail_register_no_approval_required_body
- user_mail_register_pending_approval_subject
- user_mail_register_pending_approval_body
- user_mail_status_blocked_subject
- user_mail_status_blocked_body
source_module: user
process:
'status_activated/subject':
plugin: convert_tokens
source: user_mail_status_activated_subject
'status_activated/body':
plugin: convert_tokens
source: user_mail_status_activated_body
'password_reset/subject':
plugin: convert_tokens
source: user_mail_password_reset_subject
'password_reset/body':
plugin: convert_tokens
source: user_mail_password_reset_body
'cancel_confirm/subject':
plugin: convert_tokens
source: user_mail_status_deleted_subject
'cancel_confirm/body':
plugin: convert_tokens
source: user_mail_status_deleted_body
'register_admin_created/subject':
plugin: convert_tokens
source: user_mail_register_admin_created_subject
'register_admin_created/body':
plugin: convert_tokens
source: user_mail_register_admin_created_body
'register_no_approval_required/subject':
plugin: convert_tokens
source: user_mail_register_no_approval_required_subject
'register_no_approval_required/body':
plugin: convert_tokens
source: user_mail_register_no_approval_required_body
'register_pending_approval/subject':
plugin: convert_tokens
source: user_mail_register_pending_approval_subject
'register_pending_approval/body':
plugin: convert_tokens
source: user_mail_register_pending_approval_body
'status_blocked/subject':
plugin: convert_tokens
source: user_mail_status_blocked_subject
'status_blocked/body':
plugin: convert_tokens
source: user_mail_status_blocked_body
destination:
plugin: config
config_name: user.mail

View File

@@ -0,0 +1,44 @@
id: d6_user_picture_file
label: User pictures
migration_tags:
- Drupal 6
- Content
source:
plugin: d6_user_picture_file
constants:
is_public: true
# source_base_path must be set by the tool configuring this migration. It
# represents the fully qualified path relative to which URIs in the files
# table are specified, and must end with a /.
source_base_path: ''
process:
filename: filename
uid: uid
source_full_path:
-
plugin: concat
delimiter: /
source:
- constants/source_base_path
- picture
-
plugin: urlencode
destination_full_path:
plugin: file_uri
source:
- picture
- file_directory_path
- temp_directory_path
- 'constants/is_public'
uri:
plugin: file_copy
source:
- '@source_full_path'
- '@destination_full_path'
destination:
plugin: entity:file
migration_dependencies:
# Every migration that references a file by Drupal 6 fid should specify d6_file as an
# optional dependency.
optional:
- d6_file

View File

@@ -0,0 +1,50 @@
id: d6_user_role
label: User roles
migration_tags:
- Drupal 6
- Configuration
source:
plugin: d6_user_role
process:
id:
-
plugin: machine_name
source: name
-
plugin: user_update_8002
label: name
permissions:
-
plugin: static_map
source: permissions
bypass: true
map:
'use PHP for block visibility': 'use PHP for settings'
'administer site-wide contact form': 'administer contact forms'
'post comments without approval': 'skip comment approval'
'edit own blog entries': 'edit own blog content'
'edit any blog entry': 'edit any blog content'
'delete own blog entries': 'delete own blog content'
'delete any blog entry': 'delete any blog content'
'create forum topics': 'create forum content'
'delete any forum topic': 'delete any forum content'
'delete own forum topics': 'delete own forum content'
'edit any forum topic': 'edit any forum content'
'edit own forum topics': 'edit own forum content'
- plugin: system_update_7000
- plugin: node_update_7008
- plugin: flatten
- plugin: filter_format_permission
migration: d6_filter_format
destination:
plugin: entity:user_role
migration_dependencies:
required:
- d6_filter_format
optional:
- block_content_type
- contact_category
- d6_comment_type
- d6_node_type
- d6_taxonomy_vocabulary
- d6_taxonomy_vocabulary_translation

View File

@@ -0,0 +1,30 @@
id: d6_user_settings
label: User configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: variable
variables:
- user_mail_status_blocked_notify
- user_mail_status_activated_notify
- user_email_verification
- user_register
- anonymous
source_module: user
process:
'notify/status_blocked': user_mail_status_blocked_notify
'notify/status_activated': user_mail_status_activated_notify
verify_mail: user_email_verification
register:
plugin: static_map
source: user_register
default_value: visitors_admin_approval
map:
2: visitors_admin_approval
1: visitors
0: admin_only
anonymous: anonymous
destination:
plugin: config
config_name: user.settings

View File

@@ -0,0 +1,59 @@
id: d7_user
label: User accounts
audit: true
migration_tags:
- Drupal 7
- Content
class: Drupal\user\Plugin\migrate\User
source:
plugin: d7_user
process:
# If you are using this file to build a custom migration consider removing
# the uid field to allow incremental migrations.
uid: uid
name: name
pass: pass
mail: mail
created: created
access: access
login: login
status: status
timezone: timezone
langcode:
plugin: user_langcode
source: entity_language
fallback_to_site_default: false
preferred_langcode:
plugin: user_langcode
source: language
fallback_to_site_default: true
preferred_admin_langcode:
plugin: user_langcode
source: language
fallback_to_site_default: true
init: init
roles:
plugin: migration_lookup
migration: d7_user_role
source: roles
user_picture:
-
plugin: default_value
source: picture
default_value: null
-
plugin: migration_lookup
migration: d7_file
destination:
plugin: entity:user
migration_dependencies:
required:
- d7_user_role
optional:
- d7_field_instance
- d7_file
- language
- default_language
- user_picture_field_instance
- user_picture_entity_display
- user_picture_entity_form_display

View File

@@ -0,0 +1,23 @@
id: d7_user_flood
label: User flood configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- user_failed_login_identifier_uid_only
- user_failed_login_ip_limit
- user_failed_login_ip_window
- user_failed_login_user_window
- user_failed_login_user_limit
source_module: user
process:
uid_only: user_failed_login_identifier_uid_only
ip_limit: user_failed_login_ip_limit
ip_window: user_failed_login_ip_window
user_limit: user_failed_login_user_limit
user_window: user_failed_login_user_window
destination:
plugin: config
config_name: user.flood

View File

@@ -0,0 +1,41 @@
id: d7_user_mail
label: User mail configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- user_mail_status_activated_subject
- user_mail_status_activated_body
- user_mail_password_reset_subject
- user_mail_password_reset_body
- user_mail_status_canceled_subject
- user_mail_status_canceled_body
- user_mail_register_admin_created_subject
- user_mail_register_admin_created_body
- user_mail_register_no_approval_required_subject
- user_mail_register_no_approval_required_body
- user_mail_register_pending_approval_subject
- user_mail_register_pending_approval_body
- user_mail_status_blocked_subject
- user_mail_status_blocked_body
source_module: user
process:
'status_activated/subject': user_mail_status_activated_subject
'status_activated/body': user_mail_status_activated_body
'password_reset/subject': user_mail_password_reset_subject
'password_reset/body': user_mail_password_reset_body
'cancel_confirm/subject': user_mail_status_canceled_subject
'cancel_confirm/body': user_mail_status_canceled_body
'register_admin_created/subject': user_mail_register_admin_created_subject
'register_admin_created/body': user_mail_register_admin_created_body
'register_no_approval_required/subject': user_mail_register_no_approval_required_subject
'register_no_approval_required/body': user_mail_register_no_approval_required_body
'register_pending_approval/subject': user_mail_register_pending_approval_subject
'register_pending_approval/body': user_mail_register_pending_approval_body
'status_blocked/subject': user_mail_status_blocked_subject
'status_blocked/body': user_mail_status_blocked_body
destination:
plugin: config
config_name: user.mail

View File

@@ -0,0 +1,47 @@
id: d7_user_role
label: User roles
migration_tags:
- Drupal 7
- Configuration
source:
plugin: d7_user_role
process:
id:
-
plugin: machine_name
source: name
-
plugin: user_update_8002
label: name
permissions:
-
plugin: static_map
source: permissions
bypass: true
map:
'use PHP for block visibility': 'use PHP for settings'
'administer site-wide contact form': 'administer contact forms'
'post comments without approval': 'skip comment approval'
'edit own blog entries': 'edit own blog content'
'edit any blog entry': 'edit any blog content'
'delete own blog entries': 'delete own blog content'
'delete any blog entry': 'delete any blog content'
'create forum topics': 'create forum content'
'delete any forum topic': 'delete any forum content'
'delete own forum topics': 'delete own forum content'
'edit any forum topic': 'edit any forum content'
'edit own forum topics': 'edit own forum content'
- plugin: flatten
weight: weight
destination:
plugin: entity:user_role
migration_dependencies:
optional:
- block_content_type
- contact_category
- d7_comment_type
- d7_filter_format
- d7_node_type
- d7_shortcut_set
- d7_taxonomy_vocabulary
- d7_taxonomy_vocabulary_translation

View File

@@ -0,0 +1,30 @@
id: d7_user_settings
label: User configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- user_mail_status_blocked_notify
- user_mail_status_activated_notify
- user_email_verification
- user_register
- anonymous
source_module: user
process:
'notify/status_blocked': user_mail_status_blocked_notify
'notify/status_activated': user_mail_status_activated_notify
verify_mail: user_email_verification
register:
plugin: static_map
source: user_register
default_value: visitors_admin_approval
map:
2: visitors_admin_approval
1: visitors
0: admin_only
anonymous: anonymous
destination:
plugin: config
config_name: user.settings

View File

@@ -0,0 +1,7 @@
finished:
6:
profile: user
user: user
7:
profile: user
user: user

View File

@@ -0,0 +1,32 @@
id: user_picture_entity_display
label: User picture display configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: user_picture_instance
constants:
entity_type: user
bundle: user
view_mode: default
name: user_picture
type: image
options:
label: hidden
settings:
image_style: ''
image_link: content
process:
entity_type: 'constants/entity_type'
bundle: 'constants/bundle'
view_mode: 'constants/view_mode'
field_name: 'constants/name'
type: 'constants/type'
options: 'constants/options'
'options/type': '@type'
destination:
plugin: component_entity_display
migration_dependencies:
required:
- user_picture_field_instance

View File

@@ -0,0 +1,31 @@
id: user_picture_entity_form_display
label: User picture form display configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: user_picture_instance
constants:
entity_type: user
bundle: user
form_mode: default
name: user_picture
type: image_image
options:
settings:
progress_indicator: throbber
preview_image_style: thumbnail
process:
entity_type: 'constants/entity_type'
bundle: 'constants/bundle'
field_name: 'constants/name'
form_mode: 'constants/form_mode'
type: 'constants/type'
options: 'constants/options'
'options/type': '@type'
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- user_picture_field_instance

View File

@@ -0,0 +1,26 @@
id: user_picture_field
label: User picture field configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
# We do an empty source and a proper destination to have an ID map for
# dependencies.
plugin: md_empty
constants:
entity_type: user
type: image
name: user_picture
cardinality: 1
source_module: user
process:
entity_type: 'constants/entity_type'
field_name: 'constants/name'
type: 'constants/type'
cardinality: 'constants/cardinality'
destination:
plugin: entity:field_storage_config
dependencies:
module:
- image

View File

@@ -0,0 +1,32 @@
id: user_picture_field_instance
label: User picture field instance configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: user_picture_instance
constants:
entity_type: user
bundle: user
name: user_picture
settings:
file_extensions: 'png gif jpg jpeg webp'
alt_field: false
title_field: false
min_resolution: ''
alt_field_required: false
title_field_required: false
process:
entity_type: 'constants/entity_type'
bundle: 'constants/bundle'
field_name: 'constants/name'
settings: 'constants/settings'
'settings/file_directory': file_directory
'settings/max_filesize': max_filesize
'settings/max_resolution': max_resolution
destination:
plugin: entity:field_config
migration_dependencies:
required:
- user_picture_field

View File

@@ -0,0 +1,56 @@
id: user_profile_entity_display
label: User profile display configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: profile_field
constants:
entity_type: user
bundle: user
view_mode: default
options:
label: hidden
settings: {}
process:
entity_type: 'constants/entity_type'
bundle: 'constants/bundle'
view_mode: 'constants/view_mode'
field_name:
-
plugin: migration_lookup
migration: user_profile_field
source: fid
-
plugin: skip_on_empty
method: row
-
plugin: extract
index:
- 1
type:
plugin: static_map
source: type
map:
checkbox: list_default
date: datetime_default
list: text_default
selection: list_default
textfield: text_default
textarea: text_default
url: link
options: 'constants/options'
'options/type': '@type'
hidden:
plugin: static_map
source: visibility
default_value: false
map:
1: true # PROFILE_PRIVATE
4: true # PROFILE_HIDDEN
destination:
plugin: component_entity_display
migration_dependencies:
required:
- user_profile_field_instance

View File

@@ -0,0 +1,65 @@
id: user_profile_entity_form_display
label: User profile form display configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: profile_field
constants:
empty: {}
entity_type: user
bundle: user
form_mode: default
process:
entity_type: 'constants/entity_type'
bundle: 'constants/bundle'
field_name:
-
plugin: migration_lookup
migration: user_profile_field
source: fid
-
plugin: skip_on_empty
method: row
-
plugin: extract
index:
- 1
form_mode: 'constants/form_mode'
type:
plugin: static_map
source: type
map:
checkbox: boolean_checkbox
date: datetime_default
list: text_textfield
selection: options_select
textfield: text_textfield
textarea: text_textarea
url: link_default
options: 'constants/options'
'options/type': '@type'
'options/settings':
plugin: field_instance_widget_settings
source:
- '@type'
- 'constants/empty' # we don't have any settings.
'options/settings/display_label': # Single on/off checkboxes need to have display_label = true otherwise their label doesn't show.
plugin: static_map
default_value: false
source: type
map:
checkbox: true
hidden:
plugin: static_map
source: visibility
default_value: false
map:
1: true # PROFILE_PRIVATE
4: true # PROFILE_HIDDEN
destination:
plugin: component_entity_form_display
migration_dependencies:
required:
- user_profile_field_instance

View File

@@ -0,0 +1,44 @@
id: user_profile_field
label: User profile field configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: profile_field
constants:
entity_type: user
process:
entity_type: 'constants/entity_type'
field_name:
-
plugin: machine_name
source: name
-
plugin: make_unique_entity_field
length: 30
entity_type: field_storage_config
field: field_name
type:
plugin: static_map
source: type
map:
checkbox: boolean
date: datetime
list: text
selection: list_string
textfield: text
textarea: text_long
url: link
settings:
plugin: profile_field_settings
source: type
'settings/allowed_values': options
cardinality:
plugin: static_map
default_value: 1
source: type
map:
list: -1
destination:
plugin: entity:field_storage_config

View File

@@ -0,0 +1,34 @@
id: user_profile_field_instance
label: User profile field instance configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: profile_field
constants:
entity_type: user
bundle: user
process:
entity_type: 'constants/entity_type'
bundle: 'constants/bundle'
label: title
description: explanation
field_name:
-
plugin: migration_lookup
migration: user_profile_field
source: fid
-
plugin: skip_on_empty
method: row
-
plugin: extract
index:
- 1
required: required
destination:
plugin: entity:field_config
migration_dependencies:
required:
- user_profile_field

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\user\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Determines access to routes based on login status of current user.
*/
class LoginStatusCheck implements AccessInterface {
/**
* Checks access.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(AccountInterface $account, Route $route) {
$required_status = filter_var($route->getRequirement('_user_is_logged_in'), FILTER_VALIDATE_BOOLEAN);
$actual_status = $account->isAuthenticated();
$access_result = AccessResult::allowedIf($required_status === $actual_status)->addCacheContexts(['user.roles:authenticated']);
if (!$access_result->isAllowed()) {
$access_result->setReason($required_status === TRUE ? 'This route can only be accessed by authenticated users.' : 'This route can only be accessed by anonymous users.');
}
return $access_result;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\user\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Permission access check.
*
* Determines access to routes based on permissions defined via
* $module.permissions.yml files.
*/
class PermissionAccessCheck implements AccessInterface {
/**
* Checks access.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, AccountInterface $account) {
$permission = $route->getRequirement('_permission');
if ($permission === NULL) {
return AccessResult::neutral();
}
// Allow to conjunct the permissions with OR ('+') or AND (',').
$split = explode(',', $permission);
if (count($split) > 1) {
return AccessResult::allowedIfHasPermissions($account, $split, 'AND');
}
else {
$split = explode('+', $permission);
return AccessResult::allowedIfHasPermissions($account, $split, 'OR');
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\user\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\UserInterface;
/**
* Access check for user registration routes.
*/
class RegisterAccessCheck implements AccessInterface {
/**
* Checks access.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(AccountInterface $account) {
$user_settings = \Drupal::config('user.settings');
return AccessResult::allowedIf($account->isAnonymous() && $user_settings->get('register') != UserInterface::REGISTER_ADMINISTRATORS_ONLY)->addCacheableDependency($user_settings);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Drupal\user\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Determines access to routes based on roles.
*
* You can specify the '_role' key on route requirements. If you specify a
* single role, users with that role with have access. If you specify multiple
* ones you can conjunct them with AND by using a "," and with OR by using "+".
*/
class RoleAccessCheck implements AccessInterface {
/**
* Checks access.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, AccountInterface $account) {
// Requirements just allow strings, so this might be a comma separated list.
$rid_string = $route->getRequirement('_role');
$explode_and = array_filter(array_map('trim', explode(',', $rid_string)));
if (count($explode_and) > 1) {
$diff = array_diff($explode_and, $account->getRoles());
if (empty($diff)) {
return AccessResult::allowed()->addCacheContexts(['user.roles']);
}
}
else {
$explode_or = array_filter(array_map('trim', explode('+', $rid_string)));
$intersection = array_intersect($explode_or, $account->getRoles());
if (!empty($intersection)) {
return AccessResult::allowed()->addCacheContexts(['user.roles']);
}
}
// If there is no allowed role, give other access checks a chance.
return AccessResult::neutral()->addCacheContexts(['user.roles']);
}
}

View File

@@ -0,0 +1,445 @@
<?php
namespace Drupal\user;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Html;
use Drupal\Core\Datetime\TimeZoneFormHelper;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Url;
use Drupal\language\ConfigurableLanguageManagerInterface;
use Drupal\user\Entity\Role;
use Drupal\user\Plugin\LanguageNegotiation\LanguageNegotiationUser;
use Drupal\user\Plugin\LanguageNegotiation\LanguageNegotiationUserAdmin;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the user account forms.
*/
abstract class AccountForm extends ContentEntityForm implements TrustedCallbackInterface {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a new EntityForm object.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, LanguageManagerInterface $language_manager, ?EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, ?TimeInterface $time = NULL) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('language_manager'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
/** @var \Drupal\user\UserInterface $account */
$account = $this->entity;
$user = $this->currentUser();
$config = \Drupal::config('user.settings');
$form['#cache']['tags'] = $config->getCacheTags();
$language_interface = \Drupal::languageManager()->getCurrentLanguage();
// Check for new account.
$register = $account->isNew();
// For a new account, there are 2 sub-cases:
// $self_register: A user creates their own, new, account
// (path '/user/register')
// $admin_create: An administrator creates a new account for another user
// (path '/admin/people/create')
// If the current user is logged in and has permission to create users
// then it must be the second case.
$admin_create = $register && $account->access('create');
$self_register = $register && !$admin_create;
// Account information.
$form['account'] = [
'#type' => 'container',
'#weight' => -10,
];
// The mail field is NOT required if account originally had no mail set
// and the user performing the edit has 'administer users' permission.
// This allows users without email address to be edited and deleted.
// Also see \Drupal\user\Plugin\Validation\Constraint\UserMailRequired.
$form['account']['mail'] = [
'#type' => 'email',
'#title' => $this->t('Email address'),
'#description' => $this->t('The email address is not made public. It will only be used if you need to be contacted about your account or for opted-in notifications.'),
'#required' => !(!$account->getEmail() && $user->hasPermission('administer users')),
'#default_value' => (!$register ? $account->getEmail() : ''),
'#access' => $account->mail->access('edit'),
];
// Only show name field on registration form or user can change own username.
$form['account']['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Username'),
'#maxlength' => UserInterface::USERNAME_MAX_LENGTH,
'#description' => $this->t("Several special characters are allowed, including space, period (.), hyphen (-), apostrophe ('), underscore (_), and the @ sign."),
'#required' => TRUE,
'#attributes' => [
'class' => ['username'],
'autocorrect' => 'off',
'autocapitalize' => 'off',
'spellcheck' => 'false',
],
'#default_value' => (!$register ? $account->getAccountName() : ''),
'#access' => $account->name->access('edit'),
];
// Display password field only for existing users or when user is allowed to
// assign a password during registration.
if (!$register) {
$form['account']['pass'] = [
'#type' => 'password_confirm',
'#size' => 25,
'#description' => $this->t('To change the current user password, enter the new password in both fields.'),
];
// To skip the current password field, the user must have logged in via a
// one-time link and have the token in the URL. Store this in $form_state
// so it persists even on subsequent Ajax requests.
$request = $this->getRequest();
if (!$form_state->get('user_pass_reset') && ($token = $request->query->get('pass-reset-token'))) {
$session_key = 'pass_reset_' . $account->id();
$session_value = $request->getSession()->get($session_key);
$user_pass_reset = isset($session_value) && hash_equals($session_value, $token);
$form_state->set('user_pass_reset', $user_pass_reset);
}
// The user must enter their current password to change to a new one.
if ($user->id() == $account->id()) {
$form['account']['current_pass'] = [
'#type' => 'password',
'#title' => $this->t('Current password'),
'#size' => 25,
'#access' => !$form_state->get('user_pass_reset'),
'#weight' => -5,
// Do not let web browsers remember this password, since we are
// trying to confirm that the person submitting the form actually
// knows the current one.
'#attributes' => ['autocomplete' => 'off'],
];
$form_state->set('user', $account);
// The user may only change their own password without their current
// password if they logged in via a one-time login link.
if (!$form_state->get('user_pass_reset')) {
$form['account']['current_pass']['#description'] = $this->t('Required if you want to change the %mail or %pass below. <a href=":request_new_url" title="Send password reset instructions via email.">Reset your password</a>.', [
'%mail' => $form['account']['mail']['#title'],
'%pass' => $this->t('Password'),
':request_new_url' => Url::fromRoute('user.pass')->toString(),
]);
}
}
}
elseif (!$config->get('verify_mail') || $admin_create) {
$form['account']['pass'] = [
'#type' => 'password_confirm',
'#size' => 25,
'#description' => $this->t('Provide a password for the new account in both fields.'),
'#required' => TRUE,
];
}
// When not building the user registration form, prevent web browsers from
// auto-filling/prefilling the email, username, and password fields.
if (!$register) {
foreach (['mail', 'name', 'pass'] as $key) {
if (isset($form['account'][$key])) {
$form['account'][$key]['#attributes']['autocomplete'] = 'off';
}
}
}
if (!$self_register) {
$status = $account->get('status')->value;
}
else {
$status = $config->get('register') == UserInterface::REGISTER_VISITORS ? 1 : 0;
}
$form['account']['status'] = [
'#type' => 'radios',
'#title' => $this->t('Status'),
'#default_value' => $status,
'#options' => [$this->t('Blocked'), $this->t('Active')],
'#access' => $account->status->access('edit'),
];
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
$roles = array_map(fn(RoleInterface $role) => Html::escape($role->label()), $roles);
$form['account']['roles'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Roles'),
'#default_value' => (!$register ? $account->getRoles() : []),
'#options' => $roles,
'#access' => $roles && $user->hasPermission('administer permissions'),
];
// Special handling for the inevitable "Authenticated user" role.
$form['account']['roles'][RoleInterface::AUTHENTICATED_ID] = [
'#default_value' => TRUE,
'#disabled' => TRUE,
];
$form['account']['notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user of new account'),
'#access' => $admin_create,
];
$user_preferred_langcode = $register ? $language_interface->getId() : $account->getPreferredLangcode();
$user_preferred_admin_langcode = $register ? $language_interface->getId() : $account->getPreferredAdminLangcode(FALSE);
// Is the user preferred language added?
$user_language_added = FALSE;
if ($this->languageManager instanceof ConfigurableLanguageManagerInterface) {
$negotiator = $this->languageManager->getNegotiator();
$user_language_added = $negotiator && $negotiator->isNegotiationMethodEnabled(LanguageNegotiationUser::METHOD_ID, LanguageInterface::TYPE_INTERFACE);
}
$form['language'] = [
'#type' => $this->languageManager->isMultilingual() ? 'details' : 'container',
'#title' => $this->t('Language settings'),
'#open' => TRUE,
// Display language selector when either creating a user on the admin
// interface or editing a user account.
'#access' => !$self_register,
];
$form['language']['preferred_langcode'] = [
'#type' => 'language_select',
'#title' => $this->t('Site language'),
'#languages' => LanguageInterface::STATE_CONFIGURABLE,
'#default_value' => $user_preferred_langcode,
'#description' => $user_language_added ? $this->t("This account's preferred language for emails and site presentation.") : $this->t("This account's preferred language for emails."),
// This is used to explain that user preferred language and entity
// language are synchronized. It can be removed if a different behavior is
// desired.
'#pre_render' => ['user_langcode' => [$this, 'alterPreferredLangcodeDescription']],
];
// Only show the account setting for Administration pages language to users
// if one of the detection and selection methods uses it.
$show_admin_language = FALSE;
if (($account->hasPermission('access administration pages') || $account->hasPermission('view the administration theme')) && $this->languageManager instanceof ConfigurableLanguageManagerInterface) {
$negotiator = $this->languageManager->getNegotiator();
$show_admin_language = $negotiator && $negotiator->isNegotiationMethodEnabled(LanguageNegotiationUserAdmin::METHOD_ID);
}
$form['language']['preferred_admin_langcode'] = [
'#type' => 'language_select',
'#title' => $this->t('Administration pages language'),
'#languages' => LanguageInterface::STATE_CONFIGURABLE,
'#default_value' => $user_preferred_admin_langcode,
'#access' => $show_admin_language,
'#empty_option' => $this->t('- No preference -'),
'#empty_value' => '',
];
// User entities contain both a langcode property (for identifying the
// language of the entity data) and a preferred_langcode property (see
// above). Rather than provide a UI forcing the user to choose both
// separately, assume that the user profile data is in the user's preferred
// language. This entity builder provides that synchronization. For
// use-cases where this synchronization is not desired, a module can alter
// or remove this item. Sync user langcode only when a user registers and
// not when a user is updated or translated.
if ($register) {
$form['#entity_builders']['sync_user_langcode'] = '::syncUserLangcode';
}
$system_date_config = \Drupal::config('system.date');
$form['timezone'] = [
'#type' => 'details',
'#title' => $this->t('Locale settings'),
'#open' => TRUE,
'#weight' => 6,
'#access' => $system_date_config->get('timezone.user.configurable'),
];
if ($self_register && $system_date_config->get('timezone.user.default') != UserInterface::TIMEZONE_SELECT) {
$form['timezone']['#access'] = FALSE;
}
$form['timezone']['timezone'] = [
'#type' => 'select',
'#title' => $this->t('Time zone'),
'#default_value' => $account->getTimezone() ?: $system_date_config->get('timezone.default'),
'#options' => TimeZoneFormHelper::getOptionsListByRegion($account->id() != $user->id()),
'#description' => $this->t('Select the desired local time and time zone. Dates and times throughout this site will be displayed using this time zone.'),
];
// If not set or selected yet, detect timezone for the current user only.
$user_input = $form_state->getUserInput();
if (!$account->getTimezone() && $account->id() == $user->id() && empty($user_input['timezone'])) {
$form['timezone']['#attached']['library'][] = 'core/drupal.timezone';
$form['timezone']['timezone']['#attributes'] = ['class' => ['timezone-detect']];
}
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['alterPreferredLangcodeDescription'];
}
/**
* Alters the preferred language widget description.
*
* @param array $element
* The preferred language form element.
*
* @return array
* The preferred language form element.
*/
public function alterPreferredLangcodeDescription(array $element) {
// Only add to the description if the form element has a description.
if (isset($element['#description'])) {
$element['#description'] .= ' ' . $this->t("This is also assumed to be the primary language of this account's profile information.");
}
return $element;
}
/**
* Synchronizes preferred language and entity language.
*
* @param string $entity_type_id
* The entity type identifier.
* @param \Drupal\user\UserInterface $user
* The entity updated with the submitted values.
* @param array $form
* The complete form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function syncUserLangcode($entity_type_id, UserInterface $user, array &$form, FormStateInterface &$form_state) {
$user->getUntranslated()->langcode = $user->preferred_langcode;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
// Change the roles array to a list of enabled roles.
// @todo Alter the form state as the form values are directly extracted and
// set on the field, which throws an exception as the list requires
// numeric keys. Allow to override this per field. As this function is
// called twice, we have to prevent it from getting the array keys twice.
if (is_string(key($form_state->getValue('roles')))) {
$form_state->setValue('roles', array_keys(array_filter($form_state->getValue('roles'))));
}
/** @var \Drupal\user\UserInterface $account */
$account = parent::buildEntity($form, $form_state);
// Translate the empty value '' of language selects to an unset field.
foreach (['preferred_langcode', 'preferred_admin_langcode'] as $field_name) {
if ($form_state->getValue($field_name) === '') {
$account->$field_name = NULL;
}
}
// Set existing password if set in the form state.
$current_pass = trim($form_state->getValue('current_pass', ''));
if (strlen($current_pass) > 0) {
$account->setExistingPassword($current_pass);
}
// Skip the protected user field constraint if the user came from the
// password recovery page.
$account->_skipProtectedUserFieldConstraint = $form_state->get('user_pass_reset');
return $account;
}
/**
* {@inheritdoc}
*/
protected function getEditedFieldNames(FormStateInterface $form_state) {
return array_merge([
'name',
'pass',
'mail',
'timezone',
'langcode',
'preferred_langcode',
'preferred_admin_langcode',
], parent::getEditedFieldNames($form_state));
}
/**
* {@inheritdoc}
*/
protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
// Manually flag violations of fields not handled by the form display. This
// is necessary as entity form displays only flag violations for fields
// contained in the display.
$field_names = [
'name',
'pass',
'mail',
'timezone',
'langcode',
'preferred_langcode',
'preferred_admin_langcode',
];
foreach ($violations->getByFields($field_names) as $violation) {
[$field_name] = explode('.', $violation->getPropertyPath(), 2);
$form_state->setErrorByName($field_name, $violation->getMessage());
}
parent::flagViolations($violations, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$user = $this->getEntity();
// If there's a session set to the users id, remove the password reset tag
// since a new password was saved.
$this->getRequest()->getSession()->remove('pass_reset_' . $user->id());
}
}

View File

@@ -0,0 +1,421 @@
<?php
namespace Drupal\user;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure user settings for this site.
*
* @internal
*/
class AccountSettingsForm extends ConfigFormBase {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The role storage used when changing the admin role.
*
* @var \Drupal\user\RoleStorageInterface
*/
protected $roleStorage;
/**
* Constructs a \Drupal\user\AccountSettingsForm 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\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
*/
public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ModuleHandlerInterface $module_handler, RoleStorageInterface $role_storage) {
parent::__construct($config_factory, $typedConfigManager);
$this->moduleHandler = $module_handler;
$this->roleStorage = $role_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('config.typed'),
$container->get('module_handler'),
$container->get('entity_type.manager')->getStorage('user_role')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_admin_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'system.site',
'user.mail',
'user.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$config = $this->config('user.settings');
$site_config = $this->config('system.site');
$form['#attached']['library'][] = 'user/drupal.user.admin';
// Settings for anonymous users.
$form['anonymous_settings'] = [
'#type' => 'details',
'#title' => $this->t('Anonymous users'),
'#open' => TRUE,
];
$form['anonymous_settings']['anonymous'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#config_target' => 'user.settings:anonymous',
'#description' => $this->t('The name used to indicate anonymous users.'),
'#required' => TRUE,
];
// @todo Remove this check once language settings are generalized.
if ($this->moduleHandler->moduleExists('content_translation')) {
$form['language'] = [
'#type' => 'details',
'#title' => $this->t('Language settings'),
'#open' => TRUE,
'#tree' => TRUE,
];
$form_state->set(['content_translation', 'key'], 'language');
$form['language'] += content_translation_enable_widget('user', 'user', $form, $form_state);
}
// User registration settings.
$form['registration_cancellation'] = [
'#type' => 'details',
'#title' => $this->t('Registration and cancellation'),
'#open' => TRUE,
];
$form['registration_cancellation']['user_register'] = [
'#type' => 'radios',
'#title' => $this->t('Who can register accounts?'),
'#config_target' => 'user.settings:register',
'#options' => [
UserInterface::REGISTER_ADMINISTRATORS_ONLY => $this->t('Administrators only'),
UserInterface::REGISTER_VISITORS => $this->t('Visitors'),
UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL => $this->t('Visitors, but administrator approval is required'),
],
];
$form['registration_cancellation']['user_email_verification'] = [
'#type' => 'checkbox',
'#title' => $this->t('Require email verification when a visitor creates an account'),
'#config_target' => 'user.settings:verify_mail',
'#description' => $this->t('New users will be required to validate their email address prior to logging into the site, and will be assigned a system-generated password. With this setting disabled, users will be logged in immediately upon registering, and may select their own passwords during registration.'),
];
$form['registration_cancellation']['user_password_strength'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable password strength indicator'),
'#config_target' => 'user.settings:password_strength',
];
$form['registration_cancellation']['user_cancel_method'] = [
'#type' => 'radios',
'#title' => $this->t('When cancelling a user account'),
'#config_target' => 'user.settings:cancel_method',
'#description' => $this->t('Users with the %select-cancel-method or %administer-users <a href=":permissions-url">permissions</a> can override this default method.', ['%select-cancel-method' => $this->t('Select method for cancelling account'), '%administer-users' => $this->t('Administer users'), ':permissions-url' => Url::fromRoute('user.admin_permissions')->toString()]),
];
$form['registration_cancellation']['user_cancel_method'] += user_cancel_methods();
foreach (Element::children($form['registration_cancellation']['user_cancel_method']) as $key) {
// All account cancellation methods that specify #access cannot be
// configured as default method.
// @see hook_user_cancel_methods_alter()
if (isset($form['registration_cancellation']['user_cancel_method'][$key]['#access'])) {
$form['registration_cancellation']['user_cancel_method'][$key]['#access'] = FALSE;
}
}
// Default notifications address.
$form['mail_notification_address'] = [
'#type' => 'email',
'#title' => $this->t('Notification email address'),
'#config_target' => 'system.site:mail_notification',
'#description' => $this->t("The email address to be used as the 'from' address for all account notifications listed below. If <em>'Visitors, but administrator approval is required'</em> is selected above, a notification email will also be sent to this address for any new registrations. Leave empty to use the default system email address <em>(%site-email).</em>", ['%site-email' => $site_config->get('mail')]),
'#maxlength' => 180,
];
$form['email'] = [
'#type' => 'vertical_tabs',
'#title' => $this->t('Emails'),
];
// These email tokens are shared for all settings, so just define
// the list once to help ensure they stay in sync.
$email_token_help = $this->t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
$form['email_admin_created'] = [
'#type' => 'details',
'#title' => $this->t('Welcome (new user created by administrator)'),
'#open' => $config->get('register') == UserInterface::REGISTER_ADMINISTRATORS_ONLY,
'#description' => $this->t('Edit the welcome email messages sent to new member accounts created by an administrator.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_admin_created']['user_mail_register_admin_created_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:register_admin_created.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_admin_created']['user_mail_register_admin_created_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:register_admin_created.body',
'#rows' => 15,
];
$form['email_pending_approval'] = [
'#type' => 'details',
'#title' => $this->t('Welcome (awaiting approval)'),
'#open' => $config->get('register') == UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL,
'#description' => $this->t('Edit the welcome email messages sent to new members upon registering, when administrative approval is required.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_pending_approval']['user_mail_register_pending_approval_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:register_pending_approval.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_pending_approval']['user_mail_register_pending_approval_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:register_pending_approval.body',
'#rows' => 8,
];
$form['email_pending_approval_admin'] = [
'#type' => 'details',
'#title' => $this->t('Admin (user awaiting approval)'),
'#open' => $config->get('register') == UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL,
'#description' => $this->t('Edit the email notifying the site administrator that there are new members awaiting administrative approval.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_pending_approval_admin']['register_pending_approval_admin_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:register_pending_approval_admin.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_pending_approval_admin']['register_pending_approval_admin_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:register_pending_approval_admin.body',
'#rows' => 8,
];
$form['email_no_approval_required'] = [
'#type' => 'details',
'#title' => $this->t('Welcome (no approval required)'),
'#open' => $config->get('register') == UserInterface::REGISTER_VISITORS,
'#description' => $this->t('Edit the welcome email messages sent to new members upon registering, when no administrator approval is required.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_no_approval_required']['user_mail_register_no_approval_required_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:register_no_approval_required.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_no_approval_required']['user_mail_register_no_approval_required_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:register_no_approval_required.body',
'#rows' => 15,
];
$form['email_password_reset'] = [
'#type' => 'details',
'#title' => $this->t('Password recovery'),
'#description' => $this->t('Edit the email messages sent to users who request a new password.') . ' ' . $email_token_help,
'#group' => 'email',
'#weight' => 10,
];
$form['email_password_reset']['user_mail_password_reset_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:password_reset.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_password_reset']['user_mail_password_reset_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:password_reset.body',
'#rows' => 12,
];
$form['email_activated'] = [
'#type' => 'details',
'#title' => $this->t('Account activation'),
'#description' => $this->t('Enable and edit email messages sent to users upon account activation (when an administrator activates an account of a user who has already registered, on a site where administrative approval is required).') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_activated']['user_mail_status_activated_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is activated'),
'#config_target' => 'user.settings:notify.status_activated',
];
$form['email_activated']['settings'] = [
'#type' => 'container',
'#states' => [
// Hide the additional settings when this email is disabled.
'invisible' => [
'input[name="user_mail_status_activated_notify"]' => ['checked' => FALSE],
],
],
];
$form['email_activated']['settings']['user_mail_status_activated_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:status_activated.subject',
'#states' => [
'required' => [
'input[name="user_mail_status_activated_notify"]' => ['checked' => TRUE],
],
],
'#maxlength' => 180,
];
$form['email_activated']['settings']['user_mail_status_activated_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:status_activated.body',
'#rows' => 15,
];
$form['email_blocked'] = [
'#type' => 'details',
'#title' => $this->t('Account blocked'),
'#description' => $this->t('Enable and edit email messages sent to users when their accounts are blocked.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_blocked']['user_mail_status_blocked_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is blocked'),
'#config_target' => 'user.settings:notify.status_blocked',
];
$form['email_blocked']['settings'] = [
'#type' => 'container',
'#states' => [
// Hide the additional settings when the blocked email is disabled.
'invisible' => [
'input[name="user_mail_status_blocked_notify"]' => ['checked' => FALSE],
],
],
];
$form['email_blocked']['settings']['user_mail_status_blocked_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:status_blocked.subject',
'#states' => [
'required' => [
'input[name="user_mail_status_blocked_notify"]' => ['checked' => TRUE],
],
],
'#maxlength' => 180,
];
$form['email_blocked']['settings']['user_mail_status_blocked_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:status_blocked.body',
'#rows' => 3,
];
$form['email_cancel_confirm'] = [
'#type' => 'details',
'#title' => $this->t('Account cancellation confirmation'),
'#description' => $this->t('Edit the email messages sent to users when they attempt to cancel their accounts.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_cancel_confirm']['user_mail_cancel_confirm_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:cancel_confirm.subject',
'#required' => TRUE,
'#maxlength' => 180,
];
$form['email_cancel_confirm']['user_mail_cancel_confirm_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:cancel_confirm.body',
'#rows' => 3,
];
$form['email_canceled'] = [
'#type' => 'details',
'#title' => $this->t('Account canceled'),
'#description' => $this->t('Enable and edit email messages sent to users when their accounts are canceled.') . ' ' . $email_token_help,
'#group' => 'email',
];
$form['email_canceled']['user_mail_status_canceled_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is canceled'),
'#config_target' => 'user.settings:notify.status_canceled',
];
$form['email_canceled']['settings'] = [
'#type' => 'container',
'#states' => [
// Hide the settings when the cancel notify checkbox is disabled.
'invisible' => [
'input[name="user_mail_status_canceled_notify"]' => ['checked' => FALSE],
],
],
];
$form['email_canceled']['settings']['user_mail_status_canceled_subject'] = [
'#type' => 'textfield',
'#title' => $this->t('Subject'),
'#config_target' => 'user.mail:status_canceled.subject',
'#states' => [
'required' => [
'input[name="user_mail_status_canceled_subject"]' => ['checked' => TRUE],
],
],
'#maxlength' => 180,
];
$form['email_canceled']['settings']['user_mail_status_canceled_body'] = [
'#type' => 'textarea',
'#title' => $this->t('Body'),
'#config_target' => 'user.mail:status_canceled.body',
'#rows' => 3,
];
return $form;
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Drupal\user\Authentication\Provider;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\UserSession;
use Drupal\Core\Session\SessionConfigurationInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Cookie based authentication provider.
*/
class Cookie implements AuthenticationProviderInterface, EventSubscriberInterface {
use StringTranslationTrait;
/**
* The session configuration.
*
* @var \Drupal\Core\Session\SessionConfigurationInterface
*/
protected $sessionConfiguration;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new cookie authentication provider.
*
* @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
* The session configuration.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(SessionConfigurationInterface $session_configuration, Connection $connection, MessengerInterface $messenger) {
$this->sessionConfiguration = $session_configuration;
$this->connection = $connection;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
$applies = $this->sessionConfiguration->hasSession($request);
if (!$applies && $request->query->has('check_logged_in')) {
$domain = ltrim(ini_get('session.cookie_domain'), '.') ?: $request->getHttpHost();
$this->messenger->addMessage($this->t('To log in to this site, your browser must accept cookies from the domain %domain.', ['%domain' => $domain]), 'error');
}
return $applies;
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
return $this->getUserFromSession($request->getSession());
}
/**
* Returns the UserSession object for the given session.
*
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* The session.
*
* @return \Drupal\Core\Session\AccountInterface|null
* The UserSession object for the current user, or NULL if this is an
* anonymous session.
*/
protected function getUserFromSession(SessionInterface $session) {
if ($uid = $session->get('uid')) {
// @todo Load the User entity in SessionHandler so we don't need queries.
// @see https://www.drupal.org/node/2345611
$values = $this->connection
->query('SELECT * FROM {users_field_data} [u] WHERE [u].[uid] = :uid AND [u].[default_langcode] = 1', [':uid' => $uid])
->fetchAssoc();
// Check if the user data was found and the user is active.
if (!empty($values) && $values['status'] == 1) {
// Add the user's roles.
$rids = $this->connection
->query('SELECT [roles_target_id] FROM {user__roles} WHERE [entity_id] = :uid', [':uid' => $values['uid']])
->fetchCol();
$values['roles'] = array_merge([AccountInterface::AUTHENTICATED_ROLE], $rids);
return new UserSession($values);
}
}
// This is an anonymous session.
return NULL;
}
/**
* Adds a query parameter to check successful log in redirect URL.
*
* @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
* The Event to process.
*/
public function addCheckToUrl(ResponseEvent $event) {
$response = $event->getResponse();
if ($response instanceof RedirectResponse) {
if ($event->getRequest()->getSession()->has('check_logged_in')) {
$event->getRequest()->getSession()->remove('check_logged_in');
$url = $response->getTargetUrl();
$options = UrlHelper::parse($url);
$options['query']['check_logged_in'] = '1';
$url = $options['path'] . '?' . UrlHelper::buildQuery($options['query']);
if (!empty($options['fragment'])) {
$url .= '#' . $options['fragment'];
}
// In the case of trusted redirect, we have to update the list of
// trusted URLs because here we've just modified its target URL
// which is in the list.
if ($response instanceof TrustedRedirectResponse) {
$response->setTrustedTargetUrl($url);
}
$response->setTargetUrl($url);
}
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
public static function getSubscribedEvents(): array {
$events[KernelEvents::RESPONSE][] = ['addCheckToUrl', -1000];
return $events;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\user\ContextProvider;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Sets the current user as a context.
*/
class CurrentUserContext implements ContextProviderInterface {
use StringTranslationTrait;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs a new CurrentUserContext.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(AccountInterface $account, EntityTypeManagerInterface $entity_type_manager) {
$this->account = $account;
$this->userStorage = $entity_type_manager->getStorage('user');
}
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
$current_user = $this->userStorage->load($this->account->id());
if ($current_user) {
// @todo Do not validate protected fields to avoid bug in TypedData,
// remove this in https://www.drupal.org/project/drupal/issues/2934192.
$current_user->_skipProtectedUserFieldConstraint = TRUE;
$context = EntityContext::fromEntity($current_user, $this->t('Current user'));
}
else {
// If not user is available, provide an empty context object.
$context = EntityContext::fromEntityTypeId('user', $this->t('Current user'));
}
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['user']);
$context->addCacheableDependency($cacheability);
$result = [
'current_user' => $context,
];
return $result;
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
return $this->getRuntimeContexts([]);
}
}

View File

@@ -0,0 +1,441 @@
<?php
namespace Drupal\user\Controller;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\user\UserAuthenticationInterface;
use Drupal\user\UserAuthInterface;
use Drupal\user\UserFloodControlInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Serializer;
/**
* Provides controllers for login, login status and logout via HTTP requests.
*/
class UserAuthenticationController extends ControllerBase implements ContainerInjectionInterface {
/**
* String sent in responses, to describe the user as being logged in.
*
* @var string
*/
const LOGGED_IN = '1';
/**
* String sent in responses, to describe the user as being logged out.
*
* @var string
*/
const LOGGED_OUT = '0';
/**
* The user flood control service.
*
* @var \Drupal\user\UserFloodControl
*/
protected $userFloodControl;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* The user authentication.
* @var \Drupal\user\UserAuthInterface|\Drupal\user\UserAuthenticationInterface
*/
protected $userAuth;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The serializer.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* The available serialization formats.
*
* @var array
*/
protected $serializerFormats = [];
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a new UserAuthenticationController object.
*
* @param \Drupal\user\UserFloodControlInterface $user_flood_control
* The user flood control service.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
* @param \Drupal\user\UserAuthenticationInterface|\Drupal\user\UserAuthInterface $user_auth
* The user authentication.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Symfony\Component\Serializer\Serializer $serializer
* The serializer.
* @param array $serializer_formats
* The available serialization formats.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(UserFloodControlInterface $user_flood_control, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthenticationInterface|UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats, LoggerInterface $logger) {
$this->userFloodControl = $user_flood_control;
$this->userStorage = $user_storage;
$this->csrfToken = $csrf_token;
if (!$user_auth instanceof UserAuthenticationInterface) {
@trigger_error('The $user_auth parameter implementing UserAuthInterface is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. Implement UserAuthenticationInterface instead. See https://www.drupal.org/node/3411040');
}
$this->userAuth = $user_auth;
$this->serializer = $serializer;
$this->serializerFormats = $serializer_formats;
$this->routeProvider = $route_provider;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
if ($container->hasParameter('serializer.formats') && $container->has('serializer')) {
$serializer = $container->get('serializer');
$formats = $container->getParameter('serializer.formats');
}
else {
$formats = ['json'];
$encoders = [new JsonEncoder()];
$serializer = new Serializer([], $encoders);
}
return new static(
$container->get('user.flood_control'),
$container->get('entity_type.manager')->getStorage('user'),
$container->get('csrf_token'),
$container->get('user.auth'),
$container->get('router.route_provider'),
$serializer,
$formats,
$container->get('logger.factory')->get('user')
);
}
/**
* Logs in a user.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response
* A response which contains the ID and CSRF token.
*/
public function login(Request $request) {
$format = $this->getRequestFormat($request);
$content = $request->getContent();
$credentials = $this->serializer->decode($content, $format);
if (!isset($credentials['name']) && !isset($credentials['pass'])) {
throw new BadRequestHttpException('Missing credentials.');
}
if (!isset($credentials['name'])) {
throw new BadRequestHttpException('Missing credentials.name.');
}
if (!isset($credentials['pass'])) {
throw new BadRequestHttpException('Missing credentials.pass.');
}
$this->floodControl($request, $credentials['name']);
$account = FALSE;
if ($this->userAuth instanceof UserAuthenticationInterface) {
$account = $this->userAuth->lookupAccount($credentials['name']);
}
else {
$accounts = $this->userStorage->loadByProperties(['name' => $credentials['name']]);
if ($accounts) {
$account = reset($accounts);
}
}
if ($account) {
if ($account->isBlocked()) {
throw new BadRequestHttpException('The user has not been activated or is blocked.');
}
if ($this->userAuth instanceof UserAuthenticationInterface) {
$authenticated = $this->userAuth->authenticateAccount($account, $credentials['pass']) ? $account->id() : FALSE;
}
else {
$authenticated = $this->userAuth->authenticateAccount($credentials['name'], $credentials['pass']);
}
if ($authenticated) {
$this->userFloodControl->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
$this->userLoginFinalize($account);
// Send basic metadata about the logged in user.
$response_data = [];
if ($account->get('uid')->access('view', $account)) {
$response_data['current_user']['uid'] = $account->id();
}
if ($account->get('roles')->access('view', $account)) {
$response_data['current_user']['roles'] = $account->getRoles();
}
if ($account->get('name')->access('view', $account)) {
$response_data['current_user']['name'] = $account->getAccountName();
}
$response_data['csrf_token'] = $this->csrfToken->get('rest');
$logout_route = $this->routeProvider->getRouteByName('user.logout.http');
// Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
$logout_path = ltrim($logout_route->getPath(), '/');
$response_data['logout_token'] = $this->csrfToken->get($logout_path);
$encoded_response_data = $this->serializer->encode($response_data, $format);
return new Response($encoded_response_data);
}
}
$flood_config = $this->config('user.flood');
if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) {
$this->userFloodControl->register('user.http_login', $flood_config->get('user_window'), $identifier);
}
// Always register an IP-based failed login event.
$this->userFloodControl->register('user.failed_login_ip', $flood_config->get('ip_window'));
throw new BadRequestHttpException('Sorry, unrecognized username or password.');
}
/**
* Resets a user password.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function resetPassword(Request $request) {
$format = $this->getRequestFormat($request);
$content = $request->getContent();
$credentials = $this->serializer->decode($content, $format);
// Check if a name or mail is provided.
if (!isset($credentials['name']) && !isset($credentials['mail'])) {
throw new BadRequestHttpException('Missing credentials.name or credentials.mail');
}
// Load by name if provided.
$identifier = '';
if (isset($credentials['name'])) {
$identifier = $credentials['name'];
$users = $this->userStorage->loadByProperties(['name' => trim($identifier)]);
}
elseif (isset($credentials['mail'])) {
$identifier = $credentials['mail'];
$users = $this->userStorage->loadByProperties(['mail' => trim($identifier)]);
}
/** @var \Drupal\user\UserInterface $account */
$account = reset($users);
if ($account && $account->id()) {
if ($account->isBlocked()) {
$this->logger->error('Unable to send password reset email for blocked or not yet activated user %identifier.', [
'%identifier' => $identifier,
]);
return new Response();
}
// Send the password reset email.
$mail = _user_mail_notify('password_reset', $account);
if (empty($mail)) {
throw new BadRequestHttpException('Unable to send email. Contact the site administrator if the problem persists.');
}
else {
$this->logger->info('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
return new Response();
}
}
// Error if no users found with provided name or mail.
$this->logger->error('Unable to send password reset email for unrecognized username or email address %identifier.', [
'%identifier' => $identifier,
]);
return new Response();
}
/**
* Verifies if the user is blocked.
*
* @param string $name
* The username.
*
* @return bool
* TRUE if the user is blocked, otherwise FALSE.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. There
* is no replacement.
* @see https://www.drupal.org/node/3425340
*/
protected function userIsBlocked($name) {
@trigger_error(__METHOD__ . ' is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3425340', E_USER_DEPRECATED);
return user_is_blocked($name);
}
/**
* Finalizes the user login.
*
* @param \Drupal\user\UserInterface $user
* The user.
*/
protected function userLoginFinalize(UserInterface $user) {
user_login_finalize($user);
}
/**
* Logs out a user.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*/
public function logout() {
$this->userLogout();
return new Response(NULL, 204);
}
/**
* Logs the user out.
*/
protected function userLogout() {
user_logout();
}
/**
* Checks whether a user is logged in or not.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response.
*/
public function loginStatus() {
if ($this->currentUser()->isAuthenticated()) {
$response = new Response(self::LOGGED_IN);
}
else {
$response = new Response(self::LOGGED_OUT);
}
$response->headers->set('Content-Type', 'text/plain');
return $response;
}
/**
* Gets the format of the current request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return string
* The format of the request.
*/
protected function getRequestFormat(Request $request) {
$format = $request->getRequestFormat();
if (!in_array($format, $this->serializerFormats)) {
throw new BadRequestHttpException("Unrecognized format: $format.");
}
return $format;
}
/**
* Enforces flood control for the current login request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $username
* The user name sent for login credentials.
*/
protected function floodControl(Request $request, $username) {
$flood_config = $this->config('user.flood');
if (!$this->userFloodControl->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS);
}
if ($identifier = $this->getLoginFloodIdentifier($request, $username)) {
// Don't allow login if the limit for this user has been reached.
// Default is to allow 5 failed attempts every 6 hours.
if (!$this->userFloodControl->isAllowed('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
if ($flood_config->get('uid_only')) {
$error_message = sprintf('There have been more than %s failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', $flood_config->get('user_limit'));
}
else {
$error_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
}
throw new AccessDeniedHttpException($error_message, NULL, Response::HTTP_TOO_MANY_REQUESTS);
}
}
}
/**
* Gets the login identifier for user login flood control.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $username
* The username supplied in login credentials.
*
* @return string
* The login identifier or if the user does not exist an empty string.
*/
protected function getLoginFloodIdentifier(Request $request, $username) {
$flood_config = $this->config('user.flood');
$accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]);
if ($account = reset($accounts)) {
if ($flood_config->get('uid_only')) {
// Register flood events based on the uid only, so they apply for any
// IP address. This is the most secure option.
$identifier = $account->id();
}
else {
// The default identifier is a combination of uid and IP address. This
// is less secure but more resistant to denial-of-service attacks that
// could lock out all users with public user names.
$identifier = $account->id() . '-' . $request->getClientIp();
}
return $identifier;
}
return '';
}
}

View File

@@ -0,0 +1,446 @@
<?php
namespace Drupal\user\Controller;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Flood\FloodInterface;
use Drupal\Core\Url;
use Drupal\user\Form\UserPasswordResetForm;
use Drupal\user\UserDataInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Controller routines for user routes.
*/
class UserController extends ControllerBase {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The user data service.
*
* @var \Drupal\user\UserDataInterface
*/
protected $userData;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The flood service.
*
* @var \Drupal\Core\Flood\FloodInterface
*/
protected $flood;
/**
* Constructs a UserController object.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\user\UserDataInterface $user_data
* The user data service.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Flood\FloodInterface $flood
* The flood service.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(
DateFormatterInterface $date_formatter,
UserStorageInterface $user_storage,
UserDataInterface $user_data,
LoggerInterface $logger,
FloodInterface $flood,
protected ?TimeInterface $time = NULL,
) {
$this->dateFormatter = $date_formatter;
$this->userStorage = $user_storage;
$this->userData = $user_data;
$this->logger = $logger;
$this->flood = $flood;
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('user'),
$container->get('user.data'),
$container->get('logger.factory')->get('user'),
$container->get('flood'),
$container->get('datetime.time'),
);
}
/**
* Redirects to the user password reset form.
*
* In order to never disclose a reset link via a referrer header this
* controller must always return a redirect response.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
* @param int $uid
* User ID of the user requesting reset.
* @param int $timestamp
* The current timestamp.
* @param string $hash
* Login link hash.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* The redirect response.
*/
public function resetPass(Request $request, $uid, $timestamp, $hash) {
$account = $this->currentUser();
// When processing the one-time login link, we have to make sure that a user
// isn't already logged in.
if ($account->isAuthenticated()) {
// The current user is already logged in.
if ($account->id() == $uid) {
user_logout();
// We need to begin the redirect process again because logging out will
// destroy the session.
return $this->redirect(
'user.reset',
[
'uid' => $uid,
'timestamp' => $timestamp,
'hash' => $hash,
]
);
}
// A different user is already logged in on the computer.
else {
/** @var \Drupal\user\UserInterface $reset_link_user */
$reset_link_user = $this->userStorage->load($uid);
if ($reset_link_user && $this->validatePathParameters($reset_link_user, $timestamp, $hash)) {
$this->messenger()
->addWarning($this->t('Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user. <a href=":logout">Log out</a> and try using the link again.',
[
'%other_user' => $account->getAccountName(),
'%resetting_user' => $reset_link_user->getAccountName(),
':logout' => Url::fromRoute('user.logout')->toString(),
]));
}
else {
// Invalid one-time link specifies an unknown user.
$this->messenger()->addError($this->t('The one-time login link you clicked is invalid.'));
}
return $this->redirect('<front>');
}
}
/** @var \Drupal\user\UserInterface $reset_link_user */
$reset_link_user = $this->userStorage->load($uid);
if ($redirect = $this->determineErrorRedirect($reset_link_user, $timestamp, $hash)) {
return $redirect;
}
$session = $request->getSession();
$session->set('pass_reset_hash', $hash);
$session->set('pass_reset_timeout', $timestamp);
return $this->redirect(
'user.reset.form',
['uid' => $uid]
);
}
/**
* Returns the user password reset form.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
* @param int $uid
* User ID of the user requesting reset.
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
* The form structure or a redirect response.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* If the pass_reset_timeout or pass_reset_hash are not available in the
* session. Or if $uid is for a blocked user or invalid user ID.
*/
public function getResetPassForm(Request $request, $uid) {
$session = $request->getSession();
$timestamp = $session->get('pass_reset_timeout');
$hash = $session->get('pass_reset_hash');
// As soon as the session variables are used they are removed to prevent the
// hash and timestamp from being leaked unexpectedly. This could occur if
// the user does not click on the log in button on the form.
$session->remove('pass_reset_timeout');
$session->remove('pass_reset_hash');
if (!$hash || !$timestamp) {
throw new AccessDeniedHttpException();
}
/** @var \Drupal\user\UserInterface $user */
$user = $this->userStorage->load($uid);
if ($user === NULL || !$user->isActive()) {
// Blocked or invalid user ID, so deny access. The parameters will be in
// the watchdog's URL for the administrator to check.
throw new AccessDeniedHttpException();
}
// Time out, in seconds, until login URL expires.
$timeout = $this->config('user.settings')->get('password_reset_timeout');
$expiration_date = $user->getLastLoginTime() ? $this->dateFormatter->format($timestamp + $timeout) : NULL;
return $this->formBuilder()->getForm(UserPasswordResetForm::class, $user, $expiration_date, $timestamp, $hash);
}
/**
* Validates user, hash, and timestamp; logs the user in if correct.
*
* @param int $uid
* User ID of the user requesting reset.
* @param int $timestamp
* The current timestamp.
* @param string $hash
* Login link hash.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Returns a redirect to the user edit form if the information is correct.
* If the information is incorrect redirects to 'user.pass' route with a
* message for the user.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* If $uid is for a blocked user or invalid user ID.
*/
public function resetPassLogin($uid, $timestamp, $hash, Request $request) {
/** @var \Drupal\user\UserInterface $user */
$user = $this->userStorage->load($uid);
if ($redirect = $this->determineErrorRedirect($user, $timestamp, $hash)) {
return $redirect;
}
$flood_config = $this->config('user.flood');
if ($flood_config->get('uid_only')) {
$identifier = $user->id();
}
else {
$identifier = $user->id() . '-' . $request->getClientIP();
}
$this->flood->clear('user.failed_login_user', $identifier);
$this->flood->clear('user.http_login', $identifier);
user_login_finalize($user);
$this->logger->info('User %name used one-time login link at time %timestamp.', ['%name' => $user->getDisplayName(), '%timestamp' => $timestamp]);
$this->messenger()->addStatus($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. It is recommended that you set your password.'));
// Let the user's password be changed without the current password
// check.
$token = Crypt::randomBytesBase64(55);
$request->getSession()->set('pass_reset_' . $user->id(), $token);
// Clear any flood events for this user.
$this->flood->clear('user.password_request_user', $uid);
return $this->redirect(
'entity.user.edit_form',
['user' => $user->id()],
[
'query' => ['pass-reset-token' => $token],
'absolute' => TRUE,
]
);
}
/**
* Validates user, hash, and timestamp.
*
* This method allows the 'user.reset' and 'user.reset.login' routes to use
* the same logic to check the user, timestamp and hash and redirect to the
* same location with the same messages.
*
* @param \Drupal\user\UserInterface|null $user
* User requesting reset. NULL if the user does not exist.
* @param int $timestamp
* The current timestamp.
* @param string $hash
* Login link hash.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|null
* Returns a redirect if the information is incorrect. It redirects to
* 'user.pass' route with a message for the user.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* If $uid is for a blocked user or invalid user ID.
*/
protected function determineErrorRedirect(?UserInterface $user, int $timestamp, string $hash): ?RedirectResponse {
// The current user is not logged in, so check the parameters.
$current = $this->time->getRequestTime();
// Verify that the user exists and is active.
if ($user === NULL || !$user->isActive()) {
// Blocked or invalid user ID, so deny access. The parameters will be in
// the watchdog's URL for the administrator to check.
throw new AccessDeniedHttpException();
}
// Time out, in seconds, until login URL expires.
$timeout = $this->config('user.settings')->get('password_reset_timeout');
// No time out for first time login.
if ($user->getLastLoginTime() && $current - $timestamp > $timeout) {
$this->messenger()->addError($this->t('You have tried to use a one-time login link that has expired. Request a new one using the form below.'));
return $this->redirect('user.pass');
}
elseif ($user->isAuthenticated() && $this->validatePathParameters($user, $timestamp, $hash, $timeout)) {
// The information provided is valid.
return NULL;
}
$this->messenger()->addError($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Request a new one using the form below.'));
return $this->redirect('user.pass');
}
/**
* Validates hash and timestamp.
*
* @param \Drupal\user\UserInterface $user
* User requesting reset.
* @param int $timestamp
* The timestamp.
* @param string $hash
* Login link hash.
* @param int $timeout
* Link expiration timeout.
*
* @return bool
* Whether the provided data are valid.
*/
protected function validatePathParameters(UserInterface $user, int $timestamp, string $hash, int $timeout = 0): bool {
$current = \Drupal::time()->getRequestTime();
$timeout_valid = ((!empty($timeout) && $current - $timestamp < $timeout) || empty($timeout));
return ($timestamp >= $user->getLastLoginTime()) && $timestamp <= $current && $timeout_valid && hash_equals($hash, user_pass_rehash($user, $timestamp));
}
/**
* Redirects users to their profile page.
*
* This controller assumes that it is only invoked for authenticated users.
* This is enforced for the 'user.page' route with the '_user_is_logged_in'
* requirement.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Returns a redirect to the profile of the currently logged in user.
*/
public function userPage() {
return $this->redirect('entity.user.canonical', ['user' => $this->currentUser()->id()]);
}
/**
* Redirects users to their profile edit page.
*
* This controller assumes that it is only invoked for authenticated users.
* This is typically enforced with the '_user_is_logged_in' requirement.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* Returns a redirect to the profile edit form of the currently logged in
* user.
*/
public function userEditPage() {
return $this->redirect('entity.user.edit_form', ['user' => $this->currentUser()->id()], [], 302);
}
/**
* Route title callback.
*
* @param \Drupal\user\UserInterface $user
* The user account.
*
* @return string|array
* The user account name as a render array or an empty string if $user is
* NULL.
*/
public function userTitle(?UserInterface $user = NULL) {
return $user ? ['#markup' => $user->getDisplayName(), '#allowed_tags' => Xss::getHtmlTagList()] : '';
}
/**
* Logs the current user out.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirection to home page.
*/
public function logout() {
if ($this->currentUser()->isAuthenticated()) {
user_logout();
}
return $this->redirect('<front>');
}
/**
* Confirms cancelling a user account via an email link.
*
* @param \Drupal\user\UserInterface $user
* The user account.
* @param int $timestamp
* The timestamp.
* @param string $hashed_pass
* The hashed password.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response.
*/
public function confirmCancel(UserInterface $user, $timestamp = 0, $hashed_pass = '') {
// Time out in seconds until cancel URL expires; 24 hours = 86400 seconds.
$timeout = 86400;
// Basic validation of arguments.
$account_data = $this->userData->get('user', $user->id());
if (isset($account_data['cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
// Validate expiration and hashed password/login.
if ($user->id() && $this->validatePathParameters($user, $timestamp, $hashed_pass, $timeout)) {
$edit = [
'user_cancel_notify' => $account_data['cancel_notify'] ?? $this->config('user.settings')->get('notify.status_canceled'),
];
user_cancel($edit, $user->id(), $account_data['cancel_method']);
// Since user_cancel() is not invoked via Form API, batch processing
// needs to be invoked manually and should redirect to the front page
// after completion.
return batch_process('<front>');
}
else {
$this->messenger()->addError($this->t('You have tried to use an account cancellation link that has expired. Request a new one using the form below.'));
return $this->redirect('entity.user.cancel_form', ['user' => $user->id()], ['absolute' => TRUE]);
}
}
throw new AccessDeniedHttpException();
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides routes for the entity permissions form.
*
* Use this class or EntityPermissionsRouteProviderWithCheck as a route
* provider for an entity type such as Vocabulary. Either one will provide
* routes for the entity permissions form. The
* EntityPermissionsRouteProviderWithCheck class provides a custom access check:
* it denies access if there are no entity-specific permissions. If you know
* that each entity has permissions, or if the check is too expensive, then use
* this class.
*/
class EntityPermissionsRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new EntityPermissionsRouteProvider.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$collection = new RouteCollection();
$entity_type_id = $entity_type->id();
if ($entity_permissions_route = $this->getEntityPermissionsRoute($entity_type)) {
$collection->add("entity.$entity_type_id.entity_permissions_form", $entity_permissions_route);
}
return $collection;
}
/**
* Gets the entity permissions route.
*
* Built only for entity types that are bundles of other entity types and
* define the 'entity-permissions-form' link template.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return \Symfony\Component\Routing\Route|null
* The generated route, if available.
*/
protected function getEntityPermissionsRoute(EntityTypeInterface $entity_type): ?Route {
if (!$entity_type->hasLinkTemplate('entity-permissions-form')) {
return NULL;
}
if (!$bundle_of_id = $entity_type->getBundleOf()) {
return NULL;
}
$entity_type_id = $entity_type->id();
$route = new Route(
$entity_type->getLinkTemplate('entity-permissions-form'),
[
'_title' => 'Manage permissions',
'_form' => 'Drupal\user\Form\EntityPermissionsForm',
'entity_type_id' => $bundle_of_id,
'bundle_entity_type' => $entity_type_id,
],
[
'_permission' => 'administer permissions',
],
[
// Indicate that Drupal\Core\Entity\Enhancer\EntityBundleRouteEnhancer should
// set the bundle parameter.
'_field_ui' => TRUE,
'parameters' => [
$entity_type_id => [
'type' => "entity:$entity_type_id",
'with_config_overrides' => TRUE,
],
],
'_admin_route' => TRUE,
]
);
return $route;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\Routing\Route;
/**
* Provides routes for the entity permissions form with a custom access check.
*
* Use this class or EntityPermissionsRouteProvider as a route provider for an
* entity type such as BlockContentType. Either one will provide routes for the
* entity permissions form. This class provides a custom access check: it denies
* access if there are no entity-specific permissions. If you know that each
* entity has permissions, or if the check is too expensive, then use
* EntityPermissionsRouteProvider instead of this class.
*/
class EntityPermissionsRouteProviderWithCheck extends EntityPermissionsRouteProvider {
/**
* {@inheritdoc}
*/
protected function getEntityPermissionsRoute(EntityTypeInterface $entity_type): ?Route {
$route = parent::getEntityPermissionsRoute($entity_type);
if ($route) {
$route->setRequirement('_custom_access', '\Drupal\user\Form\EntityPermissionsForm::access');
}
return $route;
}
}

View File

@@ -0,0 +1,268 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Config\Action\Attribute\ActionMethod;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\RoleInterface;
/**
* Defines the user role entity class.
*
* @ConfigEntityType(
* id = "user_role",
* label = @Translation("Role"),
* label_collection = @Translation("Roles"),
* label_singular = @Translation("role"),
* label_plural = @Translation("roles"),
* label_count = @PluralTranslation(
* singular = "@count role",
* plural = "@count roles",
* ),
* handlers = {
* "storage" = "Drupal\user\RoleStorage",
* "access" = "Drupal\user\RoleAccessControlHandler",
* "list_builder" = "Drupal\user\RoleListBuilder",
* "form" = {
* "default" = "Drupal\user\RoleForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm"
* }
* },
* admin_permission = "administer permissions",
* config_prefix = "role",
* static_cache = TRUE,
* entity_keys = {
* "id" = "id",
* "weight" = "weight",
* "label" = "label"
* },
* links = {
* "delete-form" = "/admin/people/roles/manage/{user_role}/delete",
* "edit-form" = "/admin/people/roles/manage/{user_role}",
* "edit-permissions-form" = "/admin/people/permissions/{user_role}",
* "collection" = "/admin/people/roles",
* },
* config_export = {
* "id",
* "label",
* "weight",
* "is_admin",
* "permissions",
* }
* )
*/
class Role extends ConfigEntityBase implements RoleInterface {
/**
* The machine name of this role.
*
* @var string
*/
protected $id;
/**
* The human-readable label of this role.
*
* @var string
*/
protected $label;
/**
* The weight of this role in administrative listings.
*
* @var int
*/
protected $weight;
/**
* The permissions belonging to this role.
*
* @var array
*/
protected $permissions = [];
/**
* An indicator whether the role has all permissions.
*
* @var bool
*/
protected $is_admin;
/**
* {@inheritdoc}
*/
public function getPermissions() {
if ($this->isAdmin()) {
return [];
}
return $this->permissions;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->get('weight');
}
/**
* {@inheritdoc}
*/
public function setWeight($weight) {
$this->set('weight', $weight);
return $this;
}
/**
* {@inheritdoc}
*/
public function hasPermission($permission) {
if ($this->isAdmin()) {
return TRUE;
}
return in_array($permission, $this->permissions);
}
/**
* {@inheritdoc}
*/
#[ActionMethod(adminLabel: new TranslatableMarkup('Add permission to role'))]
public function grantPermission($permission) {
if ($this->isAdmin()) {
return $this;
}
if (!$this->hasPermission($permission)) {
$this->permissions[] = $permission;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function revokePermission($permission) {
if ($this->isAdmin()) {
return $this;
}
$this->permissions = array_diff($this->permissions, [$permission]);
return $this;
}
/**
* {@inheritdoc}
*/
public function isAdmin() {
return (bool) $this->is_admin;
}
/**
* {@inheritdoc}
*/
public function setIsAdmin($is_admin) {
$this->is_admin = $is_admin;
return $this;
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageInterface $storage, array &$entities) {
parent::postLoad($storage, $entities);
// Sort the queried roles by their weight.
// See \Drupal\Core\Config\Entity\ConfigEntityBase::sort().
uasort($entities, [static::class, 'sort']);
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
if (!isset($this->weight) && ($roles = $storage->loadMultiple())) {
// Set a role weight to make this new role last.
$max = array_reduce($roles, function ($max, $role) {
return $max > $role->weight ? $max : $role->weight;
});
$this->weight = $max + 1;
}
if (!$this->isSyncing() && $this->hasTrustedData()) {
// Permissions are always ordered alphabetically to avoid conflicts in the
// exported configuration. If the save is not trusted then the
// configuration will be sorted by StorableConfigBase.
sort($this->permissions);
}
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
// Load all permission definitions.
$permission_definitions = \Drupal::service('user.permissions')->getPermissions();
$valid_permissions = array_intersect($this->permissions, array_keys($permission_definitions));
$invalid_permissions = array_diff($this->permissions, $valid_permissions);
if (!empty($invalid_permissions)) {
throw new \RuntimeException('Adding non-existent permissions to a role is not allowed. The incorrect permissions are "' . implode('", "', $invalid_permissions) . '".');
}
foreach ($valid_permissions as $permission) {
// Depend on the module that is providing this permissions.
$this->addDependency('module', $permission_definitions[$permission]['provider']);
// Depend on any other dependencies defined by permissions granted to
// this role.
if (!empty($permission_definitions[$permission]['dependencies'])) {
$this->addDependencies($permission_definitions[$permission]['dependencies']);
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
// Load all permission definitions.
$permission_definitions = \Drupal::service('user.permissions')->getPermissions();
// Convert config and content entity dependencies to a list of names to make
// it easier to check.
foreach (['content', 'config'] as $type) {
$dependencies[$type] = array_keys($dependencies[$type]);
}
// Remove any permissions from the role that are dependent on anything being
// deleted or uninstalled.
foreach ($this->permissions as $key => $permission) {
if (!isset($permission_definitions[$permission])) {
// If the permission is not defined then there's nothing we can do.
continue;
}
if (in_array($permission_definitions[$permission]['provider'], $dependencies['module'], TRUE)) {
unset($this->permissions[$key]);
$changed = TRUE;
// Process the next permission.
continue;
}
if (isset($permission_definitions[$permission]['dependencies'])) {
foreach ($permission_definitions[$permission]['dependencies'] as $type => $list) {
if (array_intersect($list, $dependencies[$type])) {
unset($this->permissions[$key]);
$changed = TRUE;
// Process the next permission.
continue 2;
}
}
}
}
return $changed;
}
}

View File

@@ -0,0 +1,603 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Flood\PrefixFloodInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\user\RoleInterface;
use Drupal\user\StatusItem;
use Drupal\user\TimeZoneItem;
use Drupal\user\UserInterface;
/**
* Defines the user entity class.
*
* The base table name here is plural, despite Drupal table naming standards,
* because "user" is a reserved word in many databases.
*
* @ContentEntityType(
* id = "user",
* label = @Translation("User"),
* label_collection = @Translation("Users"),
* label_singular = @Translation("user"),
* label_plural = @Translation("users"),
* label_count = @PluralTranslation(
* singular = "@count user",
* plural = "@count users",
* ),
* handlers = {
* "storage" = "Drupal\user\UserStorage",
* "storage_schema" = "Drupal\user\UserStorageSchema",
* "access" = "Drupal\user\UserAccessControlHandler",
* "list_builder" = "Drupal\user\UserListBuilder",
* "views_data" = "Drupal\user\UserViewsData",
* "route_provider" = {
* "html" = "Drupal\user\Entity\UserRouteProvider",
* },
* "form" = {
* "default" = "Drupal\user\ProfileForm",
* "cancel" = "Drupal\user\Form\UserCancelForm",
* "register" = "Drupal\user\RegisterForm"
* },
* "translation" = "Drupal\user\ProfileTranslationHandler"
* },
* admin_permission = "administer users",
* base_table = "users",
* data_table = "users_field_data",
* translatable = TRUE,
* entity_keys = {
* "id" = "uid",
* "langcode" = "langcode",
* "uuid" = "uuid"
* },
* links = {
* "canonical" = "/user/{user}",
* "edit-form" = "/user/{user}/edit",
* "cancel-form" = "/user/{user}/cancel",
* "collection" = "/admin/people",
* },
* field_ui_base_route = "entity.user.admin_form",
* common_reference_target = TRUE
* )
*/
class User extends ContentEntityBase implements UserInterface {
use EntityChangedTrait;
/**
* Stores a reference for a reusable anonymous user entity.
*
* @var \Drupal\user\UserInterface
*/
protected static $anonymousUser;
/**
* {@inheritdoc}
*/
public function isNew() {
return !empty($this->enforceIsNew) || $this->id() === NULL;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->getDisplayName();
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Make sure that the authenticated/anonymous roles are not persisted.
foreach ($this->get('roles') as $index => $item) {
if (in_array($item->target_id, [RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID])) {
$this->get('roles')->offsetUnset($index);
}
}
// Store account cancellation information.
foreach (['user_cancel_method', 'user_cancel_notify'] as $key) {
if (isset($this->{$key})) {
\Drupal::service('user.data')->set('user', $this->id(), substr($key, 5), $this->{$key});
}
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
if ($update) {
$session_manager = \Drupal::service('session_manager');
// If the password has been changed, delete all open sessions for the
// user and recreate the current one.
if ($this->pass->value != $this->original->pass->value) {
$session_manager->delete($this->id());
if ($this->id() == \Drupal::currentUser()->id()) {
\Drupal::service('session')->migrate();
}
$flood_config = \Drupal::config('user.flood');
$flood_service = \Drupal::flood();
$identifier = $this->id();
if ($flood_config->get('uid_only')) {
// Clear flood events based on the uid only if configured.
$flood_service->clear('user.failed_login_user', $identifier);
}
elseif ($flood_service instanceof PrefixFloodInterface) {
$flood_service->clearByPrefix('user.failed_login_user', $identifier);
}
}
// If the user was blocked, delete the user's sessions to force a logout.
if ($this->original->status->value != $this->status->value && $this->status->value == 0) {
$session_manager->delete($this->id());
}
// Send emails after we have the new user object.
if ($this->status->value != $this->original->status->value) {
// The user's status is changing; conditionally send notification email.
$op = $this->status->value == 1 ? 'status_activated' : 'status_blocked';
_user_mail_notify($op, $this);
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
$uids = array_keys($entities);
\Drupal::service('user.data')->delete(NULL, $uids);
}
/**
* {@inheritdoc}
*/
public function getRoles($exclude_locked_roles = FALSE) {
$roles = [];
// Users with an ID always have the authenticated user role.
if (!$exclude_locked_roles) {
if ($this->isAuthenticated()) {
$roles[] = RoleInterface::AUTHENTICATED_ID;
}
else {
$roles[] = RoleInterface::ANONYMOUS_ID;
}
}
foreach ($this->get('roles') as $role) {
if ($role->target_id) {
$roles[] = $role->target_id;
}
}
return $roles;
}
/**
* {@inheritdoc}
*/
public function hasRole($rid) {
return in_array($rid, $this->getRoles());
}
/**
* {@inheritdoc}
*/
public function addRole($rid) {
if (in_array($rid, [RoleInterface::AUTHENTICATED_ID, RoleInterface::ANONYMOUS_ID])) {
throw new \InvalidArgumentException('Anonymous or authenticated role ID must not be assigned manually.');
}
$roles = $this->getRoles(TRUE);
$roles[] = $rid;
$this->set('roles', array_unique($roles));
return $this;
}
/**
* {@inheritdoc}
*/
public function removeRole($rid) {
$this->set('roles', array_diff($this->getRoles(TRUE), [$rid]));
return $this;
}
/**
* {@inheritdoc}
*/
public function hasPermission(/* string */$permission) {
if (!is_string($permission)) {
@trigger_error('Calling ' . __METHOD__ . '() with a $permission parameter of type other than string is deprecated in drupal:10.3.0 and will cause an error in drupal:11.0.0. See https://www.drupal.org/node/3411485', E_USER_DEPRECATED);
return FALSE;
}
return \Drupal::service('permission_checker')->hasPermission($permission, $this);
}
/**
* {@inheritdoc}
*/
public function getPassword() {
return $this->get('pass')->value;
}
/**
* {@inheritdoc}
*/
public function setPassword(#[\SensitiveParameter] $password) {
$this->get('pass')->value = $password;
return $this;
}
/**
* {@inheritdoc}
*/
public function getEmail() {
return $this->get('mail')->value;
}
/**
* {@inheritdoc}
*/
public function setEmail($mail) {
$this->get('mail')->value = $mail;
return $this;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
return $this->get('created')->value;
}
/**
* {@inheritdoc}
*/
public function getLastAccessedTime() {
return $this->get('access')->value;
}
/**
* {@inheritdoc}
*/
public function setLastAccessTime($timestamp) {
$this->get('access')->value = $timestamp;
return $this;
}
/**
* {@inheritdoc}
*/
public function getLastLoginTime() {
return $this->get('login')->value;
}
/**
* {@inheritdoc}
*/
public function setLastLoginTime($timestamp) {
$this->get('login')->value = $timestamp;
return $this;
}
/**
* {@inheritdoc}
*/
public function isActive() {
return $this->get('status')->value == 1;
}
/**
* {@inheritdoc}
*/
public function isBlocked() {
return $this->get('status')->value == 0;
}
/**
* {@inheritdoc}
*/
public function activate() {
if ($this->isAnonymous()) {
throw new \LogicException('The anonymous user account should remain blocked at all times.');
}
$this->get('status')->value = 1;
return $this;
}
/**
* {@inheritdoc}
*/
public function block() {
$this->get('status')->value = 0;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTimeZone() {
return $this->get('timezone')->value;
}
/**
* {@inheritdoc}
*/
public function getPreferredLangcode($fallback_to_default = TRUE) {
$language_list = $this->languageManager()->getLanguages();
$preferred_langcode = $this->get('preferred_langcode')->value;
if (!empty($preferred_langcode) && isset($language_list[$preferred_langcode])) {
return $language_list[$preferred_langcode]->getId();
}
else {
return $fallback_to_default ? $this->languageManager()->getDefaultLanguage()->getId() : '';
}
}
/**
* {@inheritdoc}
*/
public function getPreferredAdminLangcode($fallback_to_default = TRUE) {
$language_list = $this->languageManager()->getLanguages();
$preferred_langcode = $this->get('preferred_admin_langcode')->value;
if (!empty($preferred_langcode) && isset($language_list[$preferred_langcode])) {
return $language_list[$preferred_langcode]->getId();
}
else {
return $fallback_to_default ? $this->languageManager()->getDefaultLanguage()->getId() : '';
}
}
/**
* {@inheritdoc}
*/
public function getInitialEmail() {
return $this->get('init')->value;
}
/**
* {@inheritdoc}
*/
public function isAuthenticated() {
return $this->id() > 0;
}
/**
* {@inheritdoc}
*/
public function isAnonymous() {
return $this->id() === 0 || $this->id() === '0';
}
/**
* {@inheritdoc}
*/
public function getAccountName() {
return $this->get('name')->value ?: '';
}
/**
* {@inheritdoc}
*/
public function getDisplayName() {
$name = $this->getAccountName() ?: \Drupal::config('user.settings')->get('anonymous');
\Drupal::moduleHandler()->alter('user_format_name', $name, $this);
return $name;
}
/**
* {@inheritdoc}
*/
public function setUsername($username) {
$this->set('name', $username);
return $this;
}
/**
* {@inheritdoc}
*/
public function setExistingPassword(#[\SensitiveParameter] $password) {
$this->get('pass')->existing = $password;
return $this;
}
/**
* {@inheritdoc}
*/
public function checkExistingPassword(UserInterface $account_unchanged) {
$existing = $this->get('pass')->existing;
return $existing !== NULL && strlen($existing) > 0 &&
\Drupal::service('password')->check(trim($existing), $account_unchanged->getPassword());
}
/**
* Returns an anonymous user entity.
*
* @return \Drupal\user\UserInterface
* An anonymous user entity.
*/
public static function getAnonymousUser() {
if (!isset(static::$anonymousUser)) {
// @todo Use the entity factory once available, see
// https://www.drupal.org/node/1867228.
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type = $entity_type_manager->getDefinition('user');
$class = $entity_type->getClass();
static::$anonymousUser = new $class([
'uid' => [LanguageInterface::LANGCODE_DEFAULT => 0],
'name' => [LanguageInterface::LANGCODE_DEFAULT => ''],
// Explicitly set the langcode to ensure that field definitions do not
// need to be fetched to figure out a default.
'langcode' => [LanguageInterface::LANGCODE_DEFAULT => LanguageInterface::LANGCODE_NOT_SPECIFIED],
], $entity_type->id());
}
return clone static::$anonymousUser;
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
$fields['uid']->setLabel(t('User ID'))
->setDescription(t('The user ID.'));
$fields['uuid']->setDescription(t('The user UUID.'));
$fields['langcode']->setLabel(t('Language code'))
->setDescription(t('The user language code.'))
->setDisplayOptions('form', ['region' => 'hidden']);
$fields['preferred_langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Preferred language code'))
->setDescription(t("The user's preferred language code for receiving emails and viewing the site."))
// @todo Define this via an options provider once
// https://www.drupal.org/node/2329937 is completed.
->addPropertyConstraints('value', [
'AllowedValues' => ['callback' => __CLASS__ . '::getAllowedConfigurableLanguageCodes'],
]);
$fields['preferred_admin_langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Preferred admin language code'))
->setDescription(t("The user's preferred language code for viewing administration pages."))
// @todo A default value of NULL is ignored, so we have to specify
// an empty field item structure instead. Fix this in
// https://www.drupal.org/node/2318605.
->setDefaultValue([0 => ['value' => NULL]])
// @todo Define this via an options provider once
// https://www.drupal.org/node/2329937 is completed.
->addPropertyConstraints('value', [
'AllowedValues' => ['callback' => __CLASS__ . '::getAllowedConfigurableLanguageCodes'],
]);
// The name should not vary per language. The username is the visual
// identifier for a user and needs to be consistent in all languages.
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('The name of this user.'))
->setRequired(TRUE)
->setConstraints([
// No Length constraint here because the UserName constraint also covers
// that.
'UserName' => [],
'UserNameUnique' => [],
]);
$fields['name']->getItemDefinition()->setClass('\Drupal\user\UserNameItem');
$fields['pass'] = BaseFieldDefinition::create('password')
->setLabel(t('Password'))
->setDescription(t('The password of this user (hashed).'))
->addConstraint('ProtectedUserField');
$fields['mail'] = BaseFieldDefinition::create('email')
->setLabel(t('Email'))
->setDescription(t('The email of this user.'))
->setDefaultValue('')
->addConstraint('UserMailUnique')
->addConstraint('UserMailRequired')
->addConstraint('ProtectedUserField');
$fields['timezone'] = BaseFieldDefinition::create('string')
->setLabel(t('Timezone'))
->setDescription(t('The timezone of this user.'))
->setSetting('max_length', 32)
// @todo Define this via an options provider once
// https://www.drupal.org/node/2329937 is completed.
->addPropertyConstraints('value', [
'AllowedValues' => ['callback' => __CLASS__ . '::getAllowedTimezones'],
]);
$fields['timezone']->getItemDefinition()->setClass(TimeZoneItem::class);
$fields['status'] = BaseFieldDefinition::create('boolean')
->setLabel(t('User status'))
->setDescription(t('Whether the user is active or blocked.'))
->setDefaultValue(FALSE);
$fields['status']->getItemDefinition()->setClass(StatusItem::class);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time that the user was created.'));
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the user was last edited.'))
->setTranslatable(TRUE);
$fields['access'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Last access'))
->setDescription(t('The time that the user last accessed the site.'))
->setDefaultValue(0);
$fields['login'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Last login'))
->setDescription(t('The time that the user last logged in.'))
->setDefaultValue(0);
$fields['init'] = BaseFieldDefinition::create('email')
->setLabel(t('Initial email'))
->setDescription(t('The email address used for initial account creation.'))
->setDefaultValue('');
$fields['roles'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Roles'))
->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
->setDescription(t('The roles the user has.'))
->setSetting('target_type', 'user_role');
return $fields;
}
/**
* Returns the role storage object.
*
* @return \Drupal\user\RoleStorageInterface
* The role storage object.
*/
protected function getRoleStorage() {
return \Drupal::entityTypeManager()->getStorage('user_role');
}
/**
* Defines allowed timezones for the field's AllowedValues constraint.
*
* @return string[]
* The allowed values.
*/
public static function getAllowedTimezones() {
return \DateTimeZone::listIdentifiers();
}
/**
* Defines allowed configurable language codes for AllowedValues constraints.
*
* @return string[]
* The allowed values.
*/
public static function getAllowedConfigurableLanguageCodes() {
return array_keys(\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_CONFIGURABLE));
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\user\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides routes for the user entity.
*/
class UserRouteProvider implements EntityRouteProviderInterface {
/**
* {@inheritdoc}
*/
public function getRoutes(EntityTypeInterface $entity_type) {
$route_collection = new RouteCollection();
$route = (new Route('/user/{user}'))
->setDefaults([
'_entity_view' => 'user.full',
'_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
])
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.view');
$route_collection->add('entity.user.canonical', $route);
$route = (new Route('/user/{user}/edit'))
->setDefaults([
'_entity_form' => 'user.default',
'_title_callback' => 'Drupal\user\Controller\UserController::userTitle',
])
->setOption('_admin_route', TRUE)
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.update');
$route_collection->add('entity.user.edit_form', $route);
$route = (new Route('/user/{user}/cancel'))
->setDefaults([
'_title' => 'Cancel account',
'_entity_form' => 'user.cancel',
])
->setOption('_admin_route', TRUE)
->setRequirement('user', '\d+')
->setRequirement('_entity_access', 'user.delete');
$route_collection->add('entity.user.cancel_form', $route);
return $route_collection;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\user;
/**
* Defines a common interface for entities that have an owner.
*
* An owner is someone who has primary control over an entity, similar to
* owners in Unix file system access. This may or may not be the entity's
* original author. The owner may also have less permissions than other users,
* such as administrators.
*
* @ingroup entity_type_characteristics
*/
interface EntityOwnerInterface {
/**
* Returns the entity owner's user entity.
*
* @return \Drupal\user\UserInterface
* The owner user entity.
*/
public function getOwner();
/**
* Sets the entity owner's user entity.
*
* @param \Drupal\user\UserInterface $account
* The owner user entity.
*
* @return $this
*/
public function setOwner(UserInterface $account);
/**
* Returns the entity owner's user ID.
*
* @return int|null
* The owner user ID, or NULL in case the user ID field has not been set on
* the entity.
*/
public function getOwnerId();
/**
* Sets the entity owner's user ID.
*
* @param int $uid
* The owner user id.
*
* @return $this
*/
public function setOwnerId($uid);
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\user;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a trait for entities that have an owner.
*/
trait EntityOwnerTrait {
/**
* Returns an array of base field definitions for entity owners.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to add the owner field to.
*
* @return \Drupal\Core\Field\BaseFieldDefinition[]
* An array of base field definitions.
*
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
* Thrown when the entity type does not implement EntityOwnerInterface or
* if it does not have an "owner" entity key.
*/
public static function ownerBaseFieldDefinitions(EntityTypeInterface $entity_type) {
if (!is_subclass_of($entity_type->getClass(), EntityOwnerInterface::class)) {
throw new UnsupportedEntityTypeDefinitionException('The entity type ' . $entity_type->id() . ' does not implement \Drupal\user\EntityOwnerInterface.');
}
if (!$entity_type->hasKey('owner')) {
throw new UnsupportedEntityTypeDefinitionException('The entity type ' . $entity_type->id() . ' does not have an "owner" entity key.');
}
return [
$entity_type->getKey('owner') => BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('User ID'))
->setSetting('target_type', 'user')
->setTranslatable($entity_type->isTranslatable())
->setDefaultValueCallback(static::class . '::getDefaultEntityOwner'),
];
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->getEntityKey('owner');
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
$key = $this->getEntityType()->getKey('owner');
$this->set($key, $uid);
return $this;
}
/**
* {@inheritdoc}
*/
public function getOwner() {
$key = $this->getEntityType()->getKey('owner');
return $this->get($key)->entity;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
$key = $this->getEntityType()->getKey('owner');
$this->set($key, $account);
return $this;
}
/**
* Default value callback for 'owner' base field.
*
* @return mixed
* A default value for the owner field.
*/
public static function getDefaultEntityOwner() {
return \Drupal::currentUser()->id();
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\user\Event;
/**
* Defines events for the user module.
*/
final class UserEvents {
/**
* The name of the event fired when a login is blocked by flood control.
*
* This event allows modules to perform an action whenever flood control has
* been triggered by excessive login attempts for a particular user account.
* The event listener method receives a \Drupal\user\Event\UserFloodEvent
* instance.
*
* @Event
*
* @see: \Drupal\user\UserFloodControl::isAllowed
* @see: \Drupal\user\EventSubscriber\UserFloodSubscriber
*
* @var string
*/
const FLOOD_BLOCKED_USER = 'user.flood_blocked_user';
/**
* The name of the event fired when a login is blocked by flood control.
*
* This event allows modules to perform an action whenever flood control has
* been triggered by excessive login attempts from a particular IP. The event
* listener method receives a \Drupal\user\Event\UserFloodEvent instance.
*
* @Event
*
* @see: \Drupal\user\UserFloodControl::isAllowed
* @see: \Drupal\user\EventSubscriber\UserFloodSubscriber
*
* @var string
*/
const FLOOD_BLOCKED_IP = 'user.flood_blocked_ip';
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Drupal\user\Event;
use Drupal\Component\EventDispatcher\Event;
/**
* Provides a user flood event for event listeners.
*/
class UserFloodEvent extends Event {
/**
* Flood event name.
*
* @var string
*/
protected $name;
/**
* Flood event threshold.
*
* @var int
*/
protected $threshold;
/**
* Flood event window.
*
* @var int
*/
protected $window;
/**
* Flood event identifier.
*
* @var string
*/
protected $identifier;
/**
* Flood event uid.
*
* @var int
*/
protected $uid = NULL;
/**
* Flood event IP.
*
* @var string
*/
protected $ip = NULL;
/**
* Constructs a user flood event object.
*
* @param string $name
* The name of the flood event.
* @param int $threshold
* The threshold for the flood event.
* @param int $window
* The window for the flood event.
* @param string $identifier
* The identifier of the flood event.
*/
public function __construct($name, $threshold, $window, $identifier) {
$this->name = $name;
$this->threshold = $threshold;
$this->window = $window;
$this->identifier = $identifier;
// The identifier could be a uid or an IP, or a composite of both.
if (is_numeric($identifier)) {
$this->uid = $identifier;
return;
}
if (str_contains($identifier, '-')) {
[$uid, $ip] = explode('-', $identifier);
$this->uid = $uid;
$this->ip = $ip;
return;
}
$this->ip = $identifier;
}
/**
* Gets the name of the user flood event object.
*
* @return string
* The name of the flood event.
*/
public function getName() {
return $this->name;
}
/**
* Gets the threshold for the user flood event object.
*
* @return int
* The threshold for the flood event.
*/
public function getThreshold() {
return $this->threshold;
}
/**
* Gets the window for the user flood event object.
*
* @return int
* The window for the flood event.
*/
public function getWindow() {
return $this->window;
}
/**
* Gets the identifier of the user flood event object.
*
* @return string
* The identifier of the flood event.
*/
public function getIdentifier() {
return $this->identifier;
}
/**
* Gets the IP of the user flood event object.
*
* @return string
* The IP of the flood event.
*/
public function getIp() {
return $this->ip;
}
/**
* Gets the uid of the user flood event object.
*
* @return int
* The uid of the flood event.
*/
public function getUid() {
return $this->uid;
}
/**
* Is the user flood event associated with an IP?
*
* @return bool
* Whether the event has an IP.
*/
public function hasIp() {
return !empty($this->ip);
}
/**
* Is the user flood event associated with a uid?
*
* @return bool
* Whether the event has a uid.
*/
public function hasUid() {
return !empty($this->uid);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Drupal\user\EventSubscriber;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Redirects users when access is denied.
*
* Anonymous users are taken to the login page when attempting to access the
* user profile pages. Authenticated users are redirected from the login form to
* their profile page and from the user registration form to their profile edit
* form.
*/
class AccessDeniedSubscriber implements EventSubscriberInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a new redirect subscriber.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(AccountInterface $account) {
$this->account = $account;
}
/**
* Redirects users when access is denied.
*
* @param \Symfony\Component\HttpKernel\Event\ExceptionEvent $event
* The event to process.
*/
public function onException(ExceptionEvent $event) {
$exception = $event->getThrowable();
if ($exception instanceof AccessDeniedHttpException) {
$route_name = RouteMatch::createFromRequest($event->getRequest())->getRouteName();
$redirect_url = NULL;
if ($this->account->isAuthenticated()) {
switch ($route_name) {
case 'user.login';
// Redirect an authenticated user to the profile page.
$redirect_url = Url::fromRoute('entity.user.canonical', ['user' => $this->account->id()], ['absolute' => TRUE]);
break;
case 'user.register';
// Redirect an authenticated user to the profile form.
$redirect_url = Url::fromRoute('entity.user.edit_form', ['user' => $this->account->id()], ['absolute' => TRUE]);
break;
}
}
elseif ($route_name === 'user.page') {
$redirect_url = Url::fromRoute('user.login', [], ['absolute' => TRUE]);
}
elseif (in_array($route_name, ['user.logout', 'user.logout.confirm'], TRUE)) {
$redirect_url = Url::fromRoute('<front>', [], ['absolute' => TRUE]);
}
if ($redirect_url) {
$event->setResponse(new RedirectResponse($redirect_url->toString()));
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Use a higher priority than
// \Drupal\Core\EventSubscriber\ExceptionLoggingSubscriber, because there's
// no need to log the exception if we can redirect.
$events[KernelEvents::EXCEPTION][] = ['onException', 75];
return $events;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Drupal\user\EventSubscriber;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\MaintenanceModeEvents;
use Drupal\Core\Site\MaintenanceModeInterface;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* Maintenance mode subscriber to log out users.
*/
class MaintenanceModeSubscriber implements EventSubscriberInterface {
/**
* The maintenance mode.
*
* @var \Drupal\Core\Site\MaintenanceMode
*/
protected $maintenanceMode;
/**
* The current account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a new MaintenanceModeSubscriber.
*
* @param \Drupal\Core\Site\MaintenanceModeInterface $maintenance_mode
* The maintenance mode.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(MaintenanceModeInterface $maintenance_mode, AccountInterface $account) {
$this->maintenanceMode = $maintenance_mode;
$this->account = $account;
}
/**
* Logout users if site is in maintenance mode and user is not exempt.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
* The event to process.
*/
public function onMaintenanceModeRequest(RequestEvent $event) {
// If the site is offline, log out unprivileged users.
if ($this->account->isAuthenticated()) {
user_logout();
// Redirect to homepage.
$event->setResponse(
new RedirectResponse(Url::fromRoute('<front>')->toString())
);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[MaintenanceModeEvents::MAINTENANCE_MODE_REQUEST][] = [
'onMaintenanceModeRequest',
-900,
];
return $events;
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Drupal\user\EventSubscriber;
use Drupal\user\Event\UserEvents;
use Drupal\user\Event\UserFloodEvent;
use Drupal\Core\Site\Settings;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Psr\Log\LoggerInterface;
/**
* Logs details of User Flood Control events.
*/
class UserFloodSubscriber implements EventSubscriberInterface {
/**
* The default logger service.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a UserFloodSubscriber.
*
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(?LoggerInterface $logger = NULL) {
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events[UserEvents::FLOOD_BLOCKED_USER][] = ['blockedUser'];
$events[UserEvents::FLOOD_BLOCKED_IP][] = ['blockedIp'];
return $events;
}
/**
* An attempt to login has been blocked based on user name.
*
* @param \Drupal\user\Event\UserFloodEvent $floodEvent
* The flood event.
*/
public function blockedUser(UserFloodEvent $floodEvent) {
if (Settings::get('log_user_flood', TRUE)) {
$uid = $floodEvent->getUid();
if ($floodEvent->hasIp()) {
$ip = $floodEvent->getIp();
$this->logger->notice('Flood control blocked login attempt for uid %uid from %ip', ['%uid' => $uid, '%ip' => $ip]);
return;
}
$this->logger->notice('Flood control blocked login attempt for uid %uid', ['%uid' => $uid]);
}
}
/**
* An attempt to login has been blocked based on IP.
*
* @param \Drupal\user\Event\UserFloodEvent $floodEvent
* The flood event.
*/
public function blockedIp(UserFloodEvent $floodEvent) {
if (Settings::get('log_user_flood', TRUE)) {
$this->logger->notice('Flood control blocked login attempt from %ip', ['%ip' => $floodEvent->getIp()]);
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Drupal\user\EventSubscriber;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Updates the current user's last access time.
*/
class UserRequestSubscriber implements EventSubscriberInterface {
/**
* The current account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new UserRequestSubscriber.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(AccountInterface $account, EntityTypeManagerInterface $entity_type_manager, protected ?TimeInterface $time = NULL) {
$this->account = $account;
$this->entityTypeManager = $entity_type_manager;
if (!$time) {
@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/3387233', E_USER_DEPRECATED);
$this->time = \Drupal::service(TimeInterface::class);
}
}
/**
* Updates the current user's last access time.
*
* @param \Symfony\Component\HttpKernel\Event\TerminateEvent $event
* The event to process.
*/
public function onKernelTerminate(TerminateEvent $event) {
if ($this->account->isAuthenticated() && $this->time->getRequestTime() - $this->account->getLastAccessedTime() > Settings::get('session_write_interval', 180)) {
// Do that no more than once per 180 seconds.
/** @var \Drupal\user\UserStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage('user');
$storage->updateLastAccessTimestamp($this->account, $this->time->getRequestTime());
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Should go before other subscribers start to write their caches.
$events[KernelEvents::TERMINATE][] = ['onKernelTerminate', 300];
return $events;
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\user\PermissionHandlerInterface;
use Drupal\user\RoleStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
/**
* Provides the permissions administration form for a bundle.
*
* This class handles bundles that are defined by configuration objects.
*
* @internal
*/
class EntityPermissionsForm extends UserPermissionsForm {
/**
* The configuration entity manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The bundle object.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $bundle;
/**
* Constructs a new EntityPermissionsForm.
*
* @param \Drupal\user\PermissionHandlerInterface $permission_handler
* The permission handler.
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration entity manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Extension\ModuleExtensionList|null $module_extension_list
* The module extension list.
*/
public function __construct(PermissionHandlerInterface $permission_handler, RoleStorageInterface $role_storage, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, EntityTypeManagerInterface $entity_type_manager, ?ModuleExtensionList $module_extension_list = NULL) {
if ($module_extension_list === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $module_extension_list argument is deprecated in drupal:10.3.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$module_extension_list = \Drupal::service('extension.list.module');
}
parent::__construct($permission_handler, $role_storage, $module_handler, $module_extension_list);
$this->configManager = $config_manager;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('user.permissions'),
$container->get('entity_type.manager')->getStorage('user_role'),
$container->get('module_handler'),
$container->get('config.manager'),
$container->get('entity_type.manager'),
$container->get('extension.list.module'),
);
}
/**
* {@inheritdoc}
*/
protected function permissionsByProvider(): array {
// Get the names of all config entities that depend on $this->bundle.
$config_name = $this->bundle->getConfigDependencyName();
$config_entities = $this->configManager
->getConfigEntitiesToChangeOnDependencyRemoval('config', [$config_name]);
$config_names = array_map(
function ($dependent_config) {
return $dependent_config->getConfigDependencyName();
}, $config_entities['delete'] ?? []
);
$config_names[] = $config_name;
// Find all the permissions that depend on $this->bundle.
$permissions = $this->permissionHandler->getPermissions();
$permissions_by_provider = [];
foreach ($permissions as $permission_name => $permission) {
$required_configs = $permission['dependencies']['config'] ?? [];
if (array_intersect($required_configs, $config_names)) {
$provider = $permission['provider'];
$permissions_by_provider[$provider][$permission_name] = $permission;
}
}
return $permissions_by_provider;
}
/**
* Builds the user permissions administration form for a bundle.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $bundle_entity_type
* (optional) The entity type ID.
* @param string|\Drupal\Core\Entity\EntityInterface $bundle
* (optional) Either the bundle name or the bundle object.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?string $bundle_entity_type = NULL, $bundle = NULL): array {
// Set $this->bundle for use by ::permissionsByProvider().
if ($bundle instanceof EntityInterface) {
$this->bundle = $bundle;
return parent::buildForm($form, $form_state);
}
$this->bundle = $this->entityTypeManager
->getStorage($bundle_entity_type)
->load($bundle);
return parent::buildForm($form, $form_state);
}
/**
* Checks that there are permissions to be managed.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route.
* @param string|EntityInterface $bundle
* (optional) The bundle. Different entity types can have different names
* for their bundle key, so if not specified on the route via a {bundle}
* parameter, the access checker determines the appropriate key name, and
* gets the value from the corresponding request attribute. For example,
* for nodes, the bundle key is "node_type", so the value would be
* available via the {node_type} parameter rather than a {bundle}
* parameter.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, $bundle = NULL): AccessResultInterface {
$permission = $route->getRequirement('_permission');
if ($permission && !$this->currentUser()->hasPermission($permission)) {
return AccessResult::neutral()->cachePerPermissions();
}
// Set $this->bundle for use by ::permissionsByProvider().
if ($bundle instanceof EntityInterface) {
$this->bundle = $bundle;
}
else {
$bundle_entity_type = $route->getDefault('bundle_entity_type');
$bundle_name = is_string($bundle) ? $bundle : $route_match->getRawParameter($bundle_entity_type);
$this->bundle = $this->entityTypeManager
->getStorage($bundle_entity_type)
->load($bundle_name);
}
if (empty($this->bundle)) {
// A typo in the request path can lead to this case.
return AccessResult::forbidden();
}
return AccessResult::allowedIf((bool) $this->permissionsByProvider());
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\RoleInterface;
use Drupal\user\RoleStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure administrator role settings for this site.
*/
class RoleSettingsForm extends FormBase {
/**
* The role storage used when changing the admin role.
*
* @var \Drupal\user\RoleStorageInterface
*/
protected $roleStorage;
/**
* Constructs a \Drupal\user\Form\RoleSettingsForm object.
*
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
*/
public function __construct(RoleStorageInterface $role_storage) {
$this->roleStorage = $role_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('user_role')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'role_settings';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Administrative role option.
$form['admin_role'] = [
'#type' => 'details',
'#title' => $this->t('Administrator role'),
'#open' => TRUE,
];
// Do not allow users to set the anonymous or authenticated user roles as
// the administrator role.
$roles = $this->roleStorage->loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$roles = array_map(fn(RoleInterface $role) => $role->label(), $roles);
$admin_roles = $this->roleStorage->getQuery()
->condition('is_admin', TRUE)
->execute();
$default_value = reset($admin_roles);
$form['admin_role']['user_admin_role'] = [
'#type' => 'select',
'#title' => $this->t('Administrator role'),
'#empty_value' => '',
'#default_value' => $default_value,
'#options' => $roles,
'#description' => $this->t('This role will be automatically granted all permissions.'),
// Don't allow to select a single admin role in case multiple roles got
// marked as admin role already.
'#access' => count($admin_roles) <= 1,
];
$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) {
if ($form_state->hasValue('user_admin_role')) {
$admin_roles = $this->roleStorage->getQuery()
->condition('is_admin', TRUE)
->execute();
foreach ($admin_roles as $rid) {
$this->roleStorage->load($rid)->setIsAdmin(FALSE)->save();
}
$new_admin_role = $form_state->getValue('user_admin_role');
if ($new_admin_role) {
$this->roleStorage->load($new_admin_role)->setIsAdmin(TRUE)->save();
}
}
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Entity\ContentEntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a confirmation form for cancelling user account.
*
* @internal
*/
class UserCancelForm extends ContentEntityConfirmFormBase {
/**
* Available account cancellation methods.
*
* @var array
*/
protected $cancelMethods;
/**
* Whether it is allowed to select cancellation method.
*
* @var bool
*/
protected $selectCancel;
/**
* The account being cancelled.
*
* @var \Drupal\user\UserInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
public function getQuestion() {
if ($this->entity->id() == $this->currentUser()->id()) {
return $this->t('Are you sure you want to cancel your account?');
}
return $this->t('Are you sure you want to cancel the account %name?', ['%name' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->toUrl();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
if ($this->selectCancel) {
return '';
}
$default_method = $this->config('user.settings')->get('cancel_method');
$own_account = $this->entity->id() == $this->currentUser()->id();
// Options supplied via user_cancel_methods() can have a custom
// #confirm_description property for the confirmation form description.
// This text refers to "Your account" so only user it if cancelling own account.
if ($own_account && isset($this->cancelMethods[$default_method]['#confirm_description'])) {
return $this->cancelMethods[$default_method]['#confirm_description'];
}
return $this->cancelMethods['#options'][$default_method];
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Confirm');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$user = $this->currentUser();
$this->cancelMethods = user_cancel_methods();
// Display account cancellation method selection, if allowed.
$own_account = $this->entity->id() == $user->id();
$this->selectCancel = $user->hasPermission('administer users') || $user->hasPermission('select account cancellation method');
$form['user_cancel_method'] = [
'#type' => 'radios',
'#title' => $own_account ? $this->t('When cancelling your account') : $this->t('Cancellation method'),
'#access' => $this->selectCancel,
];
$form['user_cancel_method'] += $this->cancelMethods;
// When managing another user, can skip the account cancellation
// confirmation mail (by default).
$override_access = !$own_account;
$form['user_cancel_confirm'] = [
'#type' => 'checkbox',
'#title' => $this->t('Require email confirmation'),
'#default_value' => !$override_access,
'#access' => $override_access,
'#description' => $this->t('When enabled, the user must confirm the account cancellation via email.'),
];
// Also allow to send account canceled notification mail, if enabled.
$default_notify = $this->config('user.settings')->get('notify.status_canceled');
$form['user_cancel_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is canceled'),
'#default_value' => ($override_access ? FALSE : $default_notify),
'#access' => $override_access && $default_notify,
'#description' => $this->t('When enabled, the user will receive an email notification after the account has been canceled.'),
];
// Always provide entity id in the same form key as in the entity edit form.
$form['uid'] = ['#type' => 'value', '#value' => $this->entity->id()];
// Store the user permissions so that it can be altered in hook_form_alter()
// if desired.
$form['access'] = [
'#type' => 'value',
'#value' => !$own_account,
];
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Cancel account immediately, if the current user has administrative
// privileges, no confirmation mail shall be sent, and the user does not
// attempt to cancel the own account.
if (!$form_state->isValueEmpty('access') && $form_state->isValueEmpty('user_cancel_confirm') && $this->entity->id() != $this->currentUser()->id()) {
user_cancel($form_state->getValues(), $this->entity->id(), $form_state->getValue('user_cancel_method'));
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
}
else {
// Store cancelling method and whether to notify the user in
// $this->entity for
// \Drupal\user\Controller\UserController::confirmCancel().
$this->entity->user_cancel_method = $form_state->getValue('user_cancel_method');
$this->entity->user_cancel_notify = $form_state->getValue('user_cancel_notify');
$this->entity->save();
_user_mail_notify('cancel_confirm', $this->entity);
$this->messenger()->addStatus($this->t('A confirmation request to cancel your account has been sent to your email address.'));
$this->logger('user')->info('Sent account cancellation request to %name %email.', ['%name' => $this->entity->label(), '%email' => '<' . $this->entity->getEmail() . '>']);
$form_state->setRedirect(
'entity.user.canonical',
['user' => $this->entity->id()]
);
}
}
}

View File

@@ -0,0 +1,304 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Render\BareHtmlPageRendererInterface;
use Drupal\Core\Url;
use Drupal\user\UserAuthenticationInterface;
use Drupal\user\UserAuthInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Drupal\user\UserFloodControlInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a user login form.
*
* @internal
*/
class UserLoginForm extends FormBase {
/**
* The user flood control service.
*
* @var \Drupal\user\UserFloodControl
*/
protected $userFloodControl;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The user authentication object.
*
* @var \Drupal\user\UserAuthInterface|\Drupal\user\UserAuthenticationInterface
*/
protected $userAuth;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The bare HTML renderer.
*
* @var \Drupal\Core\Render\BareHtmlPageRendererInterface
*/
protected $bareHtmlPageRenderer;
/**
* Constructs a new UserLoginForm.
*
* @param \Drupal\user\UserFloodControlInterface $user_flood_control
* The user flood control service.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\user\UserAuthInterface|\Drupal\user\UserAuthenticationInterface $user_auth
* The user authentication object.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_renderer
* The renderer.
*/
public function __construct(UserFloodControlInterface $user_flood_control, UserStorageInterface $user_storage, UserAuthInterface|UserAuthenticationInterface $user_auth, RendererInterface $renderer, BareHtmlPageRendererInterface $bare_html_renderer) {
$this->userFloodControl = $user_flood_control;
$this->userStorage = $user_storage;
if (!$user_auth instanceof UserAuthenticationInterface) {
@trigger_error('The $user_auth parameter not implementing UserAuthenticationInterface is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. See https://www.drupal.org/node/3411040');
}
$this->userAuth = $user_auth;
$this->renderer = $renderer;
$this->bareHtmlPageRenderer = $bare_html_renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('user.flood_control'),
$container->get('entity_type.manager')->getStorage('user'),
$container->get('user.auth'),
$container->get('renderer'),
$container->get('bare_html_page_renderer')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_login_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('system.site');
// Display login form:
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Username'),
'#size' => 60,
'#maxlength' => UserInterface::USERNAME_MAX_LENGTH,
'#required' => TRUE,
'#attributes' => [
'autocorrect' => 'none',
'autocapitalize' => 'none',
'spellcheck' => 'false',
'autofocus' => 'autofocus',
'autocomplete' => 'username',
],
];
$form['pass'] = [
'#type' => 'password',
'#title' => $this->t('Password'),
'#size' => 60,
'#required' => TRUE,
'#attributes' => [
'autocomplete' => 'current-password',
],
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = ['#type' => 'submit', '#value' => $this->t('Log in')];
$form['#validate'][] = '::validateAuthentication';
$form['#validate'][] = '::validateFinal';
$this->renderer->addCacheableDependency($form, $config);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if (empty($uid = $form_state->get('uid'))) {
return;
}
$account = $this->userStorage->load($uid);
// A destination was set, probably on an exception controller.
if (!$this->getRequest()->request->has('destination')) {
$form_state->setRedirect(
'entity.user.canonical',
['user' => $account->id()]
);
}
else {
$this->getRequest()->query->set('destination', $this->getRequest()->request->get('destination'));
}
user_login_finalize($account);
}
/**
* Sets an error if supplied username has been blocked.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no replacement.
* @see https://www.drupal.org/node/3410706
*/
public function validateName(array &$form, FormStateInterface $form_state) {
@trigger_error(__METHOD__ . ' is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3410706', E_USER_DEPRECATED);
if (!$form_state->isValueEmpty('name') && user_is_blocked($form_state->getValue('name'))) {
// Blocked in user administration.
$form_state->setErrorByName('name', $this->t('The username %name has not been activated or is blocked.', ['%name' => $form_state->getValue('name')]));
}
}
/**
* Checks supplied username/password against local users table.
*
* If successful, $form_state->get('uid') is set to the matching user ID.
*/
public function validateAuthentication(array &$form, FormStateInterface $form_state) {
$password = trim($form_state->getValue('pass'));
$flood_config = $this->config('user.flood');
$account = FALSE;
if (!$form_state->isValueEmpty('name') && strlen($password) > 0) {
// Do not allow any login from the current user's IP if the limit has been
// reached. Default is 50 failed attempts allowed in one hour. This is
// independent of the per-user limit to catch attempts from one IP to log
// in to many different user accounts. We have a reasonably high limit
// since there may be only one apparent IP for all users at an institution.
if (!$this->userFloodControl->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
$form_state->set('flood_control_triggered', 'ip');
return;
}
if ($this->userAuth instanceof UserAuthenticationInterface) {
$account = $this->userAuth->lookupAccount($form_state->getValue('name'));
}
else {
$accounts = $this->userStorage->loadByProperties(['name' => $form_state->getValue('name')]);
$account = reset($accounts);
}
if ($account && $account->isBlocked()) {
$form_state->setErrorByName('name', $this->t('The username %name has not been activated or is blocked.', ['%name' => $form_state->getValue('name')]));
}
elseif ($account && $account->isActive()) {
if ($flood_config->get('uid_only')) {
// Register flood events based on the uid only, so they apply for any
// IP address. This is the most secure option.
$identifier = $account->id();
}
else {
// The default identifier is a combination of uid and IP address. This
// is less secure but more resistant to denial-of-service attacks that
// could lock out all users with public user names.
$identifier = $account->id() . '-' . $this->getRequest()->getClientIP();
}
$form_state->set('flood_control_user_identifier', $identifier);
// If there are zero flood records for this user, then we don't need to
// clear any failed login attempts after a successful login, so check
// for this case first before checking the actual flood limit and store
// the result in form state.
if (!$this->userFloodControl->isAllowed('user.failed_login_user', 1, $flood_config->get('user_window'), $identifier)) {
// Now check the actual limit for the user. Default is to allow 5
// failed attempts every 6 hours. This means we check the flood table
// twice if flood control has already been triggered by a previous
// login attempt, but this should be the less common case.
if (!$this->userFloodControl->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
$form_state->set('flood_control_triggered', 'user');
return;
}
}
else {
$form_state->set('flood_control_skip_clear', 'user');
}
// We are not limited by flood control, so try to authenticate.
// Store the user ID in form state as a flag for self::validateFinal().
if ($this->userAuth instanceof UserAuthenticationInterface) {
$form_state->set('uid', $this->userAuth->authenticateAccount($account, $password) ? $account->id() : FALSE);
}
}
elseif (!$this->userAuth instanceof UserAuthenticationInterface) {
$uid = $this->userAuth->authenticate($form_state->getValue('name'), $password);
$form_state->set('uid', $uid);
}
}
}
/**
* Checks if user was not authenticated, or if too many logins were attempted.
*
* This validation function should always be the last one.
*/
public function validateFinal(array &$form, FormStateInterface $form_state) {
$flood_config = $this->config('user.flood');
if (!$form_state->get('uid')) {
// Always register an IP-based failed login event.
$this->userFloodControl->register('user.failed_login_ip', $flood_config->get('ip_window'));
// Register a per-user failed login event.
if ($flood_control_user_identifier = $form_state->get('flood_control_user_identifier')) {
$this->userFloodControl->register('user.failed_login_user', $flood_config->get('user_window'), $flood_control_user_identifier);
}
if ($flood_control_triggered = $form_state->get('flood_control_triggered')) {
if ($flood_control_triggered == 'user') {
$message = $this->formatPlural($flood_config->get('user_limit'), 'There has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', 'There have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')->toString()]);
}
else {
// We did not find a uid, so the limit is IP-based.
$message = $this->t('Too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', [':url' => Url::fromRoute('user.pass')->toString()]);
}
$response = $this->bareHtmlPageRenderer->renderBarePage(['#markup' => $message], $this->t('Login failed'), 'maintenance_page');
$response->setStatusCode(403);
$form_state->setResponse($response);
}
else {
$form_state->setErrorByName('name', $this->t('Unrecognized username or password. <a href=":password">Forgot your password?</a>', [':password' => Url::fromRoute('user.pass')->toString()]));
$accounts = $this->userStorage->loadByProperties(['name' => $form_state->getValue('name')]);
if (!empty($accounts)) {
$this->logger('user')->notice('Login attempt failed for %user.', ['%user' => $form_state->getValue('name')]);
}
else {
// If the username entered is not a valid user,
// only store the IP address.
$this->logger('user')->notice('Login attempt failed from %ip.', ['%ip' => $this->getRequest()->getClientIp()]);
}
}
}
elseif (!$form_state->get('flood_control_skip_clear') && $flood_control_user_identifier = $form_state->get('flood_control_user_identifier')) {
// Clear past failures for this user so as not to block a user who might
// log in and out more than once in an hour.
$this->userFloodControl->clear('user.failed_login_user', $flood_control_user_identifier);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\user\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
/**
* Provides a confirmation form for user logout.
*/
class UserLogoutConfirm extends ConfirmFormBase implements WorkspaceSafeFormInterface {
/**
* {@inheritdoc}
*/
public function getConfirmText(): TranslatableMarkup {
return $this->t('Log out');
}
/**
* {@inheritdoc}
*/
public function getQuestion(): TranslatableMarkup {
return $this->t('Are you sure you want to log out?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl(): Url {
return new Url('<front>');
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'user_logout_confirm';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
user_logout();
$form_state->setRedirect('<front>');
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Url;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a confirmation form for cancelling multiple user accounts.
*
* @internal
*/
class UserMultipleCancelConfirm extends ConfirmFormBase {
/**
* The temp store factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new UserMultipleCancelConfirm.
*
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The temp store factory.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, UserStorageInterface $user_storage, EntityTypeManagerInterface $entity_type_manager) {
$this->tempStoreFactory = $temp_store_factory;
$this->userStorage = $user_storage;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('tempstore.private'),
$container->get('entity_type.manager')->getStorage('user'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_multiple_cancel_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to cancel these user accounts?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.user.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Confirm');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Retrieve the accounts to be canceled from the temp store.
/** @var \Drupal\user\Entity\User[] $accounts */
$accounts = $this->tempStoreFactory
->get('user_user_operations_cancel')
->get($this->currentUser()->id());
if (!$accounts) {
return $this->redirect('entity.user.collection');
}
$root = NULL;
$names = [];
$form['accounts'] = ['#tree' => TRUE];
foreach ($accounts as $account) {
$uid = $account->id();
$names[$uid] = $account->label();
// Prevent user 1 from being canceled.
if ($uid <= 1) {
$root = intval($uid) === 1 ? $account : $root;
continue;
}
$form['accounts'][$uid] = [
'#type' => 'hidden',
'#value' => $uid,
];
}
$form['account']['names'] = [
'#theme' => 'item_list',
'#items' => $names,
];
// Output a notice that user 1 cannot be canceled.
if (isset($root)) {
$redirect = (count($accounts) == 1);
$message = $this->t('The user account %name cannot be canceled.', ['%name' => $root->label()]);
$this->messenger()->addMessage($message, $redirect ? MessengerInterface::TYPE_ERROR : MessengerInterface::TYPE_WARNING);
// If only user 1 was selected, redirect to the overview.
if ($redirect) {
return $this->redirect('entity.user.collection');
}
}
$form['operation'] = ['#type' => 'hidden', '#value' => 'cancel'];
// Display account cancellation method selection, if allowed.
$user = $this->currentUser();
$selectCancel = $user->hasPermission('administer users') || $user->hasPermission('select account cancellation method');
$form['user_cancel_method'] = [
'#type' => 'radios',
'#title' => $this->t('Cancellation method'),
'#access' => $selectCancel,
];
$form['user_cancel_method'] += user_cancel_methods();
if (!$selectCancel) {
// Display an item to inform the user of the setting.
$default_method = $form['user_cancel_method']['#default_value'];
$form['user_cancel_method_show'] = [
'#type' => 'item',
'#title' => $this->t('When cancelling these accounts'),
'#plain_text' => $form['user_cancel_method']['#options'][$default_method],
];
}
// Allow to send the account cancellation confirmation mail.
$form['user_cancel_confirm'] = [
'#type' => 'checkbox',
'#title' => $this->t('Require email confirmation'),
'#default_value' => FALSE,
'#description' => $this->t('When enabled, the user must confirm the account cancellation via email.'),
];
// Also allow to send account canceled notification mail, if enabled.
$form['user_cancel_notify'] = [
'#type' => 'checkbox',
'#title' => $this->t('Notify user when account is canceled'),
'#default_value' => FALSE,
'#access' => $this->config('user.settings')->get('notify.status_canceled'),
'#description' => $this->t('When enabled, the user will receive an email notification after the account has been canceled.'),
];
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$current_user_id = $this->currentUser()->id();
// Clear out the accounts from the temp store.
$this->tempStoreFactory->get('user_user_operations_cancel')->delete($current_user_id);
if ($form_state->getValue('confirm')) {
foreach ($form_state->getValue('accounts') as $uid => $value) {
// Prevent programmatic form submissions from cancelling user 1.
if ($uid <= 1) {
continue;
}
// Prevent user administrators from deleting themselves without confirmation.
if ($uid == $current_user_id) {
$admin_form_mock = [];
$admin_form_state = $form_state;
$admin_form_state->unsetValue('user_cancel_confirm');
// The $user global is not a complete user entity, so load the full
// entity.
$account = $this->userStorage->load($uid);
$admin_form = $this->entityTypeManager->getFormObject('user', 'cancel');
$admin_form->setEntity($account);
// Calling this directly required to init form object with $account.
$admin_form->buildForm($admin_form_mock, $admin_form_state);
$admin_form->submitForm($admin_form_mock, $admin_form_state);
}
else {
user_cancel($form_state->getValues(), $uid, $form_state->getValue('user_cancel_method'));
}
}
}
$form_state->setRedirect('entity.user.collection');
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace Drupal\user\Form;
use Drupal\Component\Utility\EmailValidatorInterface;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Flood\FloodInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element\Email;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\user\UserInterface;
use Drupal\user\UserStorageInterface;
use Drupal\user\UserNameValidator;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a user password reset form.
*
* Send the user an email to reset their password.
*
* @internal
*/
class UserPasswordForm extends FormBase {
use DeprecatedServicePropertyTrait;
/**
* The deprecated properties.
*/
protected array $deprecatedProperties = [
'typedDataManager' => 'typed_data_manager',
];
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The flood service.
*
* @var \Drupal\Core\Flood\FloodInterface
*/
protected $flood;
/**
* The email validator service.
*
* @var \Drupal\Component\Utility\EmailValidatorInterface
*/
protected $emailValidator;
/**
* Constructs a UserPasswordForm object.
*
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\ConfigFactory $config_factory
* The config factory.
* @param \Drupal\Core\Flood\FloodInterface $flood
* The flood service.
* @param \Drupal\user\UserNameValidator|\Drupal\Core\TypedData\TypedDataManagerInterface $userNameValidator
* The user validator service.
* @param \Drupal\Component\Utility\EmailValidatorInterface $email_validator
* The email validator service.
*/
public function __construct(
UserStorageInterface $user_storage,
LanguageManagerInterface $language_manager,
ConfigFactory $config_factory,
FloodInterface $flood,
protected UserNameValidator|TypedDataManagerInterface $userNameValidator,
EmailValidatorInterface $email_validator,
) {
$this->userStorage = $user_storage;
$this->languageManager = $language_manager;
$this->configFactory = $config_factory;
$this->flood = $flood;
$this->emailValidator = $email_validator;
if (!$userNameValidator instanceof UserNameValidator) {
@\trigger_error('Passing $userNameValidator as \Drupal\Core\TypedData\TypedDataManagerInterface to ' . __METHOD__ . ' () is deprecated in drupal:10.3.0 and is removed in drupal:11.0.0. Pass a Drupal\user\UserValidator instead. See https://www.drupal.org/node/3431205', E_USER_DEPRECATED);
$this->userNameValidator = \Drupal::service('user.name_validator');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('user'),
$container->get('language_manager'),
$container->get('config.factory'),
$container->get('flood'),
$container->get('user.name_validator'),
$container->get('email.validator'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_pass';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Username or email address'),
'#size' => 60,
'#maxlength' => max(UserInterface::USERNAME_MAX_LENGTH, Email::EMAIL_MAX_LENGTH),
'#required' => TRUE,
'#attributes' => [
'autocorrect' => 'off',
'autocapitalize' => 'off',
'spellcheck' => 'false',
'autofocus' => 'autofocus',
'autocomplete' => 'username',
],
];
// Allow logged in users to request this also.
$user = $this->currentUser();
if ($user->isAuthenticated()) {
$form['name']['#type'] = 'value';
$form['name']['#value'] = $user->getEmail();
$form['mail'] = [
'#prefix' => '<p>',
'#markup' => $this->t('Password reset instructions will be mailed to %email. You must log out to use the password reset link in the email.', ['%email' => $user->getEmail()]),
'#suffix' => '</p>',
];
}
else {
$form['mail'] = [
'#prefix' => '<p>',
'#markup' => $this->t('Password reset instructions will be sent to your registered email address.'),
'#suffix' => '</p>',
];
$form['name']['#default_value'] = $this->getRequest()->query->get('name');
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = ['#type' => 'submit', '#value' => $this->t('Submit')];
$form['#cache']['contexts'][] = 'url.query_args';
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$flood_config = $this->configFactory->get('user.flood');
if (!$this->flood->isAllowed('user.password_request_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
$form_state->setErrorByName('name', $this->t('Too many password recovery requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.'));
return;
}
$this->flood->register('user.password_request_ip', $flood_config->get('ip_window'));
// First, see if the input is possibly valid as a username.
$name = trim($form_state->getValue('name'));
$violations = $this->userNameValidator->validateName($name);
// Usernames have a maximum length shorter than email addresses. Only print
// this error if the input is not valid as a username or email address.
if ($violations->count() > 0 && !$this->emailValidator->isValid($name)) {
$form_state->setErrorByName('name', $this->t("The username or email address is invalid."));
return;
}
// Try to load by email.
$users = $this->userStorage->loadByProperties(['mail' => $name]);
if (empty($users)) {
// No success, try to load by name.
$users = $this->userStorage->loadByProperties(['name' => $name]);
}
$account = reset($users);
// Blocked accounts cannot request a new password.
if ($account && $account->id() && $account->isActive()) {
// Register flood events based on the uid only, so they apply for any
// IP address. This allows them to be cleared on successful reset (from
// any IP).
$identifier = $account->id();
if (!$this->flood->isAllowed('user.password_request_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
return;
}
$this->flood->register('user.password_request_user', $flood_config->get('user_window'), $identifier);
$form_state->setValueForElement(['#parents' => ['account']], $account);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$account = $form_state->getValue('account');
if ($account) {
// Mail one time login URL and instructions using current language.
$mail = _user_mail_notify('password_reset', $account);
if (!empty($mail)) {
$this->logger('user')
->info('Password reset instructions mailed to %name at %email.', [
'%name' => $account->getAccountName(),
'%email' => $account->getEmail(),
]);
}
}
else {
$this->logger('user')
->info('Password reset form was submitted with an unknown or inactive account: %name.', [
'%name' => $form_state->getValue('name'),
]);
}
// Make sure the status text is displayed even if no email was sent. This
// message is deliberately the same as the success message for privacy.
$this->messenger()
->addStatus($this->t('If %identifier is a valid account, an email will be sent with instructions to reset your password.', [
'%identifier' => $form_state->getValue('name'),
]));
$form_state->setRedirect('<front>');
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Url;
/**
* Form controller for the user password forms.
*
* Users followed the link in the email, now they can enter a new password.
*
* @internal
*/
class UserPasswordResetForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_pass_reset';
}
/**
* {@inheritdoc}
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\Core\Session\AccountInterface $user
* User requesting reset.
* @param string $expiration_date
* Formatted expiration date for the login link, or NULL if the link does
* not expire.
* @param int $timestamp
* The current timestamp.
* @param string $hash
* Login link hash.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?AccountInterface $user = NULL, $expiration_date = NULL, $timestamp = NULL, $hash = NULL) {
if ($expiration_date) {
$form['message'] = ['#markup' => $this->t('<p>This is a one-time login for %user_name and will expire on %expiration_date.</p><p>Click on this button to log in to the site and change your password.</p>', ['%user_name' => $user->getAccountName(), '%expiration_date' => $expiration_date])];
$form['#title'] = $this->t('Reset password');
}
else {
// No expiration for first time login.
$form['message'] = ['#markup' => $this->t('<p>This is a one-time login for %user_name.</p><p>Click on this button to log in to the site and change your password.</p>', ['%user_name' => $user->getAccountName()])];
$form['#title'] = $this->t('Set password');
}
$form['help'] = ['#markup' => '<p>' . $this->t('This login can be used only once.') . '</p>'];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Log in'),
];
$form['#action'] = Url::fromRoute('user.reset.login', [
'uid' => $user->id(),
'timestamp' => $timestamp,
'hash' => $hash,
])->toString();
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// This form works by submitting the hash and timestamp to the user.reset
// route with a 'login' action.
}
}

View File

@@ -0,0 +1,263 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\PermissionHandlerInterface;
use Drupal\user\RoleStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the user permissions administration form.
*
* @internal
*/
class UserPermissionsForm extends FormBase {
/**
* The permission handler.
*
* @var \Drupal\user\PermissionHandlerInterface
*/
protected $permissionHandler;
/**
* The role storage.
*
* @var \Drupal\user\RoleStorageInterface
*/
protected $roleStorage;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a new UserPermissionsForm.
*
* @param \Drupal\user\PermissionHandlerInterface $permission_handler
* The permission handler.
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleExtensionList|null $moduleExtensionList
* The module extension list.
*/
public function __construct(PermissionHandlerInterface $permission_handler, RoleStorageInterface $role_storage, ModuleHandlerInterface $module_handler, protected ?ModuleExtensionList $moduleExtensionList = NULL) {
$this->permissionHandler = $permission_handler;
$this->roleStorage = $role_storage;
$this->moduleHandler = $module_handler;
if ($this->moduleExtensionList === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $moduleExtensionList argument is deprecated in drupal:10.3.0 and will be required in drupal:12.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$this->moduleExtensionList = \Drupal::service('extension.list.module');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('user.permissions'),
$container->get('entity_type.manager')->getStorage('user_role'),
$container->get('module_handler'),
$container->get('extension.list.module'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'user_admin_permissions';
}
/**
* Gets the roles to display in this form.
*
* @return \Drupal\user\RoleInterface[]
* An array of role objects.
*/
protected function getRoles() {
return $this->roleStorage->loadMultiple();
}
/**
* Group permissions by the modules that provide them.
*
* @return string[][]
* A nested array. The outer keys are modules that provide permissions. The
* inner arrays are permission names keyed by their machine names.
*/
protected function permissionsByProvider(): array {
$permissions = $this->permissionHandler->getPermissions();
$permissions_by_provider = [];
foreach ($permissions as $permission_name => $permission) {
$permissions_by_provider[$permission['provider']][$permission_name] = $permission;
}
// Move the access content permission to the Node module if it is installed.
// @todo Add an alter so that this section can be moved to the Node module.
if ($this->moduleHandler->moduleExists('node')) {
// Insert 'access content' before the 'view own unpublished content' key
// in order to maintain the UI even though the permission is provided by
// the system module.
$keys = array_keys($permissions_by_provider['node']);
$offset = (int) array_search('view own unpublished content', $keys);
$permissions_by_provider['node'] = array_merge(
array_slice($permissions_by_provider['node'], 0, $offset),
['access content' => $permissions_by_provider['system']['access content']],
array_slice($permissions_by_provider['node'], $offset)
);
unset($permissions_by_provider['system']['access content']);
}
return $permissions_by_provider;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$role_names = [];
$role_permissions = [];
$admin_roles = [];
foreach ($this->getRoles() as $role_name => $role) {
// Retrieve role names for columns.
$role_names[$role_name] = $role->label();
// Fetch permissions for the roles.
$role_permissions[$role_name] = $role->getPermissions();
$admin_roles[$role_name] = $role->isAdmin();
}
// Store $role_names for use when saving the data.
$form['role_names'] = [
'#type' => 'value',
'#value' => $role_names,
];
// Render role/permission overview:
$hide_descriptions = system_admin_compact_mode();
$form['system_compact_link'] = [
'#id' => FALSE,
'#type' => 'system_compact_link',
];
$form['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$form['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Filter permissions'),
'#title_display' => 'invisible',
'#size' => 30,
'#placeholder' => $this->t('Filter by permission name'),
'#description' => $this->t('Enter permission name'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '#permissions',
'autocomplete' => 'off',
],
];
$form['permissions'] = [
'#type' => 'table',
'#header' => [$this->t('Permission')],
'#id' => 'permissions',
'#attributes' => ['class' => ['permissions', 'js-permissions']],
'#sticky' => TRUE,
];
foreach ($role_names as $name) {
$form['permissions']['#header'][] = [
'data' => $name,
'class' => ['checkbox'],
];
}
foreach ($this->permissionsByProvider() as $provider => $permissions) {
// Module name.
$form['permissions'][$provider] = [
[
'#wrapper_attributes' => [
'colspan' => count($role_names) + 1,
'class' => ['module'],
'id' => 'module-' . $provider,
],
'#markup' => $this->moduleExtensionList->getName($provider),
],
];
foreach ($permissions as $perm => $perm_item) {
// Fill in default values for the permission.
$perm_item += [
'description' => '',
'restrict access' => FALSE,
'warning' => !empty($perm_item['restrict access']) ? $this->t('Warning: Give to trusted roles only; this permission has security implications.') : '',
];
$form['permissions'][$perm]['description'] = [
'#type' => 'inline_template',
'#template' => '<div class="permission"><span class="title table-filter-text-source">{{ title }}</span>{% if description or warning %}<div class="description">{% if warning %}<em class="permission-warning">{{ warning }}</em> {% endif %}{{ description }}</div>{% endif %}</div>',
'#context' => [
'title' => $perm_item['title'],
],
];
// Show the permission description.
if (!$hide_descriptions) {
$form['permissions'][$perm]['description']['#context']['description'] = $perm_item['description'];
$form['permissions'][$perm]['description']['#context']['warning'] = $perm_item['warning'];
}
foreach ($role_names as $rid => $name) {
$form['permissions'][$perm][$rid] = [
'#title' => $name . ': ' . $perm_item['title'],
'#title_display' => 'invisible',
'#wrapper_attributes' => [
'class' => ['checkbox'],
],
'#type' => 'checkbox',
'#default_value' => in_array($perm, $role_permissions[$rid]) ? 1 : 0,
'#attributes' => ['class' => ['rid-' . $rid, 'js-rid-' . $rid]],
'#parents' => [$rid, $perm],
];
// Show a column of disabled but checked checkboxes.
if ($admin_roles[$rid]) {
$form['permissions'][$perm][$rid]['#disabled'] = TRUE;
$form['permissions'][$perm][$rid]['#default_value'] = TRUE;
}
}
}
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save permissions'),
'#button_type' => 'primary',
];
$form['#attached']['library'][] = 'user/drupal.user.permissions';
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValue('role_names') as $role_name => $name) {
user_role_change_permissions($role_name, (array) $form_state->getValue($role_name));
}
$this->messenger()->addStatus($this->t('The changes have been saved.'));
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides the user permissions administration form for one or more module(s).
*
* @internal
*/
class UserPermissionsModuleSpecificForm extends UserPermissionsForm {
/**
* The module list.
*
* A keyed array of module machine names.
*
* @var string[]
*/
protected $moduleList;
/**
* {@inheritdoc}
*/
protected function permissionsByProvider(): array {
return array_intersect_key(
parent::permissionsByProvider(),
array_flip($this->moduleList)
);
}
/**
* Builds the user permissions administration form for a specific module(s).
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $modules
* (optional) One or more module machine names, comma-separated.
*/
public function buildForm(array $form, FormStateInterface $form_state, $modules = ''): array {
$this->moduleList = explode(',', $modules);
return parent::buildForm($form, $form_state);
}
/**
* Checks that at least one module defines permissions.
*
* @param string $modules
* (optional) One or more module machine names, comma-separated.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access($modules): AccessResultInterface {
foreach (explode(',', $modules) as $module) {
if ($this->permissionHandler->moduleProvidesPermissions($module)) {
return AccessResult::allowed();
}
}
return AccessResult::forbidden();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\user\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\RoleInterface;
/**
* Provides the user permissions administration form for a specific role.
*
* @internal
*/
class UserPermissionsRoleSpecificForm extends UserPermissionsForm {
/**
* The specific role for this form.
*
* @var \Drupal\user\RoleInterface
*/
protected $userRole;
/**
* {@inheritdoc}
*/
protected function getRoles() {
return [$this->userRole->id() => $this->userRole];
}
/**
* Builds the user permissions administration form for a specific role.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\user\RoleInterface|null $user_role
* (optional) The user role used for this form. Defaults to NULL.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?RoleInterface $user_role = NULL) {
$this->userRole = $user_role;
return parent::buildForm($form, $form_state);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Drupal\user;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
/**
* Provides a helper for generating module permissions links.
*/
class ModulePermissionsLinkHelper {
use StringTranslationTrait;
/**
* Constructs a new service instance.
*
* @param \Drupal\user\PermissionHandlerInterface $permissionHandler
* The user permissions handler service.
* @param \Drupal\Core\Access\AccessManagerInterface $accessManager
* The access manager service.
* @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
* The module handler service.
*/
public function __construct(
protected PermissionHandlerInterface $permissionHandler,
protected AccessManagerInterface $accessManager,
protected ModuleExtensionList $moduleExtensionList,
) {}
/**
* Generates a link pointing to a given module's permissions page section.
*
* @param string $module
* The module name.
* @param string $name
* The module display name.
*
* @return array|null
* A module permissions link as a render array or NULL if the module doesn't
* expose any permission or the current user cannot access it.
*/
public function getModulePermissionsLink(string $module, string $name): ?array {
if ($this->permissionHandler->moduleProvidesPermissions($module)) {
if ($this->accessManager->checkNamedRoute('user.admin_permissions.module', ['modules' => $module])) {
$url = new Url('user.admin_permissions.module', ['modules' => $module]);
return [
'title' => t('Configure @module permissions', ['@module' => $name]),
'description' => '',
'url' => $url,
];
}
}
return NULL;
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace Drupal\user;
use Drupal\Core\Discovery\YamlDiscovery;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\CallableResolver;
/**
* Provides the available permissions based on yml files.
*
* To define permissions you can use a $module.permissions.yml file. This file
* defines machine names, human-readable names, restrict access (if required for
* security warning), and optionally descriptions for each permission type. The
* machine names are the canonical way to refer to permissions for access
* checking.
*
* If your module needs to define dynamic permissions you can use the
* permission_callbacks key to declare a callable that will return an array of
* permissions, keyed by machine name. Each item in the array can contain the
* same keys as an entry in $module.permissions.yml.
*
* Here is an example from the core filter module (comments have been added):
* @code
* # The key is the permission machine name, and is required.
* administer filters:
* # (required) Human readable name of the permission used in the UI.
* title: 'Administer text formats and filters'
* # (optional) Additional description fo the permission used in the UI.
* description: 'Define how text is handled by combining filters into text formats.'
* # (optional) Boolean, when set to true a warning about site security will
* # be displayed on the Permissions page. Defaults to false.
* restrict access: false
*
* # An array of callables used to generate dynamic permissions.
* permission_callbacks:
* # The callable should return an associative array with one or more
* # permissions. Each permission array can use the same keys as the example
* # permission defined above. Additionally, a dependencies key is supported.
* # For more information about permission dependencies see
* # PermissionHandlerInterface::getPermissions().
* - Drupal\filter\FilterPermissions::permissions
* @endcode
*
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
* @see filter.permissions.yml
* @see \Drupal\filter\FilterPermissions
* @see user_api
*/
class PermissionHandler implements PermissionHandlerInterface {
use StringTranslationTrait;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The YAML discovery class to find all .permissions.yml files.
*
* @var \Drupal\Core\Discovery\YamlDiscovery
*/
protected $yamlDiscovery;
/**
* The callable resolver.
*
* @var \Drupal\Core\Utility\CallableResolver
*/
protected CallableResolver $callableResolver;
/**
* Constructs a new PermissionHandler.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation.
* @param \Drupal\Core\Utility\CallableResolver|\Drupal\Core\Controller\ControllerResolverInterface $callable_resolver
* The callable resolver.
* @param \Drupal\Core\Extension\ModuleExtensionList|null $moduleExtensionList
* The module extension list.
*/
public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, ControllerResolverInterface|CallableResolver $callable_resolver, protected ?ModuleExtensionList $moduleExtensionList = NULL) {
if ($callable_resolver instanceof ControllerResolverInterface) {
@trigger_error('Calling ' . __METHOD__ . '() with an argument of ControllerResolverInterface is deprecated in drupal:10.2.0 and is removed in drupal:11.0.0. Use \Drupal\Core\Utility\CallableResolver instead. See https://www.drupal.org/node/3397954', E_USER_DEPRECATED);
$callable_resolver = \Drupal::service('callable_resolver');
}
$this->callableResolver = $callable_resolver;
// @todo It would be nice if you could pull all module directories from the
// container.
$this->moduleHandler = $module_handler;
$this->stringTranslation = $string_translation;
if ($this->moduleExtensionList === NULL) {
@trigger_error('Calling ' . __METHOD__ . '() without the $moduleExtensionList argument is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3310017', E_USER_DEPRECATED);
$this->moduleExtensionList = \Drupal::service('extension.list.module');
}
}
/**
* Gets the YAML discovery.
*
* @return \Drupal\Core\Discovery\YamlDiscovery
* The YAML discovery.
*/
protected function getYamlDiscovery() {
if (!isset($this->yamlDiscovery)) {
$this->yamlDiscovery = new YamlDiscovery('permissions', $this->moduleHandler->getModuleDirectories());
}
return $this->yamlDiscovery;
}
/**
* {@inheritdoc}
*/
public function getPermissions() {
$all_permissions = $this->buildPermissionsYaml();
return $this->sortPermissions($all_permissions);
}
/**
* {@inheritdoc}
*/
public function moduleProvidesPermissions($module_name) {
// @todo Static cache this information.
// https://www.drupal.org/node/2339487
$permissions = $this->getPermissions();
foreach ($permissions as $permission) {
if ($permission['provider'] == $module_name) {
return TRUE;
}
}
return FALSE;
}
/**
* Builds all permissions provided by .permissions.yml files.
*
* @return array[]
* An array with the same structure as
* PermissionHandlerInterface::getPermissions().
*
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
protected function buildPermissionsYaml() {
$all_permissions = [];
$all_callback_permissions = [];
foreach ($this->getYamlDiscovery()->findAll() as $provider => $permissions) {
// The top-level 'permissions_callback' is a list of methods in callable
// syntax, see \Drupal\Core\Utility\CallableResolver. These methods
// should return an array of permissions in the same structure.
if (isset($permissions['permission_callbacks'])) {
foreach ($permissions['permission_callbacks'] as $permission_callback) {
$callback = $this->callableResolver->getCallableFromDefinition($permission_callback);
if ($callback_permissions = call_user_func($callback)) {
// Add any callback permissions to the array of permissions. Any
// defaults can then get processed below.
foreach ($callback_permissions as $name => $callback_permission) {
if (!is_array($callback_permission)) {
$callback_permission = [
'title' => $callback_permission,
];
}
$callback_permission += [
'description' => NULL,
'provider' => $provider,
];
$all_callback_permissions[$name] = $callback_permission;
}
}
}
unset($permissions['permission_callbacks']);
}
foreach ($permissions as &$permission) {
if (!is_array($permission)) {
$permission = [
'title' => $permission,
];
}
$permission['title'] = $this->t($permission['title']);
$permission['description'] = isset($permission['description']) ? $this->t($permission['description']) : NULL;
$permission['provider'] = !empty($permission['provider']) ? $permission['provider'] : $provider;
}
$all_permissions += $permissions;
}
return $all_permissions + $all_callback_permissions;
}
/**
* Sorts the given permissions by provider name and title.
*
* @param array $all_permissions
* The permissions to be sorted.
*
* @return array[]
* An array with the same structure as
* PermissionHandlerInterface::getPermissions().
*
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
protected function sortPermissions(array $all_permissions = []) {
// Get a list of all the modules providing permissions and sort by
// display name.
$modules = $this->getModuleNames();
uasort($all_permissions, function (array $permission_a, array $permission_b) use ($modules) {
if ($modules[$permission_a['provider']] == $modules[$permission_b['provider']]) {
return $permission_a['title'] <=> $permission_b['title'];
}
else {
return $modules[$permission_a['provider']] <=> $modules[$permission_b['provider']];
}
});
return $all_permissions;
}
/**
* Returns all module names.
*
* @return string[]
* Returns the human readable names of all modules keyed by machine name.
*/
protected function getModuleNames() {
$modules = [];
foreach (array_keys($this->moduleHandler->getModuleList()) as $module) {
$modules[$module] = $this->moduleExtensionList->getName($module);
}
asort($modules);
return $modules;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Drupal\user;
/**
* Defines an interface to list available permissions.
*/
interface PermissionHandlerInterface {
/**
* Gets all available permissions.
*
* @return array
* An array whose keys are permission names and whose corresponding values
* are arrays containing the following key-value pairs:
* - title: The human-readable name of the permission, to be shown on the
* permission administration page. This should be wrapped in the t()
* function so it can be translated.
* - description: (optional) A description of what the permission does. This
* should be wrapped in the t() function so it can be translated.
* - restrict access: (optional) A boolean which can be set to TRUE to
* indicate that site administrators should restrict access to this
* permission to trusted users. This should be used for permissions that
* have inherent security risks across a variety of potential use cases
* (for example, the "administer filters" and "bypass node access"
* permissions provided by Drupal core). When set to TRUE, a standard
* warning message defined in user_admin_permissions() will be displayed
* with the permission on the permission administration page. Defaults
* to FALSE.
* - warning: (optional) A translated warning message to display for this
* permission on the permission administration page. This warning
* overrides the automatic warning generated by 'restrict access' being
* set to TRUE. This should rarely be used, since it is important for all
* permissions to have a clear, consistent security warning that is the
* same across the site. Use the 'description' key instead to provide any
* information that is specific to the permission you are defining.
* - dependencies: (optional) An array of dependency entities used when
* building this permission, structured in the same way as the return
* of ConfigEntityInterface::calculateDependencies(). For example, if this
* permission was generated as effect of the existence of node type
* 'article', then value of the dependency key is:
* @code
* 'dependencies' => ['config' => ['node.type.article']]
* @endcode
* The module providing this permission doesn't have to be added as a
* dependency. It is automatically parsed, stored and retrieved from the
* 'provider' key.
* - provider: The provider name of the permission. This is set
* automatically to the module that provides the permission.yml file.
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
*/
public function getPermissions();
/**
* Determines whether a module provides some permissions.
*
* @param string $module_name
* The module name.
*
* @return bool
* Returns TRUE if the module provides some permissions, otherwise FALSE.
*/
public function moduleProvidesPermissions($module_name);
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Adds a role to a user.
*/
#[Action(
id: 'user_add_role_action',
label: new TranslatableMarkup('Add a role to the selected users'),
type: 'user'
)]
class AddRoleUser extends ChangeUserRoleBase {
/**
* {@inheritdoc}
*/
public function execute($account = NULL) {
$rid = $this->configuration['rid'];
// Skip adding the role to the user if they already have it.
if ($account !== FALSE && !$account->hasRole($rid)) {
// For efficiency manually save the original account before applying
// any changes.
$account->original = clone $account;
$account->addRole($rid)->save();
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Blocks a user.
*/
#[Action(
id: 'user_block_user_action',
label: new TranslatableMarkup('Block the selected users'),
type: 'user'
)]
class BlockUser extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($account = NULL) {
// Skip blocking user if they are already blocked.
if ($account !== FALSE && $account->isActive()) {
// For efficiency manually save the original account before applying any
// changes.
$account->original = clone $account;
$account->block();
$account->save();
}
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\user\UserInterface $object */
$access = $object->status->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Cancels a user account.
*/
#[Action(
id: 'user_cancel_user_action',
label: new TranslatableMarkup('Cancel the selected user accounts'),
type: 'user',
confirm_form_route_name: 'user.multiple_cancel_confirm'
)]
class CancelUser extends ActionBase implements ContainerFactoryPluginInterface {
/**
* The tempstore factory.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a CancelUser object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
$this->currentUser = $current_user;
$this->tempStoreFactory = $temp_store_factory;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('tempstore.private'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
$this->tempStoreFactory->get('user_user_operations_cancel')->set($this->currentUser->id(), $entities);
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
$this->executeMultiple([$object]);
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\user\UserInterface $object */
return $object->access('delete', $account, $return_as_object);
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for operations to change a user's role.
*/
abstract class ChangeUserRoleBase extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
use DependencyTrait;
/**
* The user role entity type.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeInterface $entity_type) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityType = $entity_type;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getDefinition('user_role')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'rid' => '',
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$roles = array_map(fn(RoleInterface $role) => $role->label(), $roles);
$form['rid'] = [
'#type' => 'radios',
'#title' => $this->t('Role'),
'#options' => $roles,
'#default_value' => $this->configuration['rid'],
'#required' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['rid'] = $form_state->getValue('rid');
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
if (!empty($this->configuration['rid'])) {
$prefix = $this->entityType->getConfigPrefix() . '.';
$this->addDependency('config', $prefix . $this->configuration['rid']);
}
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\user\UserInterface $object */
$access = $object->access('update', $account, TRUE)
->andIf($object->roles->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Removes a role from a user.
*/
#[Action(
id: 'user_remove_role_action',
label: new TranslatableMarkup('Remove a role from the selected users'),
type: 'user'
)]
class RemoveRoleUser extends ChangeUserRoleBase {
/**
* {@inheritdoc}
*/
public function execute($account = NULL) {
$rid = $this->configuration['rid'];
// Skip removing the role from the user if they already don't have it.
if ($account !== FALSE && $account->hasRole($rid)) {
// For efficiency manually save the original account before applying
// any changes.
$account->original = clone $account;
$account->removeRole($rid)->save();
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\user\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Action\Attribute\Action;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Unblocks a user.
*/
#[Action(
id: 'user_unblock_user_action',
label: new TranslatableMarkup('Unblock the selected users'),
type: 'user'
)]
class UnblockUser extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($account = NULL) {
// Skip unblocking user if they are already unblocked.
if ($account !== FALSE && $account->isBlocked()) {
$account->activate();
$account->save();
}
}
/**
* {@inheritdoc}
*/
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\user\UserInterface $object */
$access = $object->status->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Drupal\user\Plugin\Block;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\user\Form\UserLoginForm;
use Drupal\user\UserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'User login' block.
*/
#[Block(
id: "user_login_block",
admin_label: new TranslatableMarkup("User login"),
category: new TranslatableMarkup("Forms")
)]
class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
use RedirectDestinationTrait;
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a new UserLoginBlock instance.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Drupal\Core\Form\FormBuilderInterface $formBuilder
* The form builder.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
RouteMatchInterface $route_match,
protected ?FormBuilderInterface $formBuilder = NULL,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
if (!$formBuilder) {
@trigger_error('Calling ' . __METHOD__ . ' without the $formBuilder argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3159776', E_USER_DEPRECATED);
$this->formBuilder = \Drupal::service('form_builder');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match'),
$container->get('form_builder')
);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
$route_name = $this->routeMatch->getRouteName();
if ($account->isAnonymous() && !in_array($route_name, ['user.login', 'user.logout'])) {
return AccessResult::allowed()
->addCacheContexts(['route.name', 'user.roles:anonymous']);
}
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public function build() {
$form = $this->formBuilder->getForm(UserLoginForm::class);
unset($form['name']['#attributes']['autofocus']);
$form['name']['#size'] = 15;
$form['pass']['#size'] = 15;
// Instead of setting an actual action URL, we set the placeholder, which
// will be replaced at the very last moment. This ensures forms with
// dynamically generated action URLs don't have poor cacheability.
// Use the proper API to generate the placeholder, when we have one. See
// https://www.drupal.org/node/2562341. The placeholder uses a fixed string
// that is
// Crypt::hashBase64('\Drupal\user\Plugin\Block\UserLoginBlock::build');
// This is based on the implementation in
// \Drupal\Core\Form\FormBuilder::prepareForm(), but the user login block
// requires different behavior for the destination query argument.
// cspell:disable-next-line
$placeholder = 'form_action_p_4r8ITd22yaUvXM6SzwrSe9rnQWe48hz9k1Sxto3pBvE';
$form['#attached']['placeholders'][$placeholder] = [
'#lazy_builder' => ['\Drupal\user\Plugin\Block\UserLoginBlock::renderPlaceholderFormAction', []],
];
$form['#action'] = $placeholder;
// Build action links.
$items = [];
if (\Drupal::config('user.settings')->get('register') != UserInterface::REGISTER_ADMINISTRATORS_ONLY) {
$items['create_account'] = [
'#type' => 'link',
'#title' => $this->t('Create new account'),
'#url' => Url::fromRoute('user.register', [], [
'attributes' => [
'title' => $this->t('Create a new user account.'),
'class' => ['create-account-link'],
],
]),
];
}
$items['request_password'] = [
'#type' => 'link',
'#title' => $this->t('Reset your password'),
'#url' => Url::fromRoute('user.pass', [], [
'attributes' => [
'title' => $this->t('Send password reset instructions via email.'),
'class' => ['request-password-link'],
],
]),
];
return [
'user_login_form' => $form,
'user_links' => [
'#theme' => 'item_list',
'#items' => $items,
],
];
}
/**
* #lazy_builder callback; renders a form action URL including destination.
*
* @return array
* A renderable array representing the form action.
*
* @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
*/
public static function renderPlaceholderFormAction() {
return [
'#type' => 'markup',
'#markup' => UrlHelper::filterBadProtocol(Url::fromRoute('<current>', [], ['query' => \Drupal::destination()->getAsArray(), 'external' => FALSE])->toString()),
'#cache' => ['contexts' => ['url.path', 'url.query_args']],
];
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['renderPlaceholderFormAction'];
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\user\Plugin\Condition;
use Drupal\Component\Utility\Html;
use Drupal\Core\Condition\Attribute\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Provides a 'User Role' condition.
*/
#[Condition(
id: "user_role",
label: new TranslatableMarkup("User Role"),
context_definitions: [
"user" => new EntityContextDefinition(
data_type: "entity:user",
label: new TranslatableMarkup("User"),
),
],
)]
class UserRole extends ConditionPluginBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['roles'] = [
'#type' => 'checkboxes',
'#title' => $this->t('When the user has the following roles'),
'#default_value' => $this->configuration['roles'],
'#options' => array_map(fn(RoleInterface $role) => Html::escape($role->label()), Role::loadMultiple()),
'#description' => $this->t('If you select no roles, the condition will evaluate to TRUE for all users.'),
];
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'roles' => [],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['roles'] = array_filter($form_state->getValue('roles'));
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function summary() {
// Use the role labels. They will be sanitized below.
$roles = array_map(fn(RoleInterface $role) => $role->label(), Role::loadMultiple());
$roles = array_intersect_key($roles, $this->configuration['roles']);
if (count($roles) > 1) {
$roles = implode(', ', $roles);
}
else {
$roles = reset($roles);
}
if (!empty($this->configuration['negate'])) {
return $this->t('The user is not a member of @roles', ['@roles' => $roles]);
}
else {
return $this->t('The user is a member of @roles', ['@roles' => $roles]);
}
}
/**
* {@inheritdoc}
*/
public function evaluate() {
if (empty($this->configuration['roles']) && !$this->isNegated()) {
return TRUE;
}
$user = $this->getContextValue('user');
return (bool) array_intersect($this->configuration['roles'], $user->getRoles());
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
// Optimize cache context, if a user cache context is provided, only use
// user.roles, since that's the only part this condition cares about.
$contexts = [];
foreach (parent::getCacheContexts() as $context) {
$contexts[] = $context == 'user' ? 'user.roles' : $context;
}
return $contexts;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\user\Plugin\Derivative;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local task definitions for all entity bundles.
*/
class UserLocalTask extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a UserLocalTask object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
$entity_definitions = $this->entityTypeManager->getDefinitions();
foreach ($entity_definitions as $bundle_type_id => $bundle_entity_type) {
if (!$bundle_entity_type->hasLinkTemplate('entity-permissions-form')) {
continue;
}
if (!$entity_type_id = $bundle_entity_type->getBundleOf()) {
continue;
}
$entity_type = $entity_definitions[$entity_type_id];
if (!$base_route = $entity_type->get('field_ui_base_route')) {
continue;
}
$this->derivatives["permissions_$bundle_type_id"] = [
'route_name' => "entity.$bundle_type_id.entity_permissions_form",
'weight' => 10,
'title' => $this->t('Manage permissions'),
'base_route' => $base_route,
] + $base_plugin_definition;
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View File

@@ -0,0 +1,266 @@
<?php
namespace Drupal\user\Plugin\EntityReferenceSelection;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Attribute\EntityReferenceSelection;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides specific access control for the user entity type.
*/
#[EntityReferenceSelection(
id: "default:user",
label: new TranslatableMarkup("User selection"),
entity_types: ["user"],
group: "default",
weight: 1
)]
class UserSelection extends DefaultSelection {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Constructs a new UserSelection object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, Connection $connection, ?EntityFieldManagerInterface $entity_field_manager = NULL, ?EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, ?EntityRepositoryInterface $entity_repository = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $module_handler, $current_user, $entity_field_manager, $entity_type_bundle_info, $entity_repository);
$this->connection = $connection;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('current_user'),
$container->get('database'),
$container->get('entity_field.manager'),
$container->get('entity_type.bundle.info'),
$container->get('entity.repository')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'filter' => [
'type' => '_none',
'role' => NULL,
],
'include_anonymous' => TRUE,
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$configuration = $this->getConfiguration();
$form['include_anonymous'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include the anonymous user.'),
'#default_value' => $configuration['include_anonymous'],
];
// Add user specific filter options.
$form['filter']['type'] = [
'#type' => 'select',
'#title' => $this->t('Filter by'),
'#options' => [
'_none' => $this->t('- None -'),
'role' => $this->t('User role'),
],
// Use a form process callback to build #ajax property properly and also
// to avoid code duplication.
// @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::fieldSettingsAjaxProcess()
'#ajax' => TRUE,
'#limit_validation_errors' => [],
'#default_value' => $configuration['filter']['type'],
];
$form['filter']['settings'] = [
'#type' => 'container',
'#attributes' => ['class' => ['entity_reference-settings']],
'#process' => [['\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', 'formProcessMergeParent']],
];
if ($configuration['filter']['type'] == 'role') {
$roles = Role::loadMultiple();
unset($roles[RoleInterface::ANONYMOUS_ID]);
unset($roles[RoleInterface::AUTHENTICATED_ID]);
$roles = array_map(fn(RoleInterface $role) => $role->label(), $roles);
$form['filter']['settings']['role'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Restrict to the selected roles'),
'#required' => TRUE,
'#options' => $roles,
'#default_value' => $configuration['filter']['role'],
];
}
$form += parent::buildConfigurationForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
$configuration = $this->getConfiguration();
// Filter out the Anonymous user if the selection handler is configured to
// exclude it.
if (!$configuration['include_anonymous']) {
$query->condition('uid', 0, '<>');
}
// The user entity doesn't have a label column.
if (isset($match)) {
$query->condition('name', $match, $match_operator);
}
// Filter by role.
if (!empty($configuration['filter']['role'])) {
$query->condition('roles', $configuration['filter']['role'], 'IN');
}
// Adding the permission check is sadly insufficient for users: core
// requires us to also know about the concept of 'blocked' and 'active'.
if (!$this->currentUser->hasPermission('administer users')) {
$query->condition('status', 1);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$user = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable user, it needs to be active.
if (!$this->currentUser->hasPermission('administer users')) {
/** @var \Drupal\user\UserInterface $user */
$user->activate();
}
return $user;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if ($role = $this->getConfiguration()['filter']['role']) {
$entities = array_filter($entities, function ($user) use ($role) {
/** @var \Drupal\user\UserInterface $user */
return !empty(array_intersect($user->getRoles(), $role));
});
}
if (!$this->currentUser->hasPermission('administer users')) {
$entities = array_filter($entities, function ($user) {
/** @var \Drupal\user\UserInterface $user */
return $user->isActive();
});
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function entityQueryAlter(SelectInterface $query) {
parent::entityQueryAlter($query);
// Bail out early if we do not need to match the Anonymous user.
if (!$this->getConfiguration()['include_anonymous']) {
return;
}
if ($this->currentUser->hasPermission('administer users')) {
// In addition, if the user is administrator, we need to make sure to
// match the anonymous user, that doesn't actually have a name in the
// database.
$conditions = &$query->conditions();
foreach ($conditions as $key => $condition) {
if ($key !== '#conjunction' && is_string($condition['field']) && $condition['field'] === 'users_field_data.name') {
// Remove the condition.
unset($conditions[$key]);
// Re-add the condition and a condition on uid = 0 so that we end up
// with a query in the form:
// WHERE (name LIKE :name) OR (:anonymous_name LIKE :name AND uid = 0)
$or = $this->connection->condition('OR');
$or->condition($condition['field'], $condition['value'], $condition['operator']);
// Sadly, the Database layer doesn't allow us to build a condition
// in the form ':placeholder = :placeholder2', because the 'field'
// part of a condition is always escaped.
// As a (cheap) workaround, we separately build a condition with no
// field, and concatenate the field and the condition separately.
$value_part = $this->connection->condition('AND');
$value_part->condition('anonymous_name', $condition['value'], $condition['operator']);
$value_part->compile($this->connection, $query);
$or->condition(($this->connection->condition('AND'))
->where(str_replace($query->escapeField('anonymous_name'), ':anonymous_name', (string) $value_part), $value_part->arguments() + [':anonymous_name' => \Drupal::config('user.settings')->get('anonymous')])
->condition('base_table.uid', 0)
);
$query->condition($or);
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Drupal\user\Plugin\Field\FieldFormatter;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'author' formatter.
*/
#[FieldFormatter(
id: 'author',
label: new TranslatableMarkup('Author'),
description: new TranslatableMarkup('Display the referenced author user entity.'),
field_types: [
'entity_reference',
],
)]
class AuthorFormatter extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
$elements[$delta] = [
'#theme' => 'username',
'#account' => $entity,
'#link_options' => ['attributes' => ['rel' => 'author']],
'#cache' => [
'tags' => $entity->getCacheTags(),
],
];
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getFieldStorageDefinition()->getSetting('target_type') == 'user';
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity) {
return $entity->access('view label', NULL, TRUE);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\user\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'user_name' formatter.
*/
#[FieldFormatter(
id: 'user_name',
label: new TranslatableMarkup('User name'),
description: new TranslatableMarkup('Display the user or author name.'),
field_types: [
'string',
],
)]
class UserNameFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
$options = parent::defaultSettings();
$options['link_to_entity'] = TRUE;
return $options;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form = parent::settingsForm($form, $form_state);
$form['link_to_entity'] = [
'#type' => 'checkbox',
'#title' => $this->t('Link to the user'),
'#default_value' => $this->getSetting('link_to_entity'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
/** @var \Drupal\user\UserInterface $user */
if ($user = $item->getEntity()) {
if ($this->getSetting('link_to_entity')) {
$elements[$delta] = [
'#theme' => 'username',
'#account' => $user,
'#link_options' => ['attributes' => ['rel' => 'user']],
'#cache' => [
'tags' => $user->getCacheTags(),
],
];
}
else {
$elements[$delta] = [
'#markup' => $user->getDisplayName(),
'#cache' => [
'tags' => $user->getCacheTags(),
],
];
}
}
}
return $elements;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getTargetEntityTypeId() === 'user' && $field_definition->getName() === 'name';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\user\Plugin\LanguageNegotiation;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\language\Attribute\LanguageNegotiation;
use Drupal\language\LanguageNegotiationMethodBase;
use Symfony\Component\HttpFoundation\Request;
/**
* Class for identifying language from the user preferences.
*/
#[LanguageNegotiation(
id: LanguageNegotiationUser::METHOD_ID,
name: new TranslatableMarkup('User'),
weight: -4,
description: new TranslatableMarkup("Follow the user's language preference.")
)]
class LanguageNegotiationUser extends LanguageNegotiationMethodBase {
/**
* The language negotiation method id.
*/
const METHOD_ID = 'language-user';
/**
* {@inheritdoc}
*/
public function getLangcode(?Request $request = NULL) {
$langcode = NULL;
// User preference (only for authenticated users).
if ($this->languageManager && $this->currentUser->isAuthenticated()) {
$preferred_langcode = $this->currentUser->getPreferredLangcode(FALSE);
$languages = $this->languageManager->getLanguages();
if (!empty($preferred_langcode) && isset($languages[$preferred_langcode])) {
$langcode = $preferred_langcode;
}
}
// No language preference from the user.
return $langcode;
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Drupal\user\Plugin\LanguageNegotiation;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\PathProcessor\PathProcessorManager;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\AdminContext;
use Drupal\Core\Routing\StackedRouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\language\Attribute\LanguageNegotiation;
use Drupal\language\LanguageNegotiationMethodBase;
use Drupal\Core\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Exception\ExceptionInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
/**
* Identifies admin language from the user preferences.
*/
#[LanguageNegotiation(
id: LanguageNegotiationUserAdmin::METHOD_ID,
name: new TranslatableMarkup('Account administration pages'),
types: [LanguageInterface::TYPE_INTERFACE],
weight: -10,
description: new TranslatableMarkup('Account administration pages language setting.')
)]
class LanguageNegotiationUserAdmin extends LanguageNegotiationMethodBase implements ContainerFactoryPluginInterface {
/**
* The language negotiation method id.
*/
const METHOD_ID = 'language-user-admin';
/**
* The admin context.
*
* @var \Drupal\Core\Routing\AdminContext
*/
protected $adminContext;
/**
* The router.
*
* This is only used when called from an event subscriber, before the request
* has been populated with the route info.
*
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
*/
protected $router;
/**
* The path processor manager.
*
* @var \Drupal\Core\PathProcessor\PathProcessorManager
*/
protected $pathProcessorManager;
/**
* The stacked route match.
*
* @var \Drupal\Core\Routing\StackedRouteMatchInterface
*/
protected $stackedRouteMatch;
/**
* Constructs a new LanguageNegotiationUserAdmin instance.
*
* @param \Drupal\Core\Routing\AdminContext $admin_context
* The admin context.
* @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $router
* The router.
* @param \Drupal\Core\PathProcessor\PathProcessorManager $path_processor_manager
* The path processor manager.
* @param \Drupal\Core\Routing\StackedRouteMatchInterface $stacked_route_match
* The stacked route match.
*/
public function __construct(AdminContext $admin_context, UrlMatcherInterface $router, PathProcessorManager $path_processor_manager, StackedRouteMatchInterface $stacked_route_match) {
$this->adminContext = $admin_context;
$this->router = $router;
$this->pathProcessorManager = $path_processor_manager;
$this->stackedRouteMatch = $stacked_route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('router.admin_context'),
$container->get('router'),
$container->get('path_processor_manager'),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function getLangcode(?Request $request = NULL) {
$langcode = NULL;
// User preference (only for administrators).
if (($this->currentUser->hasPermission('access administration pages') || $this->currentUser->hasPermission('view the administration theme')) && ($preferred_admin_langcode = $this->currentUser->getPreferredAdminLangcode(FALSE)) && $this->isAdminPath($request)) {
$langcode = $preferred_admin_langcode;
}
// Not an admin, no admin language preference or not on an admin path.
return $langcode;
}
/**
* Checks whether the given path is an administrative one.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return bool
* TRUE if the path is administrative, FALSE otherwise.
*/
protected function isAdminPath(Request $request) {
$result = FALSE;
if ($request && $this->adminContext) {
// If called from an event subscriber, the request may not have the route
// object yet (it is still being built), so use the router to look up
// based on the path.
$route_match = $this->stackedRouteMatch->getRouteMatchFromRequest($request);
if ($route_match && !$route_object = $route_match->getRouteObject()) {
try {
// Some inbound path processors make changes to the request. Make a
// copy as we're not actually routing the request so we do not want to
// make changes.
$cloned_request = clone $request;
// Process the path as an inbound path. This will remove any language
// prefixes and other path components that inbound processing would
// clear out, so we can attempt to load the route clearly.
$path = $this->pathProcessorManager->processInbound(urldecode(rtrim($cloned_request->getPathInfo(), '/')), $cloned_request);
$attributes = $this->router->match($path);
}
catch (ExceptionInterface | HttpException) {
return FALSE;
}
$route_object = $attributes[RouteObjectInterface::ROUTE_OBJECT];
}
$result = $this->adminContext->isAdminRoute($route_object);
}
return $result;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Drupal\user\Plugin\Menu;
use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A menu link that shows "Log in" or "Log out" as appropriate.
*/
class LoginLogoutMenuLink extends MenuLinkDefault {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new LoginLogoutMenuLink.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
* The static override storage.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $static_override);
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('menu_link.static.overrides'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
if ($this->currentUser->isAuthenticated()) {
return $this->t('Log out');
}
else {
return $this->t('Log in');
}
}
/**
* {@inheritdoc}
*/
public function getRouteName() {
if ($this->currentUser->isAuthenticated()) {
return 'user.logout';
}
else {
return 'user.login';
}
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['user.roles:authenticated'];
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Drupal\user\Plugin\Search;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\PagerSelectExtender;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\search\Attribute\Search;
use Drupal\search\Plugin\SearchPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Executes a keyword search for users against the {users} database table.
*/
#[Search(
id: 'user_search',
title: new TranslatableMarkup('Users'),
)]
class UserSearch extends SearchPluginBase implements AccessibleInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('database'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('current_user'),
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* Creates a UserSearch object.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, array $configuration, $plugin_id, $plugin_definition) {
$this->database = $database;
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->addCacheTags(['user_list']);
}
/**
* {@inheritdoc}
*/
public function access($operation = 'view', ?AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIf(!empty($account) && $account->hasPermission('access user profiles'))->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function execute() {
$results = [];
if (!$this->isSearchExecutable()) {
return $results;
}
// Process the keywords.
$keys = $this->keywords;
// Escape for LIKE matching.
$keys = $this->database->escapeLike($keys);
// Replace wildcards with MySQL/PostgreSQL wildcards.
$keys = preg_replace('!\*+!', '%', $keys);
// Run the query to find matching users.
$query = $this->database
->select('users_field_data', 'users')
->extend(PagerSelectExtender::class);
$query->fields('users', ['uid']);
$query->condition('default_langcode', 1);
if ($this->currentUser->hasPermission('administer users')) {
// Administrators can also search in the otherwise private email field,
// and they don't need to be restricted to only active users.
$query->fields('users', ['mail']);
$query->condition($query->orConditionGroup()
->condition('name', '%' . $keys . '%', 'LIKE')
->condition('mail', '%' . $keys . '%', 'LIKE')
);
}
else {
// Regular users can only search via usernames, and we do not show them
// blocked accounts.
$query->condition('name', '%' . $keys . '%', 'LIKE')
->condition('status', 1);
}
$uids = $query
->limit(15)
->execute()
->fetchCol();
$accounts = $this->entityTypeManager->getStorage('user')->loadMultiple($uids);
foreach ($accounts as $account) {
$result = [
'title' => $account->getDisplayName(),
'link' => $account->toUrl('canonical', ['absolute' => TRUE])->toString(),
];
if ($this->currentUser->hasPermission('administer users')) {
$result['title'] .= ' (' . $account->getEmail() . ')';
}
$this->addCacheableDependency($account);
$results[] = $result;
}
return $results;
}
/**
* {@inheritdoc}
*/
public function getHelp() {
$help = [
'list' => [
'#theme' => 'item_list',
'#items' => [
$this->t('User search looks for user names and partial user names. Example: mar would match usernames mar, delmar, and maryjane.'),
$this->t('You can use * as a wildcard within your keyword. Example: m*r would match user names mar, delmar, and elementary.'),
],
],
];
return $help;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Checks if the plain text password is provided for editing a protected field.
*/
#[Constraint(
id: 'ProtectedUserField',
label: new TranslatableMarkup('Password required for protected field change', [], ['context' => 'Validation'])
)]
class ProtectedUserFieldConstraint extends SymfonyConstraint {
/**
* Violation message.
*
* @var string
*/
public $message = "Your current password is missing or incorrect; it's required to change the %name.";
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the ProtectedUserFieldConstraint constraint.
*/
class ProtectedUserFieldConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* User storage handler.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* Constructs the object.
*
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage handler.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
*/
public function __construct(UserStorageInterface $user_storage, AccountProxyInterface $current_user) {
$this->userStorage = $user_storage;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('user'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*
* phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
* @return void
*/
public function validate($items, Constraint $constraint) {
if (!isset($items)) {
return;
}
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
$field = $items->getFieldDefinition();
/** @var \Drupal\user\UserInterface $account */
$account = $items->getEntity();
if (!isset($account) || !empty($account->_skipProtectedUserFieldConstraint)) {
// Looks like we are validating a field not being part of a user, or the
// constraint should be skipped, so do nothing.
return;
}
// Only validate for existing entities and if this is the current user.
if (!$account->isNew() && $account->id() == $this->currentUser->id()) {
/** @var \Drupal\user\UserInterface $account_unchanged */
$account_unchanged = $this->userStorage
->loadUnchanged($account->id());
$changed = FALSE;
// Special case for the password, it being empty means that the existing
// password should not be changed, ignore empty password fields.
$value = $items->value;
if ($field->getName() != 'pass' || !empty($value)) {
// Compare the values of the field this is being validated on.
$changed = $items->getValue() != $account_unchanged->get($field->getName())->getValue();
}
if ($changed && (!$account->checkExistingPassword($account_unchanged))) {
$this->context->addViolation($constraint->message, ['%name' => $field->getLabel()]);
}
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Checks if the user's email address is provided if required.
*
* The user mail field is NOT required if account originally had no mail set
* and the user performing the edit has 'administer users' permission.
* This allows users without email address to be edited and deleted.
*/
#[Constraint(
id: 'UserMailRequired',
label: new TranslatableMarkup('User email required', [], ['context' => 'Validation'])
)]
class UserMailRequired extends SymfonyConstraint {
/**
* Violation message. Use the same message as FormValidator.
*
* Note that the name argument is not sanitized so that translators only have
* one string to translate. The name is sanitized in self::validate().
*
* @var string
*/
public $message = '@name field is required.';
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraint;
/**
* Checks if the user's email address is provided if required.
*
* The user mail field is NOT required if account originally had no mail set
* and the user performing the edit has 'administer users' permission.
* This allows users without email address to be edited and deleted.
*/
class UserMailRequiredValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*
* phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
* @return void
*/
public function validate($items, Constraint $constraint) {
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
/** @var \Drupal\user\UserInterface $account */
$account = $items->getEntity();
if (!isset($account)) {
return;
}
$existing_value = NULL;
// Only validate for existing user.
if (!$account->isNew()) {
$account_unchanged = \Drupal::entityTypeManager()
->getStorage('user')
->loadUnchanged($account->id());
$existing_value = $account_unchanged->getEmail();
}
$required = !(!$existing_value && \Drupal::currentUser()->hasPermission('administer users'));
if ($required && (!isset($items) || $items->isEmpty())) {
$this->context->addViolation($constraint->message, ['@name' => $account->getFieldDefinition('mail')->getLabel()]);
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldConstraint;
/**
* Checks if a user's email address is unique on the site.
*/
#[Constraint(
id: 'UserMailUnique',
label: new TranslatableMarkup('User email unique', [], ['context' => 'Validation'])
)]
class UserMailUnique extends UniqueFieldConstraint {
public $message = 'The email address %value is already taken.';
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Checks if a value is a valid user name.
*/
#[Constraint(
id: 'UserName',
label: new TranslatableMarkup('User name', [], ['context' => 'Validation'])
)]
class UserNameConstraint extends SymfonyConstraint {
public $emptyMessage = 'You must enter a username.';
public $spaceBeginMessage = 'The username cannot begin with a space.';
public $spaceEndMessage = 'The username cannot end with a space.';
public $multipleSpacesMessage = 'The username cannot contain multiple spaces in a row.';
public $illegalMessage = 'The username contains an illegal character.';
public $tooLongMessage = 'The username %name is too long: it must be %max characters or less.';
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\user\UserInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the UserName constraint.
*/
class UserNameConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($items, Constraint $constraint) {
if (empty($items) || ($items instanceof FieldItemListInterface && $items->isEmpty())) {
$this->context->addViolation($constraint->emptyMessage);
return;
}
$name = $items instanceof FieldItemListInterface ? $items->first()->value : $items;
if (str_starts_with($name, ' ')) {
$this->context->addViolation($constraint->spaceBeginMessage);
}
if (str_ends_with($name, ' ')) {
$this->context->addViolation($constraint->spaceEndMessage);
}
if (str_contains($name, ' ')) {
$this->context->addViolation($constraint->multipleSpacesMessage);
}
if (preg_match('/[^\x{80}-\x{F7} a-z0-9@+_.\'-]/i', $name)
|| preg_match(
// Non-printable ISO-8859-1 + NBSP
'/[\x{80}-\x{A0}' .
// Soft-hyphen
'\x{AD}' .
// Various space characters
'\x{2000}-\x{200F}' .
// Bidirectional text overrides
'\x{2028}-\x{202F}' .
// Various text hinting characters
'\x{205F}-\x{206F}' .
// Byte order mark
'\x{FEFF}' .
// Full-width latin
'\x{FF01}-\x{FF60}' .
// Replacement characters
'\x{FFF9}-\x{FFFD}' .
// NULL byte and control characters
'\x{0}-\x{1F}]/u',
$name)
) {
$this->context->addViolation($constraint->illegalMessage);
}
if (mb_strlen($name) > UserInterface::USERNAME_MAX_LENGTH) {
$this->context->addViolation($constraint->tooLongMessage, ['%name' => $name, '%max' => UserInterface::USERNAME_MAX_LENGTH]);
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\user\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldConstraint;
/**
* Checks if a user name is unique on the site.
*/
#[Constraint(
id: 'UserNameUnique',
label: new TranslatableMarkup('User name unique', [], ['context' => 'Validation'])
)]
class UserNameUnique extends UniqueFieldConstraint {
public $message = 'The username %value is already taken.';
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Drupal\user\Plugin\migrate;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\Migration;
/**
* Plugin class for user migrations dealing with profile values.
*/
class ProfileValues extends Migration {
/**
* Flag determining whether the process plugin has been initialized.
*
* @var bool
*/
protected $init = FALSE;
/**
* {@inheritdoc}
*/
public function getProcess() {
if (!$this->init) {
$this->init = TRUE;
$definition['source'] = [
'plugin' => 'profile_field',
'ignore_map' => TRUE,
] + $this->source;
$definition['destination']['plugin'] = 'null';
$definition['idMap']['plugin'] = 'null';
try {
$this->checkRequirements();
$profile_field_migration = $this->migrationPluginManager->createStubMigration($definition);
$migrate_executable = new MigrateExecutable($profile_field_migration);
$source_plugin = $profile_field_migration->getSourcePlugin();
$source_plugin->checkRequirements();
foreach ($source_plugin as $row) {
$name = $row->getSourceProperty('name');
$fid = $row->getSourceProperty('fid');
// The user profile field name can be greater than 32 characters. Use
// the migrated profile field name in the process pipeline.
$configuration =
[
'migration' => 'user_profile_field',
'source_ids' => $fid,
'no_stub' => TRUE,
];
$plugin = $this->processPluginManager->createInstance('migration_lookup', $configuration, $profile_field_migration);
$new_value = $plugin->transform($fid, $migrate_executable, $row, 'tmp');
if (isset($new_value[1])) {
// Set the destination to the migrated profile field name.
$this->process[$new_value[1]] = $name;
}
}
}
catch (RequirementsException $e) {
// The checkRequirements() call will fail when the profile module does
// not exist on the source site, or if the required migrations have not
// yet run.
}
}
return parent::getProcess();
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\user\Plugin\migrate;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate_drupal\Plugin\migrate\FieldMigration;
/**
* Plugin class for Drupal 7 user migrations dealing with fields and profiles.
*/
class User extends FieldMigration {
/**
* {@inheritdoc}
*/
public function getProcess() {
if (!$this->init) {
$this->init = TRUE;
$this->fieldDiscovery->addEntityFieldProcesses($this, 'user');
$definition = [
'source' => [
'plugin' => 'profile_field',
'ignore_map' => TRUE,
],
'idMap' => [
'plugin' => 'null',
],
'destination' => [
'plugin' => 'null',
],
];
try {
$profile_migration = $this->migrationPluginManager->createStubMigration($definition);
// Ensure that Profile is enabled in the source DB.
$profile_migration->checkRequirements();
foreach ($profile_migration->getSourcePlugin() as $row) {
$name = $row->getSourceProperty('name');
$this->process[$name] = $name;
}
}
catch (RequirementsException $e) {
// The checkRequirements() call will fail when the profile module does
// not exist on the source site.
}
}
return parent::getProcess();
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Drupal\user\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EmailItem;
use Drupal\Core\Password\PasswordInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Row;
use Drupal\user\UserNameItem;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a destination plugin for migrating user entities.
*
* Example:
*
* The example below migrates users and preserves original passwords from a
* source that has passwords as MD5 hashes without salt. The passwords will be
* salted and re-hashed before they are saved to the destination Drupal
* database. The MD5 hash used in the example is a hash of 'password'.
*
* The example uses the EmbeddedDataSource source plugin for the sake of
* simplicity. The mapping between old user_ids and new Drupal uids is saved in
* the migration map table.
* @code
* id: custom_user_migration
* label: Custom user migration
* source:
* plugin: embedded_data
* data_rows:
* -
* user_id: 1
* name: JohnSmith
* mail: johnsmith@example.com
* hash: '5f4dcc3b5aa765d61d8327deb882cf99'
* ids:
* user_id:
* type: integer
* process:
* name: name
* mail: mail
* pass: hash
* status:
* plugin: default_value
* default_value: 1
* destination:
* plugin: entity:user
* md5_passwords: true
* @endcode
*
* For configuration options inherited from the parent class, refer to
* \Drupal\migrate\Plugin\migrate\destination\EntityContentBase.
*
* The example above is about migrating an MD5 password hash. For more examples
* on different password hash types and a list of other user properties, refer
* to the handbook documentation:
* @see https://www.drupal.org/docs/8/api/migrate-api/migrate-destination-plugins-examples/migrating-users
*/
#[MigrateDestination('entity:user')]
class EntityUser extends EntityContentBase {
/**
* The password service class.
*
* @var \Drupal\Core\Password\PasswordInterface
*/
protected $password;
/**
* Builds a user entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
* @param \Drupal\Core\Password\PasswordInterface $password
* The password service.
* @param \Drupal\Core\Session\AccountSwitcherInterface|null $account_switcher
* The account switcher service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, PasswordInterface $password, ?AccountSwitcherInterface $account_switcher = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_field_manager, $field_type_manager, $account_switcher);
$this->password = $password;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager')->getStorage($entity_type),
array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type)),
$container->get('entity_field.manager'),
$container->get('plugin.manager.field.field_type'),
$container->get('password'),
$container->get('account_switcher')
);
}
/**
* {@inheritdoc}
* @throws \Drupal\migrate\MigrateException
*/
public function import(Row $row, array $old_destination_id_values = []) {
// Do not overwrite the root account password.
if ($row->getDestinationProperty('uid') == 1) {
$row->removeDestinationProperty('pass');
}
return parent::import($row, $old_destination_id_values);
}
/**
* {@inheritdoc}
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = []) {
// Do not overwrite the root account password.
if ($entity->id() != 1) {
// Set the pre_hashed password so that the PasswordItem field does not hash
// already hashed passwords. If the md5_passwords configuration option is
// set we need to rehash the password and prefix with a U.
// @see \Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem::preSave()
$entity->pass->pre_hashed = TRUE;
if (isset($this->configuration['md5_passwords'])) {
$entity->pass->value = 'U' . $this->password->hash($entity->pass->value);
}
}
return parent::save($entity, $old_destination_id_values);
}
/**
* {@inheritdoc}
*/
protected function processStubRow(Row $row) {
parent::processStubRow($row);
$field_definitions = $this->entityFieldManager
->getFieldDefinitions($this->storage->getEntityTypeId(),
$this->getKey('bundle'));
// Name is generated using a dedicated sample value generator to ensure
// uniqueness and a valid length.
// @todo Remove this as part of https://www.drupal.org/node/3352288.
$name = UserNameItem::generateSampleValue($field_definitions['name']);
$row->setDestinationProperty('name', reset($name));
// Email address is not defined as required in the base field definition but
// is effectively required by the UserMailRequired constraint. This means
// that Entity::processStubRow() did not populate it - we do it here.
$mail = EmailItem::generateSampleValue($field_definitions['mail']);
$row->setDestinationProperty('mail', reset($mail));
}
/**
* {@inheritdoc}
*/
public function getHighestId() {
$highest_id = parent::getHighestId();
// Every Drupal site must have a user with UID of 1 and it's normal for
// migrations to overwrite this user.
if ($highest_id === 1) {
return 0;
}
return $highest_id;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Drupal\user\Plugin\migrate\destination;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a destination plugin for migrating user role entities.
*/
#[MigrateDestination('entity:user_role')]
class EntityUserRole extends EntityConfigBase {
/**
* All permissions on the destination site.
*
* @var string[]
*/
protected $destinationPermissions = [];
/**
* Builds a user role entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The storage for this entity type.
* @param array $bundles
* The list of bundles this entity type has.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param array $destination_permissions
* All available permissions.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, array $destination_permissions) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $language_manager, $config_factory);
$this->destinationPermissions = $destination_permissions;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
$entity_type_id = static::getEntityTypeId($plugin_id);
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('entity_type.manager')->getStorage($entity_type_id),
array_keys($container->get('entity_type.bundle.info')->getBundleInfo($entity_type_id)),
$container->get('language_manager'),
$container->get('config.factory'),
array_keys($container->get('user.permissions')->getPermissions()),
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []): array|bool {
$permissions = $row->getDestinationProperty('permissions') ?? [];
// Get permissions that do not exist on the destination.
$invalid_permissions = array_diff($permissions, $this->destinationPermissions);
if ($invalid_permissions) {
sort($invalid_permissions);
// Log the message in the migration message table.
$message = "Permission(s) '" . implode("', '", $invalid_permissions) . "' not found.";
$this->migration->getIdMap()
->saveMessage($row->getSourceIdValues(), $message, MigrationInterface::MESSAGE_WARNING);
}
$valid_permissions = array_intersect($permissions, $this->destinationPermissions);
$row->setDestinationProperty('permissions', $valid_permissions);
return parent::import($row, $old_destination_id_values);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\user\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\user\UserData as UserDataStorage;
use Drupal\migrate\Row;
use Drupal\migrate\Plugin\migrate\destination\DestinationBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* Migration destination for user data.
*/
#[MigrateDestination('user_data')]
class UserData extends DestinationBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\user\UserData
*/
protected $userData;
/**
* Builds a user data entity destination.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\user\UserData $user_data
* The user data service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, UserDataStorage $user_data) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->userData = $user_data;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('user.data')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
$uid = $row->getDestinationProperty('uid');
$module = $row->getDestinationProperty('module');
$key = $row->getDestinationProperty('key');
$this->userData->set($module, $uid, $key, $row->getDestinationProperty('settings'));
return [$uid, $module, $key];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['uid']['type'] = 'integer';
$ids['module']['type'] = 'string';
$ids['key']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'uid' => 'The user id.',
'module' => 'The module name responsible for the settings.',
'key' => 'The setting key to save under.',
'settings' => 'The settings to save.',
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\user\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Plugin to replace !tokens with [tokens].
*/
#[MigrateProcess(
id: "convert_tokens",
handle_multiples: TRUE,
)]
class ConvertTokens extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$tokens = [
'!site' => '[site:name]',
'!username' => '[user:name]',
'!mailto' => '[user:mail]',
'!login_uri' => '[site:login-url]',
'!uri_brief' => '[site:url-brief]',
'!edit_uri' => '[user:edit-url]',
'!login_url' => '[user:one-time-login-url]',
'!uri' => '[site:url]',
'!date' => '[date:medium]',
'!password' => '',
];
// Given that our source is a database column that could hold a NULL
// value, sometimes that filters down to here. str_replace() cannot
// handle NULLs as the subject, so we reset to an empty string.
if (is_null($value)) {
$value = '';
}
if (is_string($value)) {
return str_replace(array_keys($tokens), $tokens, $value);
}
else {
throw new MigrateException('Value must be a string.');
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\user\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
#[MigrateProcess('profile_field_settings')]
class ProfileFieldSettings extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($type, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$settings = [];
switch ($type) {
case 'date':
$settings['datetime_type'] = 'date';
break;
}
return $settings;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\user\Plugin\migrate\process;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a process plugin for the user langcode.
*/
#[MigrateProcess('user_langcode')]
class UserLangcode extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManager
*/
protected $languageManager;
/**
* Constructs a UserLangcode object.
*
* @param array $configuration
* Plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\Language\LanguageManager $language_manager
* The language manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManager $language_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!isset($this->configuration['fallback_to_site_default'])) {
$this->configuration['fallback_to_site_default'] = TRUE;
}
// If the user's language is empty, it means the locale module was not
// installed, so the user's langcode should be English and the user's
// preferred_langcode and preferred_admin_langcode should fallback to the
// default language.
if (empty($value)) {
if ($this->configuration['fallback_to_site_default']) {
return $this->languageManager->getDefaultLanguage()->getId();
}
else {
return 'en';
}
}
// If the user's language does not exist, use the default language.
elseif ($this->languageManager->getLanguage($value) === NULL) {
return $this->languageManager->getDefaultLanguage()->getId();
}
// If the langcode is a valid one, just return it.
return $value;
}
}

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