drupalorg-1.0.x-dev/drupalorg.module
drupalorg.module
<?php
/**
* @file
* Drupal.org Custom Migrations.
*/
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Link;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\drupalorg\Utilities\GitLabClientHelper;
use Drupal\drupalorg\Utilities\GitLabTokenRenew;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\User;
use Drupal\views\ViewExecutable;
/**
* Implements hook_module_implements_alter().
*/
function drupalorg_module_implements_alter(&$implementations, $hook) {
if ($hook === 'filter_secure_image_alter') {
// drupalorg_filter_secure_image_alter() replaces default functionality.
// @todo D11 Use the #[RemoveHook] attribute instead, see
// https://www.drupal.org/node/3496786
unset($implementations['filter']);
}
}
/**
* Implements hook_cron().
*/
function drupalorg_cron() {
// Check for connection to GitLab.
try {
// Renew GitLab token automatically.
(new GitLabTokenRenew())->renewToken();
$days_to_expiry = (new GitLabTokenRenew())->daysToExpiry();
if ($days_to_expiry < 2) {
$message = ($days_to_expiry < 0) ? 'The GitLab token expired @days day(s) ago.' : 'The GitLab token will expire in @days day(s).';
\Drupal::logger('drupalorg')->error($message, [
'@days' => abs($days_to_expiry),
]);
}
}
catch (\Throwable $e) {
\Drupal::logger('drupalorg')->error('Could not connect to GitLab. Code @code. Message: @message', [
'@code' => $e->getCode(),
'@message' => $e->getMessage(),
]);
}
}
/**
* Implements hook_theme().
*/
function drupalorg_theme() {
return [
'drupalorg_issue_forks_management' => [
'variables' => [
'forks' => NULL,
'branches' => NULL,
'issue_specific_branches' => NULL,
'merge_requests' => NULL,
'issue' => NULL,
'source_link' => NULL,
'partial' => NULL,
'project' => NULL,
'logged_in' => NULL,
],
'template' => 'drupalorg-issue-forks-management',
'file' => 'drupalorg.theme.inc',
],
'drupalorg_sponsor_widget' => [
'variables' => [
'organization_name' => NULL,
'organization_url' => NULL,
'organization_logo' => NULL,
],
'template' => 'drupalorg-sponsor-widget',
],
'drupalorg_documentation_issue_submission' => [
'variables' => [
'url' => NULL,
],
'template' => 'drupalorg-documentation-issue-submission',
],
];
}
/**
* Implements hook_page_attachments_alter().
*/
function drupalorg_page_attachments_alter(array &$attachments) {
if (\Drupal::service('router.admin_context')->isAdminRoute()) {
$attachments['#attached']['library'][] = 'drupalorg/icon-field';
}
}
/**
* Implements hook_form_FORM_BASE_alter().
*/
function drupalorg_form_user_form_alter(&$form, &$form_state) {
// @todo Port drupalorg_form_user_profile_form_alter.
// Remove field title and add some text about how the field data will be used.
$form['field_demographics']['widget']['#title'] = t('Please indicate any of the following underrepresented communities that you identify with. We have <a href="/drupalorg/docs/user-accounts/demographic-information" target="_blank">provided some examples</a> of each, but these are not exhaustive—if you feel you identify, please mark the box.');
$form['field_demographics']['widget']['aa']['#prefix'] = t('<br><p>This field will remain private, and will only be used to provide anonymized, aggregate data about the makeup of the Drupal community.</p>');
}
/**
* Implements hook_form_alter().
*/
function drupalorg_form_alter(&$form, &$form_state, $form_id) {
/** @var \Drupal\drupalorg\UserService $user_service */
$user_service = \Drupal::service('drupalorg.user_service');
$user = User::load(\Drupal::currentUser()->id());
$icon_fields = [
'field_footer_cta_icon',
'field_footer_first_link_icon',
'field_footer_second_link_icon',
'field_bordered_link_icon',
];
foreach ($form as $key => &$element) {
if (in_array($key, $icon_fields)) {
$element['#attributes']['class'][] = 'icon-field';
}
}
if ($form_id == 'menu_link_content_main_form') {
unset($form['description']);
unset($form['expanded']);
unset($form['view_mode']);
}
if (isset($form['field_project_machine_name'])) {
// Validate full project short name.
$form['field_project_machine_name']['#element_validate'][] = 'drupalorg_project_machine_name_validate';
/** @var \Drupal\Core\Form\FormState $form_state */
$form_object = $form_state->getFormObject();
if ($form_object instanceof EntityForm) {
$entity = $form_object->getEntity();
if (!$entity->isNew()) {
$form['field_project_machine_name']['#disabled'] = TRUE;
}
}
}
// Limit shared account access.
if (str_starts_with($form_id, 'node_') && str_ends_with($form_id, '_edit_form')) {
$form_object = $form_state->getFormObject();
if ($form_object instanceof EntityForm) {
$entity = $form_object->getEntity();
if ($entity instanceof NodeInterface && !$user_service->isAccountAllowedToEditNode($user, $entity)) {
\Drupal::messenger()->addStatus(t('Your account is a <a href="/terms#accounts">shared account</a>, which can not edit this type of content.'));
}
}
}
// Alter & prefill MailChimp signup forms.
if (str_starts_with($form_id, 'mailchimp_signup_subscribe_')) {
if (!empty($form['mergevars']['COUNTRY'])) {
$country_allowed_values = options_allowed_values($user->get('field_country')->getFieldDefinition()->getFieldStorageDefinition());
$form['mergevars']['COUNTRY']['#type'] = 'select';
$form['mergevars']['COUNTRY']['#options'] = array_combine($country_allowed_values, $country_allowed_values);
$form['mergevars']['COUNTRY']['#size'] = 1;
if (empty($form['mergevars']['COUNTRY']['#required'])) {
$form['mergevars']['COUNTRY']['#empty_option'] = t('- Select -');
}
}
// Add empty options.
foreach (['WHAT_BRING', 'VERTICAL'] as $var) {
if (!empty($form['mergevars'][$var])) {
$form['mergevars'][$var]['#empty_option'] = t('- Select -');
}
}
// Conditional fields.
if (!empty($form['mergevars']['WHAT_BRING']) && !empty($form['mergevars']['VERTICAL'])) {
$form['mergevars']['VERTICAL']['#states'] = [
'visible' => [
':input[name="mergevars[WHAT_BRING]"]' => [
['value' => 'I’m testing Drupal for my company or organization'],
['value' => 'I’m evaluating Drupal for a client or external project'],
['value' => 'I’m comparing Drupal with other CMS platforms'],
],
],
];
}
// Add attribution.
$lead_form_id = preg_replace('/^mailchimp_signup_subscribe_(block_)?/', '', $form_id);
$form['#lead_form_id'] = $lead_form_id;
if (!empty($form['mergevars']['SOURCE'])) {
// Hide field which records the source of the signup.
$form['mergevars']['SOURCE']['#type'] = 'value';
$form['mergevars']['SOURCE']['#value'] = $lead_form_id . ' form on Drupal.org';
}
// Add extra form API definition to interest groups. Can be moved to
// settings.php or elsewhere if many are added.
$interest_group_descriptions = [
'657567a77b' => [
'info' => [
'#markup' => t('By participating in this trial, you acknowledge that you have read, understood, and agree to the <a href="/terms#trial-consent">terms of this consent</a> and you grant permission to the Drupal Association to send you communications as well as share your personal information for the purposes of facilitating your participation in the trial and the Purpose set forth therein.'),
],
'#required' => TRUE,
],
'18c2ff7dc8' => [
'info' => [
'#markup' => t('By participating in this trial, you acknowledge that you have read, understood, and agree to the Drupal Association <a href="/privacy">privacy policy</a>.'),
],
'#required' => TRUE,
],
];
foreach ($interest_group_descriptions as $id => $extra) {
if (!empty($form['mailchimp_lists']['interest_groups'][$id])) {
$form['mailchimp_lists']['interest_groups'][$id] = array_merge($form['mailchimp_lists']['interest_groups'][$id], $extra);
}
}
// Prefill if logged in.
if ($user->isAuthenticated()) {
if (!empty($form['mergevars']['EMAIL'])) {
$form['mergevars']['EMAIL']['#default_value'] = $user->getEmail();
}
if (!empty($form['mergevars']['FNAME'])) {
$form['mergevars']['FNAME']['#default_value'] = $user->get('field_first_name')->value;
}
if (!empty($form['mergevars']['LNAME'])) {
$form['mergevars']['LNAME']['#default_value'] = $user->get('field_last_name')->value;
}
if (!empty($form['mergevars']['COUNTRY'])) {
if (($value = $user->get('field_country')->value) && isset($country_allowed_values[$value])) {
$form['mergevars']['COUNTRY']['#default_value'] = $country_allowed_values[$value];
}
}
/** @var \Drupal\user\UserDataInterface $user_data */
$user_data_service = \Drupal::service('user.data');
$data = $user_data_service->get('drupalorg', $user->id(), 'lead_forms');
}
// Try setting country based on GeoIP, if it is not already set.
if (!empty($form['mergevars']['COUNTRY']) && empty($form['mergevars']['COUNTRY']['#default_value'])) {
if (($geoip_country = \Drupal::request()->headers->get('GeoIP-country')) && isset($country_allowed_values[$geoip_country])) {
$form['mergevars']['COUNTRY']['#default_value'] = $country_allowed_values[$geoip_country];
}
}
$form['#submit'][] = '_drupalorg_mailchimp_form_submit';
}
}
/**
* Note that someone has completed a lead form.
*
* @param array $form
* The form array.
*/
function _drupalorg_mailchimp_form_submit(array $form): void {
$user = User::load(\Drupal::currentUser()->id());
if ($user->isAuthenticated()) {
/** @var \Drupal\user\UserDataInterface $user_data */
$user_data_service = \Drupal::service('user.data');
$data = $user_data_service->get('drupalorg', $user->id(), 'lead_forms');
$data[$form['#lead_form_id']] = TRUE;
$user_data_service->set('drupalorg', $user->id(), 'lead_forms', $data);
}
else {
$session = \Drupal::request()->getSession();
$data = $session->get('drupalorg_lead_forms', []);
$data[$form['#lead_form_id']] = TRUE;
$session->set('drupalorg_lead_forms', $data);
}
}
/**
* Validate a project short name.
*
* @see http://www.php.net/manual/en/functions.user-defined.php
* @see https://drupal.org/node/2172891
*/
function drupalorg_project_machine_name_validate($element, FormState &$form_state) {
$value = $form_state->getValue('field_project_machine_name')[0]['value'] ?? NULL;
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $value)) {
$form_state->setErrorByName('field_project_machine_name', t('@title must follow <a href="@url">PHP naming convention</a>.', [
'@title' => $element['widget'][0]['#title'],
'@url' => 'http://www.php.net/manual/en/functions.user-defined.php',
]));
}
}
/**
* Implements hook_options_list_alter().
*/
function drupalorg_options_list_alter(array &$options, array $context) {
$icon_fields = [
'field_footer_cta_icon',
'field_footer_first_link_icon',
'field_footer_second_link_icon',
'field_bordered_link_icon',
];
$field_name = $context['fieldDefinition']->getName();
if (!in_array($field_name, $icon_fields)) {
return;
}
// Loop through the icons and add an icon image next to the item.
foreach ($options as $idx => $option) {
global $base_url;
$default_theme_name = \Drupal::config('system.theme')->get('default');
$theme_path = \Drupal::service('extension.list.theme')->getPath($default_theme_name);
$icon = str_replace(' ', '-', strtolower($option));
$icon_path = $base_url . '/' . $theme_path . '/src/assets/svgs/icon-' . $icon . '.svg';
$options[$idx] = $option . '<img src="' . $icon_path . '">';
}
}
/**
* Implements hook_entity_create_access().
*/
function drupalorg_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
if ($context['entity_type_id'] !== 'node') {
return AccessResult::neutral();
}
$allowed_types = Settings::get('drupalorg_allowed_content_types');
if (is_array($allowed_types) && array_search('*', $allowed_types) === FALSE) {
if (!in_array($entity_bundle, $allowed_types)) {
return AccessResult::forbidden();
}
}
/** @var \Drupal\drupalorg\UserService $user_service */
$user_service = \Drupal::service('drupalorg.user_service');
$user = User::load($account->id());
if (!$user_service->isAccountAllowedToCreateNode($user, $entity_bundle)) {
return AccessResult::forbidden();
}
return AccessResult::allowedIfHasPermission($account, 'create ' . $entity_bundle . ' content');
}
/**
* Get the project identifier & IID from a GitLab security issue URL.
*
* @param string $url
* GitLab security issue URL.
*
* @return array
* With keys 'project' & 'iid'.
*/
function _drupalorg_parse_security_issue_url($url) {
preg_match('#^/(?P<project>security/.*-security)/-/issues/(?P<iid>[0-9]+)$#', parse_url($url, PHP_URL_PATH), $match);
return $match;
}
/**
* Implements hook_ENTITY_TYPE_access().
*/
function drupalorg_node_access(NodeInterface $node, $operation, AccountInterface $account) {
/** @var \Drupal\drupalorg\ProjectService $project_service */
$project_service = \Drupal::service('drupalorg.project_service');
/** @var \Drupal\drupalorg\UserService $user_service */
$user_service = \Drupal::service('drupalorg.user_service');
$entity_type = $node->bundle();
$request = \Drupal::request();
$user = User::load($account->id());
$shared_account = $user_service->isSharedAccount($user);
$allowed_types = Settings::get('drupalorg_allowed_content_types');
$gitlab_client_helper = new GitLabClientHelper();
if ($operation === 'create') {
if (is_array($allowed_types) && array_search('*', $allowed_types) === FALSE) {
if (!in_array($entity_type, $allowed_types)) {
return AccessResult::forbidden();
}
}
if (!$user_service->isAccountAllowedToEditNode($user, $node)) {
\Drupal::messenger()->addStatus(t('Your account is a <a href="/terms#accounts">shared account</a>, which can not create this type of content.'));
return AccessResult::forbidden();
}
if (
in_array($entity_type, $project_service->projectNodeTypes()) &&
!$shared_account &&
!$user->hasRole('git_user')
) {
\Drupal::messenger()->addStatus(t('Agree to Drupal.org’s <a href="!git">Git access agreement</a> to create projects.', [
'!git' => Url::fromUserInput('/user/' . $user->id() . '/git')->toString(),
]));
return AccessResult::forbidden();
}
if ($entity_type === 'sa') {
// Security team is always allowed.
if ($account->hasPermission('manage security releases')) {
return AccessResult::allowed();
}
// Otherwise, advisories must be associated with an issue in GitLab.
if (empty($request->query->get('issue'))) {
return AccessResult::forbidden();
}
$gitlab_url = $gitlab_client_helper->getGitLabUrl();
if (!preg_match('#^' . preg_quote($gitlab_url, '#') . '#', $request->query->get('issue'))) {
return AccessResult::forbidden();
}
// They must also have a project.
if (empty($request->query->get('field_project')) || !($project_node = Node::load($request->query->get('field_project')))) {
return AccessResult::forbidden();
}
// With maintainer access.
if ($project_service->isProjectMaintainer($user, $project_node)) {
return AccessResult::forbidden();
}
// The issue URL must look valid.
if (!($parsed_url = _drupalorg_parse_security_issue_url($request->query->get('issue')))) {
return AccessResult::forbidden();
}
$machine_name = $project_node->get('field_project_machine_name')->value;
if (!preg_match('#^security/.*-' . preg_quote($machine_name, '#') . '-security$#', $parsed_url['project'])) {
return AccessResult::forbidden();
}
// The issue must exist. Since we have already verified that the author
// is a maintainer, we do not have to worry about timing attacks to
// enumerate issue existence.
try {
$gitlab_client = $gitlab_client_helper->client();
$gitlab_issue = $gitlab_client->issues()->show($parsed_url['project'], $parsed_url['iid']);
}
catch (Exception $e) {
if ($e->getCode() !== 404) {
\Drupal::logger('drupalorg')->error(t('Exception in checking sa create access for @project @iid: %msg', [
'@project' => $parsed_url['project'],
'@iid' => $parsed_url['iid'],
'%msg' => $e->getMessage(),
]));
}
return AccessResult::forbidden();
}
}
}
elseif ($operation === 'update') {
if (is_array($allowed_types) && array_search('*', $allowed_types) === FALSE) {
if (!in_array($entity_type, $allowed_types)) {
return AccessResult::forbidden();
}
}
if (!$user_service->isAccountAllowedToEditNode($user, $node)) {
return AccessResult::forbidden();
}
// phpcs:disable Drupal.Commenting.InlineComment.SpacingBefore
// D7 historic hack:
// We had this in the D7 site as a way of limiting access to certain pages,
// but we are dropping this in favor of roles and permissions and
// Workbench Access rules. Leave the code below in case we still find a
// case where we might want to recover this logic.
// ---------------------------------------------------------------------
// The documentation team lock pages by setting the body filter format to
// a format which only privileged accounts have access to. This behavior
// worked in D6 but not D7, so we need to workaround this by restricting
// node editing access based on the body filter format.
// @see https://www.drupal.org/node/1824490
// if (isset($node->body)) {
// foreach ($node->body as $langcode => $items) {
// foreach ($items as $delta => $item) {
// // filter_access() only checks the ->format member, which $item
// // provides, using it directly saves calling filter_format_load().
// if (!filter_access((object)$item, $account)) {
// // The user doesn't have permissions to use the filter format,
// // so deny access to edit the page.
// return AccessResult::forbidden();
// }
// }
// }
// }
// phpcs:enable Drupal.Commenting.InlineComment.SpacingBefore
if ($entity_type === 'sa') {
if (!$node->isPublished()) {
// Draft security advisories can be edited by their project maintainer.
$project_node = Node::load($node->get('field_project')->target_id);
if ($project_node && $project_service->isProjectMaintainer($user, $project_node)) {
return AccessResult::allowed();
}
}
}
}
elseif ($operation === 'delete') {
if (is_array($allowed_types) && array_search('*', $allowed_types) === FALSE) {
if (!in_array($entity_type, $allowed_types)) {
return AccessResult::forbidden();
}
}
// Do not allow deletion of full projects with code. Since this is an API
// call to GitLab, only check if the user has access.
if (
$project_service->isProject($node) &&
!$project_service->isSandbox($node) &&
$user->hasPermission('delete any ' . $node->bundle() . ' content') &&
($repository = $project_service->getProjectRepositoryInformation($node))
) {
try {
$gitlab_client = $gitlab_client_helper->client();
if (!empty($gitlab_client->repositories()->branches($repository['gitlab_project_id'], ['per_page' => 1]))) {
// The project contains code; don’t allow users to delete it.
return AccessResult::forbidden();
}
}
catch (Exception $e) {
\Drupal::logger('drupalorg')->error(t('Exception in checking if a project has commits: @error', [
'@error' => $e->getMessage(),
]));
return AccessResult::forbidden();
}
}
}
if ($operation !== 'view') {
// phpcs:disable Drupal.Commenting.InlineComment.SpacingBefore
// D7 Organic Groups hack:
// Documentation pages and guides will no longer have something like OG
// controlling access.
// ---------------------------------------------------------------------
// For documentation pages in guides, strengthen core node access to deny
// instead of ignore for lack of permission. Users must have both the
// required roles and group membership, if any. For example,
// documentation pages have restrictions from node_node_access(), but do
// not require documentation guide membership to edit.
// if (
// in_array($entity_type, ['documentation', 'guide', 'event']) &&
// node_node_access($node, $operation, $account) === NODE_ACCESS_IGNORE
// ) {
// return AccessResult::forbidden();
// }
// phpcs:enable Drupal.Commenting.InlineComment.SpacingBefore
}
if ($operation === 'view' && !$node->isPublished()) {
if ($user->hasPermission('view any unpublished ' . $entity_type . ' content')) {
return AccessResult::allowed();
}
elseif ($entity_type === 'sa') {
// Project maintainers can view their draft advisories.
$project_node = Node::load($node->get('field_project')->target_id);
if ($project_node && $project_service->isProjectMaintainer($user, $project_node)) {
return AccessResult::allowed();
}
}
}
return AccessResult::neutral();
}
/**
* Implements hook_entity_field_access().
*/
function drupalorg_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
if (empty($items)) {
return AccessResult::neutral();
}
$entity = $items->getEntity();
$field_name = $field_definition->getName();
if ($field_definition->getTargetEntityTypeId() == 'user') {
if ($operation === 'view') {
switch ($field_name) {
case 'field_notes':
case 'field_demographics':
case 'field_da_listing_opt_out':
return AccessResult::forbidden();
case 'field_fingerprint':
case 'field_reported_registration_ip':
return $account->hasPermission('administer users') ? AccessResult::allowed() : AccessResult::forbidden();
}
}
elseif ($operation === 'edit') {
switch ($field_name) {
case 'field_terms_of_service':
// You can only agree for yourself.
if ($account->id() !== $entity->id()) {
return AccessResult::forbidden();
}
// Once agreed to, hide.
if (!$entity->get($field_name)->isEmpty() && $entity->get($field_name)->getValue()[0]['value']) {
return AccessResult::forbidden();
}
break;
case 'field_demographics':
case 'field_da_listing_opt_out':
// Only you can see your selections.
if ($entity->id() === $account->id()) {
return AccessResult::neutral();
}
return AccessResult::forbidden();
case 'field_notes':
case 'field_fingerprint':
case 'field_reported_registration_ip':
return $account->hasPermission('administer users') ? AccessResult::allowed() : AccessResult::forbidden();
case 'field_subscribe_membership':
// Has individual membership.
if (!$entity->get($field_name)->isEmpty() && $entity->get($field_name)->getValue()[0]['value']) {
return AccessResult::neutral();
}
// Hide otherwise.
return AccessResult::forbidden();
}
}
}
elseif ($field_definition->getTargetEntityTypeId() == 'node') {
if ($operation === 'view') {
switch ($field_name) {
case 'field_drupalorg_rank_components':
case 'field_weighted_org_issue_cred_yr':
case 'field_org_special_program':
case 'field_org_special_program_desc':
return $account->hasPermission('administer nodes') ? AccessResult::allowed() : AccessResult::forbidden();
}
}
elseif ($operation === 'edit') {
// Handle the case of file uploads on certain node types.
if ($field_definition->getType() === 'file' && in_array($entity->getEntityTypeId(), ['forum', 'page'])) {
return $account->hasPermission('administer nodes') ? AccessResult::allowed() : AccessResult::forbidden();
}
switch ($field_name) {
case 'field_org_membership_status':
case 'field_status':
case 'field_organization_technologies':
case 'field_organization_support':
case 'field_short_description':
if (!empty($entity->get('uid')->getValue()) && $entity->get('uid')->getValue()[0]['target_id'] === $account->id()) {
// People cannot edit their own nodes for these fields.
return AccessResult::forbidden();
}
// For other users' nodes, allow administrators to edit.
return $account->hasPermission('administer nodes') ? AccessResult::allowed() : AccessResult::forbidden();
case 'field_org_special_program':
case 'field_org_special_program_desc':
case 'field_migration_budget':
// Only association staff can edit these fields.
return $account->hasPermission('administer association sponsorships') ? AccessResult::allowed() : AccessResult::forbidden();
case 'field_org_rank_components':
// Only populated by drush command.
return AccessResult::forbidden();
case 'field_project_has_releases':
// Always allow admins to configure releases. Otherwise restrict
// access to maintainers with 'administer releases' permission on
// the project.
return $account->hasPermission('administer projects') ? AccessResult::allowed() : AccessResult::forbidden();
case 'field_logo_url':
// Only set by automation.
return AccessResult::forbidden();
case 'field_project_has_issue_queue':
// Always allow admins to configure issue tracking. Otherwise
// restrict access to maintainers with 'maintain issues'
// permission on the project.
/** @var \Drupal\drupalorg\ProjectService $project_service */
$project_service = \Drupal::service('drupalorg.project_service');
$maintainer = $project_service->isProjectMaintainer($account, $entity);
if (
$entity->get('uid')->target_id === $account->id() ||
$account->hasPermission('administer nodes') ||
$maintainer
) {
return AccessResult::neutral();
}
else {
return AccessResult::forbidden();
}
case 'field_new_page_and_guide_review':
if (!$entity->isNew()) {
// Only allow access if a guide maintainer is editing. So
// someone adding a new page can’t grant this for themselves.
return $account->hasPermission('administer nodes') ? AccessResult::allowed() : AccessResult::forbidden();
// @todo we don't have anything matching this yet.
// return og_user_access_entity('administer og menu',
// $entity_type, $entity, $account);
}
case 'field_parent_section':
if ($entity->getEntityTypeId() === 'guide') {
// Parent section allows top-level documentation guides to be
// placed in sections, for setting breadcrumbs. Only allow for
// people who can create top-level guides. See also
// drupalorg_form_guide_node_form_alter().
return $account->hasPermission('administer group') ? AccessResult::allowed() : AccessResult::forbidden();
}
break;
// @todo We don't have anything OG yet.
// case 'field_announcement_version':
// // Only show in the /about/announcements section.
// return ($context = og_context()) && $context['gid'] == 3223030;
case 'field_role_approval_required':
return $account->hasPermission('administer users') ? AccessResult::allowed() : AccessResult::forbidden();
}
}
}
return AccessResult::neutral();
}
/**
* Implements hook_views_data_alter().
*/
function drupalorg_views_data_alter(array &$data) {
$data['node_field_data']['drupalorg_node_status'] = [
'title' => t('Published status or admin user (Drupal.org)'),
'help' => t('Filters out unpublished content if the current user cannot view it, including “view unpublished … content” access.'),
'filter' => [
'field' => 'status',
'id' => 'drupalorg_node_status',
'label' => t('Published status, admin user, or allowed unpublished content.'),
],
];
}
/**
* Implements hook_views_pre_render().
*/
function drupalorg_views_pre_render(ViewExecutable $view) {
if ($view->id() === 'case_studies' && $view->current_display == 'embed_services') {
// Count case studies for the organization and replace in the header.
$organization_nid = $view->argument['field_case_organizations_target_id']->getValue();
$count = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
->accessCheck(TRUE)
->condition('type', 'casestudy')
->condition('status', NodeInterface::PUBLISHED)
->condition('field_case_organizations.target_id', $organization_nid)
->count()->execute();
$view->header['area']->options['content']['value'] = Link::fromTextAndUrl(\Drupal::translation()->formatPlural($count, '1 case study', '@count case studies'), Url::fromUserInput('/node/' . $organization_nid . '/case-studies'))->toString();
}
}
/**
* Implements hook_mail().
*/
function drupalorg_mail($key, &$message, $params) {
$language = \Drupal::languageManager()->getLanguage($message['langcode']);
$options = ['langcode' => $language->getId()];
switch ($key) {
case 'security_issue_opened':
$message['subject'] = t('[Drupal Security] Potential security vulnerability in @project_title', [
'@project_title' => $params['project_node']->getTitle(),
], $options);
$message['body'][] = t('Hello @maintainer_name,', [
'@maintainer_name' => $params['maintainer_name'],
], $options);
$message['body'][] = t('The Drupal security team received report of possible vulnerabilities in the “@project_machine_name” project on Drupal.org.', [
'@project_machine_name' => $params['project_node']->get('field_project_machine_name')->value,
], $options);
$message['body'][] = t('Along with other contributors/reporters, you have been granted access to the private issue at @issue_url where you will find more details about the vulnerability. Maintainers, please do NOT commit any code right away!', [
'@issue_url' => $params['issue_url'],
], $options);
$message['body'][] = t('1. If you are the maintainer, please comment on the issue acknowledging that you are aware of the vulnerability and working on a fix. However, anyone given access to this issue can contribute. So, whether you’re the maintainer or not, please feel free to either work on a fix, or review any existing fix in the issue history.
2. You should take a moment to familiarize yourself with our process for fixing issues like these. We’ve described the process at https://www.drupal.org/i/101497.
3. The security team can help out if you have questions, and we will work to ensure that any fix addresses the security issue in question.
4. Finally, until publicly disclosed, please remember to keep this vulnerability a secret, so as not to jeopardize the sites using this module.', [], $options);
$message['body'][] = t('We look forward to resolving this issue promptly, and working together with you to protect users. If you have any questions, please add them to the issue at the URL given above, or write to us at security@drupal.org.', [], $options);
$message['body'][] = t('Sincerely,
The Drupal Security Team', [], $options);
break;
}
}
// Workaround for missing dependency error caused from the project module not
// being ported.
// Impacts node.field_issue_sa_version and node.field_issue_version.
// phpcs:disable Drupal.NamingConventions.ValidFunctionName.InvalidPrefix
if (!function_exists('project_release_version_allowed_values')) {
/**
* Replacement if the function is not present.
*/
function project_release_version_allowed_values() {
return [
// Check return syntax, which might be 'name => label'.
'10.x',
'11.x',
];
}
}
/**
* Implements hook_entity_insert().
*/
function drupalorg_entity_insert(EntityInterface $entity) {
// Only act on taxonomy terms in the 'section' vocabulary.
if (
$entity->getEntityTypeId() === 'taxonomy_term' &&
$entity->bundle() === 'section'
) {
// Ensure all section terms have menus.
drupalorg_ensure_section_menus();
}
}
/**
* Ensure all section terms have menus.
*/
function drupalorg_ensure_section_menus() {
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$menu_storage = \Drupal::entityTypeManager()->getStorage('menu');
$terms = $term_storage->loadByProperties(['vid' => 'section']);
foreach ($terms as $term) {
$term_name = $term->label();
$term_id = $term->id();
$menu_name = drupalorg_generate_menu_machine_name($term_id);
// Create the menu if it does not already exist.
if (!$menu_storage->load($menu_name)) {
$menu = $menu_storage->create([
'id' => $menu_name,
'label' => 'Section ' . $term_name . ' menu',
'description' => 'Menu for the ' . $term_name . ' section.',
]);
$menu->save();
}
}
}
/**
* Generate a menu machine name based on the term ID.
*/
function drupalorg_generate_menu_machine_name($term_id) {
// Always use the TID for the menu machine name to ensure uniqueness and
// avoid length issues.
return 'section_' . $term_id . '_menu';
}
/**
* Implements hook_entity_delete().
*/
function drupalorg_entity_delete(EntityInterface $entity) {
if ($entity->getEntityTypeId() === 'node' && $entity instanceof NodeInterface) {
// Ensure all nodes that are deleted get captured in drupalorg_node_deleted
// table. If a comment had spam flags, tell us how many.
// D7: "flag_get_counts('node', $node->nid);".
$flag_counts = 0;
// Insert node into drupalorg_node_deleted.
\Drupal::database()->insert('drupalorg_node_deleted')->fields([
'nid' => $entity->id(),
'node_type' => $entity->bundle(),
'uid' => $entity->getOwnerId(),
'title' => $entity->label(),
'created' => $entity->getCreatedTime(),
'changed' => $entity->getChangedTime(),
'status' => (int) $entity->isPublished(),
'name' => \Drupal::currentUser()->getAccountName(),
'language' => $entity->language()?->getId() ?? '',
'node_body' => $entity->hasField('body') ? $entity->get('body')->getString() : '',
// 'flag_count' => $flag_counts['drupalorg_node_spam'] ?? 0,
'flag_count' => $flag_counts,
'deleted_by' => \Drupal::currentUser()->id(),
'deleted_on' => \Drupal::time()->getRequestTime(),
])->execute();
}
}
/**
* Implements hook_filter_secure_image_alter().
*/
function drupalorg_filter_secure_image_alter(DOMElement &$image): void {
$src = $image->getAttribute('src');
// Do a more-lenient replacement than FileUrlGenerator::transformRelative(),
// that allows both www and new domains in absolute URLs.
$src = preg_replace("@^https?://(www|new)\.drupal\.org($|/)@", '/', $src, 1, $count);
if ($count > 0) {
$image->setAttribute('src', $src);
// Reimplement parts of _filter_html_image_secure_process().
$base_path = base_path();
$base_path_length = mb_strlen($base_path);
$local_dir = \Drupal::root() . '/';
// Remove the $base_path to get the path relative to the Drupal root.
// Ensure the path refers to an actual image by prefixing the image source
// with the Drupal root and running getimagesize() on it.
$local_image_path = $local_dir . mb_substr($src, $base_path_length);
$local_image_path = rawurldecode($local_image_path);
if (@getimagesize($local_image_path)) {
// The image has the right path. Erroneous images are dealt with below.
return;
}
}
// @todo D11 This function has been removed from Drupal 11. Call the hook
// attribute it moved to instead.
// @phpstan-ignore function.notFound
filter_filter_secure_image_alter($image);
}
