safedelete-1.0.0/safedelete.module
safedelete.module
<?php
/**
* @file
* Module safedelete.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Url;
use Drupal\safedelete\AdminHelper;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\Entity\File;
use Drupal\Core\Render\Markup;
/**
* Implements hook_ENTITY_TYPE_insert().
*/
function safedelete_node_insert(EntityInterface $entity) {
$entity_id = $entity->id();
drupal_register_shutdown_function('_handleInsertFileUsage_PageCreation', $entity, $entity_id);
}
/**
* Implements hook_ENTITY_delete().
* Cleanup file_usage when we delete this node.
*/
function safedelete_entity_delete(EntityInterface $entity) {
// Exit early if possible.
if ($entity->getEntityTypeId() != 'node') {
return;
}
$entityid = $entity->id();
AdminHelper::deleteRecordsInFileUsage($entityid);
}
/**
* Implements hook_ENTITY_TYPE_presave().
*/
function safedelete_node_presave(EntityInterface $entity) {
// Exit early if possible.
if ($entity->getEntityTypeId() != 'node') {
return;
}
$bundles = \Drupal::config('safedelete.settings')->get('bundle');
if (!in_array($entity->bundle(), $bundles)) {
// This bundle is not enabled, do not perform validation checks.
return;
}
if ((!($entity->isNew())) && $entity->hasField('body')) {
// Handle existing node (page).
$entity_id = $entity->id();
$qeryarray = NULL;
$qeryarray = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
->accessCheck(FALSE)
->latestRevision()
->condition('nid' , $entity_id, '=')
->execute();
$latestver = key($qeryarray);
$db_nodeenity = NULL;
if (isset($latestver)) {
$db_nodeenity = \Drupal::entityTypeManager()
->getStorage('node')
->loadRevision($latestver);
}
$all_languages = AdminHelper::getAllEnabledLanguages();
foreach ($all_languages as $other_langcode => $other_language) {
if ($db_nodeenity->hasTranslation($other_langcode) && $entity->hasTranslation($other_langcode)) {
$filearray = []; // stores fid and number of the same file count for the usage definition change
$dbfilearray = [];
$dbbody_field = NULL;
$body_field = NULL;
$regex_mediaobj = "/data-entity-type=\"media\" data-entity-uuid=\"([a-z]|[0-9]){8}-(([0-9]|[a-z]){4}-){3}([0-9]|[a-z]){12}\"/";
$regex_uuid = "/([a-z]|[0-9]){8}-(([0-9]|[a-z]){4}-){3}([0-9]|[a-z]){12}/";
$body_field = $entity->getTranslation($other_langcode)->get('body')->value;
$dbbody_field = $db_nodeenity->getTranslation($other_langcode)->get('body')->value;
if (!is_null($body_field) || empty(!$body_field)) {
// Using PHP 8.1 or higher, do not do a preg_match_all on null.
preg_match_all($regex_mediaobj, $body_field, $matches, PREG_SET_ORDER);
}
else {
$matches = [];
}
foreach ($matches as $match) {
$file = reset($match);
preg_match_all($regex_uuid, $file, $id_match, PREG_SET_ORDER);
$muuid = $id_match[0][0];
$mentity = \Drupal::service('entity.repository')->loadEntityByUuid('media', $muuid);
if (is_null($mentity)) {
continue;
}
$fid = $mentity->getSource()->getSourceFieldValue($mentity);
if ((array_key_exists($fid, $filearray))) {
$filearray[$fid] = $filearray[$fid] + 1;
}
else {
$filearray[$fid] = 1;
}
}
if ($dbbody_field != $body_field) {
$matches = [];
if (!is_null($dbbody_field) && !empty($dbbody_field)) {
// Using PHP 8.1 or higher, do not do a preg_match_all on null.
preg_match_all($regex_mediaobj, $dbbody_field, $matches, PREG_SET_ORDER);
}
foreach ($matches as $match) {
$file = reset($match);
preg_match_all($regex_uuid, $file, $id_match, PREG_SET_ORDER);
$muuid = $id_match[0][0];
$mentity = \Drupal::service('entity.repository')->loadEntityByUuid('media', $muuid);
if (is_null($mentity)) {
continue;
}
$fid = $mentity->getSource()->getSourceFieldValue($mentity);
if ((array_key_exists($fid, $dbfilearray))) {
$dbfilearray[$fid] = $dbfilearray[$fid] + 1;
}
else {
$dbfilearray[$fid] = 1;
}
}
// Check if the media object(s) are removed or added.
if (count($filearray) > 0) {
$file_usage = \Drupal::service('file.usage');
// Current editing content has embedded file(s)
foreach ($filearray as $fileid => $refcount) {
if (count($dbfilearray) > 0 && (array_key_exists($fileid, $dbfilearray))) {
// Do nothing due to the file usage simple definition - only count 1 for the same file inserted into the editor multi-times.
$dbrefcount = $dbfilearray[$fileid];
$difcount = $refcount - $dbrefcount;
// @todo if we change the file usage defintion, we count m file usage for the same file is embedded into the same content
}
else {
// New media reference
// Assumption: only add the file once.
$fentity = File::load($fileid);
if ($fentity) {
$file_usage->add($fentity, 'editor', 'node', $entity_id);
}
}
}//end for loop -$filearray - default
if (count($dbfilearray) > 0) {
foreach ($dbfilearray as $fileid => $refcount) {
if (!(array_key_exists($fileid, $filearray))) {
// Assumption: only remove the file once.
// @todo Change the logic as a loop with count- $refcount.
$fentity = File::load($fileid);
if ($fentity) {
$file_usage->delete($fentity, 'editor', 'node', $entity_id);
}
}
}
}
}
// Current editing content has no any embedded files.
else {
if (count($dbfilearray) > 0) {
// Check if the media object(s) are total removed from the content.
foreach ($dbfilearray as $fileid => $refcount) {
$fentity = File::load($fileid);
$file_usage = \Drupal::service('file.usage');
$file_usage->delete($fentity, 'editor', 'node', $entity_id);
}
}
} //End Processing the default body
}
}
} // end of the loop - lang
} // end of the custom file handler for embedding the files
}
/**
* Implements hook_entity_type_predelete().
*/
function safedelete_file_predelete(EntityInterface $entity) {
$has_show_delete_permission = \Drupal::currentUser()->hasPermission('safedelete show delete button');
if (!$has_show_delete_permission) {
$has_show_delete_permission = \Drupal::config('safedelete.settings')->get('delete_button');
}
if ($has_show_delete_permission) {
return;
}
$file_usage = \Drupal::service('file.usage');
$usage = $file_usage->listUsage($entity);
if (!empty($usage)) {
// Set error message
// display error.
// Prevent delete.
$entity_type = $entity->getEntityTypeId();
$id = $entity->id();
$response = new RedirectResponse(Url::fromUri('internal:/' . $entity_type . '/' . $id . '/delete')->toString());
$response->send();
\Drupal::messenger()->addMessage(t('Safe delete prevented you from deleting "%title" entity_id %id due to file usage.', [
'%id' => $entity->id(),
'%title' => $entity->getFilename(),
]), 'warning', TRUE);
exit;
}
}
/**
* Implements hook_entity_type_predelete().
*/
function safedelete_media_predelete(EntityInterface $entity) {
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$query = \Drupal::entityQuery('node');
$and1 = $query->accessCheck(FALSE)
->orConditionGroup()
->condition('body', $entity->uuid(), 'CONTAINS');
$query->accessCheck(FALSE)
->condition($and1);
$nids = $query->execute();
if (!empty($nids)) {
// Set error message
// display error.
// Prevent delete.
$entity_type = $entity->getEntityTypeId();
$id = $entity->id();
$response = new RedirectResponse(Url::fromUri('internal:/' . $entity_type . '/' . $id . '/delete')->toString());
$response->send();
\Drupal::messenger()->addMessage(t('Safe delete prevented you from deleting "%title" entity_id %id due to existing uuid references in other entities body content.', [
'%id' => $entity->id(),
'%title' => $entity->getName(),
]), 'warning', TRUE);
exit;
}
}
/**
* Implements hook_entity_type_predelete().
*/
function safedelete_node_predelete(EntityInterface $entity) {
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$query = \Drupal::entityQuery('node');
$and1 = $query->accessCheck(FALSE)
->orConditionGroup()
->condition('body', $entity->uuid(), 'CONTAINS');
$query->accessCheck(FALSE)
->condition($and1);
$nids = $query->execute();
if (!empty($nids)) {
// Set error message
// display error.
// Prevent delete.
$entity_type = $entity->getEntityTypeId();
$id = $entity->id();
$response = new RedirectResponse(Url::fromUri('internal:/' . $entity_type . '/' . $id . '/delete')->toString());
$response->send();
\Drupal::messenger()->addMessage(t('Safe delete prevented you from deleting "%title" entity_id %id due to existing linkit references in other entities body content.', [
'%id' => $entity->id(),
'%title' => $entity->getTitle(),
]), 'warning', TRUE);
exit;
}
}
/**
* Implements safedelete_form_validate_HOOK().
*/
function safedelete_form_validate_enable_bulkpublish(array $form, FormStateInterface $form_state) {
$username = \Drupal::currentUser()->getAccountName();
if ($form_state->hasValue('enable_bulk_publish_validate')) {
$tmpvar = $form_state->getValue('enable_bulk_publish_validate');
$tmpconfig = \Drupal::config('safedelete.settings')->get('enforce_bulk_publish_verify_checkpagepublished');
if ($tmpvar != $tmpconfig) {
// Update SafeDelete settings enforce_bulk_publish_verify_checkpagepublished.
$config = \Drupal::service('config.factory')->getEditable('safedelete.settings');
if ($tmpvar == 0) {
\Drupal::logger('safedelete')->notice($username . " disable the checkbox (Enable bulk publish validation to check if the linked pages are unpublished.)");
$config->set('enforce_bulk_publish_verify_checkpagepublished', $tmpvar)->save();
}
}
}
}
/**
* Implements safedelete_form_submit_HOOK().
*/
function safedelete_form_submit_enable_bulkpublish(array $form, FormStateInterface $form_state) {
$tmpconfig = \Drupal::config('safedelete.settings')->get('enforce_bulk_publish_verify_checkpagepublished');
if (!$tmpconfig) {
// Update Settings.
$config = \Drupal::service('config.factory')->getEditable('safedelete.settings');
\Drupal::logger('safedelete')->notice("Enable the checkbox (Enable bulk publish validation to check if the linked pages are unpublished.)");
$config->set('enforce_bulk_publish_verify_checkpagepublished', TRUE)->save();
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function safedelete_form_views_form_moderated_content_moderated_content_alter(&$form, FormStateInterface $form_state, $form_id) {
$route_name = \Drupal::routeMatch()->getRouteName();
if ($route_name != 'content_moderation.admin_moderated_content') {
return;
}
if (isset($form['header']['node_bulk_form']['action']['#options']['publish_latest'])) {
// Only add this option when publish latest version action is available.
if (($form_id == 'views_form_moderated_content_moderated_content')) {
if ((!isset($form['enable_bulk_publish_validate']))) {
$form['enable_bulk_publish_validate'] = [
'#type' => 'checkbox',
'#title' => t('Enable bulk publish validation to check if the linked pages are unpublished.'),
'#default_value' => \Drupal::config('safedelete.settings')->get('enforce_bulk_publish_verify_checkpagepublished') ? \Drupal::config('safedelete.settings')->get('enforce_bulk_publish_verify_checkpagepublished') : 0,
'#required' => FALSE,
];
$form['enable_bulk_publish_validate']['#weight'] = -1;
$form['#validate'][] = 'safedelete_form_validate_enable_bulkpublish';
$form['#submit'][] = 'safedelete_form_submit_enable_bulkpublish';
}
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function safedelete_form_views_form_content_page_1_alter(&$form, FormStateInterface $form_state, $form_id) {
$route_name = \Drupal::routeMatch()->getRouteName();
if ($route_name != 'system.admin_content') {
return;
}
if (isset($form['header']['node_bulk_form']['action']['#options']['publish_latest'])) {
// Only add this option when publish latest version actoin is available.
if ($form_id == 'views_form_content_page_1') {
if ((!isset($form['enable_bulk_publish_validate']))) {
$form['enable_bulk_publish_validate']['widget'][0] = [
'#type' => 'checkbox',
'#title' => t('Enable bulk publish validation to check if the linked pages are unpublished.'),
'#default_value' => \Drupal::config('safedelete.settings')->get('enforce_bulk_publish_verify_checkpagepublished') ? \Drupal::config('safedelete.settings')->get('enforce_bulk_publish_verify_checkpagepublished') : 0,
'#required' => FALSE,
];
$form['enable_bulk_publish_validate']['#weight'] = -1;
$form['#validate'][] = 'safedelete_form_validate_enable_bulkpublish';
$form['#submit'][] = 'safedelete_form_submit_enable_bulkpublish';
}
}
}
}
/**
* Implements hook_form_alter().
*/
function safedelete_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if ((strpos($form_id, 'media') !== FALSE && strpos($form_id, 'delete_form') !== FALSE && $form_id != "media_type_delete_form")
||
($form_id == 'file_delete_form')
) {
$limit = 20;
$path = \Drupal::request()->getpathInfo();
$arg = explode('/', $path);
$langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
$index_arg = 0;
$idx = 0;
foreach ($arg as $segment) {
if (is_numeric($segment) && !is_bool($segment)) {
$index_arg = $idx;
}
$idx++;
}
if (isset($arg[$index_arg]) && is_numeric($arg[$index_arg])) {
$mid = $arg[$index_arg];
$fid = $arg[$index_arg];
}
else {
// This should never happen.
return;
}
if ($form_id == 'file_delete_form') {
AdminHelper::checkFileReferencesMessage($mid, $bundle, $show_button, $markup, 'delete', $limit = 20);
if ($show_button) {
$form['description']['#markup'] = $markup;
$form['#attached']['library'][] = 'safedelete/safedelete';
$form['actions']['submit']['#access'] = $show_button ? TRUE : FALSE;
}
else {
$form['description']['#markup'] = $markup;
// $form['actions']['submit']['#access'] = $show_button ? TRUE : FALSE;
$has_show_delete_permission = \Drupal::currentUser()->hasPermission('safedelete show delete button');
if (!$has_show_delete_permission) {
$has_show_delete_permission = \Drupal::config('safedelete.settings')->get('delete_button');
}
if (!$has_show_delete_permission) {
$form['actions']['submit']['#attributes'] = ['disabled' => $show_button ? 'enabled' : 'disabled'];
}
}
}
elseif (strpos($form_id, 'media') !== FALSE) {
AdminHelper::checkMediaReferencesMessage($mid, $bundle, $show_button, $markup, 'delete', $limit = 20);
if ($show_button) {
$form['description']['#markup'] = $markup;
$form['#attached']['library'][] = 'safedelete/safedelete';
$form['actions']['submit']['#access'] = $show_button ? TRUE : FALSE;
}
else {
$form['description']['#markup'] = $markup;
// $form['actions']['submit']['#access'] = $show_button ? TRUE : FALSE;
$form['actions']['submit']['#attributes'] = ['disabled' => $show_button ? 'enabled' : 'disabled'];
}
}
}
if (strpos($form_id, 'node') !== FALSE &&
strpos($form_id, 'delete_form') !== FALSE &&
$form_id != "node_type_delete_form") {
$limit = 20;
$path = \Drupal::request()->getpathInfo();
$arg = explode('/', $path);
$langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
$index_arg = 0;
$idx = 0;
foreach ($arg as $segment) {
if (is_numeric($segment) && !is_bool($segment)) {
$index_arg = $idx;
}
$idx++;
}
if (isset($arg[$index_arg]) && is_numeric($arg[$index_arg])) {
$nid = $arg[$index_arg];
}
else {
// This should never happen.
return;
}
AdminHelper::checkNodeReferencesMessage($nid, $bundle, $show_button, $markup, 'delete', $limit = 20);
if ($show_button) {
$form['description']['#markup'] = $markup;
$form['#attached']['library'][] = 'safedelete/safedelete';
$form['actions']['submit']['#access'] = $show_button ? TRUE : FALSE;
}
else {
$form['description']['#markup'] = $markup;
// $form['actions']['submit']['#access'] = $show_button ? TRUE : FALSE;
$form['actions']['submit']['#attributes'] = ['disabled' => $show_button ? 'enabled' : 'disabled'];
}
}
// This section must stay at the end of the form_alter.
// Handle form entity add/edit.
$form_object = $form_state->getFormObject();
if (!$form_object instanceof EntityForm) {
return;
}
$entity = $form_object->getEntity();
$entity_type_id = $entity->getEntityTypeId();
$bundle = $entity->bundle();
if (!empty($entity) && $entity_type_id == 'node') {
// Add our validation handler here.
if (str_starts_with($form_id, 'node_' . $bundle . '_edit_form') !== FALSE ||
str_starts_with($form_id, 'node_' . $bundle . '_form')) {
$safe_anchors = \Drupal::config('safedelete.settings')->get('safeanchors');
if ($safe_anchors && !in_array('safedelete_entity_form_safe_anchors_validate', $form['#validate'])) {
$form['#validate'][] = 'safedelete_entity_form_safe_anchors_validate';
}
}
}
if (!empty($entity) && $entity_type_id == 'block') {
// Disable this for now as it's not fully developped or tested.
// @todo ,finish this.
return;
$safe_anchors = \Drupal::config('safedelete.settings')->get('safeanchors');
if ($safe_anchors && !in_array('safedelete_entity_form_safe_anchors_validate', $form['#validate'])) {
$form['#validate'][] = 'safedelete_entity_form_safe_anchors_validate';
}
}
}
/**
* Submit handler for enforcing use of linkit style anchors.
*
* @param array $form
* Form definition array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state object.
*/
function safedelete_entity_form_safe_anchors_validate(array &$form, FormStateInterface $form_state) {
$language = \Drupal::languageManager()->getCurrentLanguage();
$langcode = $language->getId();
$moderation_state = '';
$form_object = $form_state->getFormObject();
$entity = NULL;
if ($form_object instanceof EntityForm) {
$entity = $form_object->getEntity();
$entity_id = NULL;
if (!$entity->isNew()) {
$entity_id = $entity->id();
}
$entity_type = $entity->getEntityTypeId();
$bundle = $entity->bundle();
if ($bundle == 'block') {
return;
}
$entityFieldManager = \Drupal::service('entity_field.manager');
$fields = $entityFieldManager->getFieldDefinitions($entity_type, $bundle);
$validate = TRUE;
if (isset($fields['moderation_state'])) {
$moderation_state = $form_state->getValue('moderation_state')[0]['value'];
$validate = $moderation_state == 'published' ? TRUE : FALSE;
if (!$validate) {
$validate = $moderation_state == 'archived' ? TRUE : FALSE;
}
}
// Process the translated values.
$values = $form_state->getValues();
$original_language = $entity->language()->getId();
// Handle moderation_state value.
if ($validate) {
if ($entity->hasField('body') && !empty($entity->body) && isset($values['body'])) {
$show_button = TRUE;
$archived_validation = FALSE;
$msgdetail_isArchived = NULL;
$body_field_val = '';
foreach ($values['body'] as $body_item) {
$body_field_val .= $body_item['value'];
}
$field_name = 'body';
if ($moderation_state == 'archived') {
$found_token_href = FALSE;
$found_absolute_url = FALSE;
$found_internal_anchor = FALSE;
$nid = $entity->id();
$bundle = $entity->bundle();
$found_suspect_node = FALSE;
AdminHelper::checkNodeReferencesMessage($nid, $bundle, $show_button, $msgdetail_isPublished, 'archived_archived', 20);
if (!$show_button) {
$found_suspect_node = TRUE;
$error_message = t('Unable to archive due to published or draft content linking to this archive candidate node. See warning message above');
$ms = &$form['moderation_state']['widget'][0];
$key = isset($ms['state']) ? 'state' : (isset($ms['target_state']) ? 'target_state' : NULL);
if ($key) {
// Attach the error ONLY to the “Change to” select.
$form_state->setError($ms[$key], $error_message);
}
else {
// Fallback (rare): target by name using parents syntax.
$form_state->setErrorByName('moderation_state][widget][0][state', $error_message);
}
}
else {
$found_suspect_node = FALSE;
$archived_validation = TRUE;
$msgdetail_isArchived = $msgdetail_isPublished;
}
}
else {
AdminHelper::checkPagePublished($body_field_val, $found_suspect_node, $msgdetail_isPublished);
AdminHelper::checkUrlToken($body_field_val, $found_token_href, $msgdetail_isToken);
AdminHelper::checkAbsoluteUrlPlus($body_field_val, $found_absolute_url, $msgdetail_isAbsoluteURL);
AdminHelper::checkAbsoluteUrl($body_field_val, $found_absolute_url, $msgdetail_isAbsoluteURL);
AdminHelper::checkInternalAnchors($body_field_val, $found_internal_anchor, $msgdetail_isInternal);
}
$markup_head_head = '<div data-drupal-messages="" class="messages-list"><div role="contentinfo" aria-labelledby="message-warning-title" class="messages-list__item messages messages--warning">';
$markup_inner_foot = '</div></div>';
$markup_foot = "</li></ul></div></div>" . $markup_inner_foot;
if ($found_token_href || $found_suspect_node || $found_absolute_url || $found_internal_anchor) {
$lang = $langcode;
if ($show_button) {
$markup_head = $markup_head_head . '<div role="alert"><div class="messages__header"><h2 id="message-warning-title" class="messages__title">' .
t('Safedelete validation (%lang)', ['%lang' => $lang]) . '</h2></div><div class="messages__content"><ul class="messages__list"><li>';
}
if (!$show_button) {
$markup_head = '';
$markup_foot = '';
}
$markup = '';
if ($found_token_href) {
$markup .= $msgdetail_isToken;
}
if ($found_suspect_node) {
$markup .= $msgdetail_isPublished;
}
if ($found_absolute_url) {
$markup .= $msgdetail_isAbsoluteURL;
}
if ($found_internal_anchor) {
$markup .= $msgdetail_isInternal;
}
$markup = Markup::create($markup_head . $markup . $markup_foot);
$form['description']['#markup'] = $markup;
$form['description']['#weight'] = '-99';
$error_message = t('Unable to publish node %entity_id. See warning message(s).', ['%entity_id' => $entity_id]);
if ($moderation_state != 'archived') {
$form_state->setErrorByName($field_name, $error_message);
\Drupal::messenger()->addMessage(t('Safe delete prevented you from publishing "%title" entity_id %id.', [
'%id' => $entity_id,
'%title' => $entity->getTitle(),
]), 'error', TRUE);
}
$found_token_href = FALSE;
$found_suspect_node = FALSE;
$found_absolute_url = FALSE;
$found_internal_anchor = FALSE;
}
else if ($archived_validation) {
\Drupal::messenger()->addMessage($msgdetail_isArchived, 'warning', TRUE);
}
$all_languages = AdminHelper::getAllEnabledLanguages();
foreach ($all_languages as $other_langcode => $other_language) {
if (isset($values[$field_name . '-etuf-' . $other_langcode])) {
$body_other_array = $values[$field_name . '-etuf-' . $other_langcode][0]['value'];
AdminHelper::checkPagePublished($body_other_array, $found_suspect_node, $msgdetail_isPublished);
AdminHelper::checkUrlToken($body_other_array, $found_token_href, $msgdetail_isToken);
AdminHelper::checkAbsoluteUrlPlus($body_other_array, $found_absolute_url, $msgdetail_isAbsoluteURL, $other_langcode);
AdminHelper::checkAbsoluteUrl($body_other_array, $found_absolute_url, $msgdetail_isAbsoluteURL);
AdminHelper::checkInternalAnchors($body_other_array, $found_internal_anchor, $msgdetail_isInternal, $other_langcode);
if ($found_token_href || $found_suspect_node || $found_absolute_url || $found_internal_anchor) {
// Reset markup.
$markup = '';
if ($found_token_href) {
$markup .= $msgdetail_isToken;
}
if ($found_suspect_node) {
$markup .= $msgdetail_isPublished;
}
if ($found_absolute_url) {
$markup .= $msgdetail_isAbsoluteURL;
}
if ($found_internal_anchor) {
$markup .= $msgdetail_isInternal;
}
$markup_head = $markup_head_head . '<div role="alert"><div class="messages__header"><h2 id="message-error-title" class="messages__title">' .
t('Safedelete validation (%lang)', ['%lang' => $other_langcode]) . '</h2></div><div class="messages__content"><ul class="messages__list"><li>';
$markup = Markup::create($markup_head . $markup . $markup_foot);
$markup = Markup::create($markup);
// $form['description']['#markup'] .= $markup;
// $form['body' . '-etuf-' . $other_langcode]['#weight'] = '-99';
$error_message = t('Unable to publish node %entity_id. See warning message(s).', ['%entity_id' => $entity_id]);
$form_state->setErrorByName($field_name . '-etuf-' . $other_langcode, $error_message);
\Drupal::messenger()->addMessage(t('Safe delete prevented you from publishing "%title" entity_id %id.', [
'%id' => $entity_id,
'%title' => $entity->getTitle(),
]), 'error', TRUE);
}
}
}
}
}
}
}
/**
* Form submission handler for node_form().
*
* Configure entity translation node object for entity_translation_unified_form.
*/
function safedelete_node_form_safe_archive_validate(array &$form, FormStateInterface $form_state) {
// @todo Review possibility of moving validate to a field constraint instead.
$language = \Drupal::languageManager()->getCurrentLanguage();
$langcode = $language->getId();
$form_object = $form_state->getFormObject();
$entity = NULL;
if ($form_object instanceof EntityForm) {
$entity = $form_object->getEntity();
if ($entity->isNew()) {
// Skipping checks because it hasn't been referenced yet.
return;
}
$entity_type = $entity->getEntityTypeId();
$bundle = $entity->bundle();
$entityFieldManager = \Drupal::service('entity_field.manager');
$fields = $entityFieldManager->getFieldDefinitions($entity_type, $bundle);
// Process the translated values.
$values = $form_state->getValues();
foreach ($fields as $field_name => $field_definition) {
if ($field_name == 'moderation_state' && is_array($values[$field_name])) {
// Handle moderation_state value.
$moderationArray = reset($values[$field_name]);
$state = $moderationArray['value'];
if ($state == 'archived') {
$nid = $entity->id();
AdminHelper::checkNodeReferencesMessage($nid, $bundle, $show_button, $markup, 'archived', $limit = 20);
if (!$show_button) {
$markup = Markup::create($markup);
$form['description']['#markup'] = $markup;
$form['description']['#weight'] = '-99';
$error_message = t('Unable to archive due to other content linking to this content. See warning message above');
$form_state->setErrorByName($field_name, $error_message);
}
}
}
$all_languages = AdminHelper::getAllEnabledLanguages();
foreach ($all_languages as $other_langcode => $other_language) {
if ($entity->hasTranslation($other_langcode)) {
$field_type = $field_definition->getType();
if ($field_name == 'moderation_state' && is_array($values[$field_name])) {
$field_name_test = $field_name;
if (isset($values[$field_name . '-etuf-' . $other_langcode])) {
$field_name_test = $field_name . '-etuf-' . $other_langcode;
}
// Handle moderation_state value.
$moderationArray = reset($values[$field_name_test]);
$state = $moderationArray['value'];
if ($state == 'archived') {
if (!$show_button) {
$form_state->setErrorByName($field_name_test, $error_message);
}
}
}
}
}
}
}
}
/**
* Implements hook_help().
*/
function safedelete_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.safedelete':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Safe Delete module is built to check for links in body fields and if there are links that will be b roken prevent the Node deletion') . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Configuring Safe Delete') . '</dt>';
$output .= '<dd>' . t('The Safe Delete module provides page for configuring the show delete button, limit, format <a href=":config">Safe Delete settings</a>.', [':config' => Url::fromRoute('safedelete.settings')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
case 'safedelete.settings':
return '<p>' . t('This page shows you all available administration tasks for Safe Delete module.') . '</p>';
}
}
/**
* Implements custom file usage hanlder for the page creation case.
*/
function _handleInsertFileUsage_PageCreation($entity, $entity_id) {
if ($entity->hasField('body')) {
$all_languages = AdminHelper::getAllEnabledLanguages();
foreach ($all_languages as $other_langcode => $other_language) {
if ($entity->hasTranslation($other_langcode)) {
// Stores fid and number of the same file count for the usage definition change.
$filearray = [];
$body_field = NULL;
$regex_mediaobj = "/data-entity-type=\"media\" data-entity-uuid=\"([a-z]|[0-9]){8}-(([0-9]|[a-z]){4}-){3}([0-9]|[a-z]){12}\"/";
$regex_uuid = "/([a-z]|[0-9]){8}-(([0-9]|[a-z]){4}-){3}([0-9]|[a-z]){12}/";
$body_field = $entity->getTranslation($other_langcode)->get('body')->value;
if (!is_null($body_field) && !empty($body_field)) {
// Using PHP 8.1 or higher, do not do a preg_match_all on null.
preg_match_all($regex_mediaobj, $body_field, $matches, PREG_SET_ORDER);
}
else {
$matches = [];
}
foreach ($matches as $match) {
$file = reset($match);
preg_match_all($regex_uuid, $file, $id_match, PREG_SET_ORDER);
$muuid = $id_match[0][0];
$mentity = \Drupal::service('entity.repository')->loadEntityByUuid('media', $muuid);
if (is_null($mentity)) {
continue;
}
$fid = $mentity->getSource()->getSourceFieldValue($mentity);
if ((array_key_exists($fid, $filearray))) {
$filearray[$fid] = $filearray[$fid] + 1;
}
else {
$filearray[$fid] = 1;
}
}
if (count($filearray) > 0) {
foreach ($filearray as $fileid => $refcount) {
$fentity = File::load($fileid);
$file_usage = \Drupal::service('file.usage');
$file_usage->add($fentity, 'editor', 'node', $entity_id);
}
}
} // end of each lang
} // end of for loop
}
}
/**
* Implements hook_theme().
*/
function safedelete_theme($existing, $type, $theme, $path) {
return [
'safedelete_orphanedpages' => [
'variables' => [
'orphanedpages_container' => NULL,
'showconfigsettinglink' => NULL,
'vars_array' => NULL,
],
],
];
}
/**
*
*/
function batch_ProcessNodes_Op($id, $operation_details, $nid, $filedirectory, $exceptionarray, $super_type, &$context) {
// Store some results for post-processing in the 'finished' callback.
// The contents of 'results' will be available as $results in the
// 'finished' function (in this example, batch_example_finished()).
$context['results'][] = $id;
$database = \Drupal::database();
// Reset flag.
$resultflag = FALSE;
$entity_manager = \Drupal::entityTypeManager();
$node = $entity_manager->getStorage('node')->load($nid);
if (isset($node) && sizeof($exceptionarray) >= 0 && !in_array($nid, $exceptionarray)) {
$nmstate = 'published';
if (!$node->isPublished()) {
$nmstate = 'unpublished';
}
if ($node->hasField('moderation_state')) {
$nmstate = $node->get('moderation_state')->getString();
}
if ($nmstate == 'published') {
$uuid = AdminHelper::getUuidFromNid($nid, $database);
$type = AdminHelper::getNodeTypeFromNid($nid, $database);
$resultreason = "";
if ($type == $super_type && !AdminHelper::hasMenuLink($nid)) {
// Do further check if the page is linked by other pages.
if (!AdminHelper::findLinkitReferences($uuid, $nid)) {
$resultflag = TRUE;
$resultreason = t('No Menu Link and No Other Pages linked');
}
}
elseif ($type != $super_type) {
// Check if the special page type is linked by other pages.
if (!AdminHelper::findLinkitReferences($uuid, $nid)) {
$resultflag = TRUE;
$resultreason = t('No Other Pages linked to this.');
}
}
}
if ($resultflag) {
// @todo Performance and scalability improvement suggestion:
// Build a custom pager and do the node load only for the current page.
// Refactor this so that the nid is stored in an array in this forloop,
// then just after the for loop process the nid array do the node load for only the current 'page'.
$langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
$ntype = $node->type->entity->label();
$ntitle = $node->get('title')->value;
$reporteddateobj = new \DateTime('now');
// array_shift($datearray);
$reporteddate = $reporteddateobj->format('Y-m-d H:i:s');
$orphanedpages_results[$nid]['nid'] = $nid;
$orphanedpages_results[$nid]['title'] = $ntitle;
$orphanedpages_results[$nid]['href'] = AdminHelper::getUrlForNode($nid, $langcode);
$orphanedpages_results[$nid]['type'] = $node->type->entity->label();
$orphanedpages_results[$nid]['reason'] = $resultreason;
$orphanedpages_results[$nid]['reporteddate'] = $reporteddate;
// Save date into the json file.
$arrayforjsonobj = [
'nid' => $nid,
'title' => $ntitle,
'type' => $node->type->entity->label(),
'href' => AdminHelper::getUrlForNode($nid, $langcode),
'reason' => $resultreason,
'reporteddate' => $reporteddate,
];
$json = json_encode($arrayforjsonobj, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$fp = $filedirectory . $nid . '.json';
$file = \Drupal::service('file.repository')->writeData($json, $fp, FileSystemInterface::EXISTS_REPLACE);
}
}
// Optional message displayed under the progressbar.
$context['message'] = t('Running Batch "@id" @details',
['@id' => $id, '@details' => $operation_details]
);
}
/**
* Batch 'finished' callback used by batch.
*/
function batch_ProcessNodes_finished($success, $results, $operations) {
$messenger = \Drupal::messenger();
if ($success) {
// Here we could do something meaningful with the results.
// We just display the number of nodes we processed...
$messenger->addMessage(t('Checking orphaned nodes proccess are successfully finished.'));
// $messenger->addMessage(t('@count nodes are processed.', ['@count' => count($results)]));
// $messenger->addMessage(t('The final result was "%final"', ['%final' => end($results)]));
}
else {
// An error occurred.
// $operations contains the operations that remained unprocessed.
$error_operation = reset($operations);
$messenger->addMessage(
t('An error occurred while processing @operation with arguments : @args',
[
'@operation' => $error_operation[0],
'@args' => print_r($error_operation[0], TRUE),
]
)
);
}
}
/**
* Implements hook_cron().
*/
function safedelete_cron() {
$reporteddate = '';
$orphanedpages_results = [];
$filedirectory = 'private://orphaned_pages_files/';
$database = \Drupal::database();
AdminHelper::listOrphanedNodes($reporteddate, $orphanedpages_results, $filedirectory, $database);
}
/**
* Implements hook_moderated_content_bulk_publish_verify_archived().
*
* @param \Drupal\moderated_content_bulk_publish\HookObject.
* An object with properties by reference.
*/
function safedelete_moderated_content_bulk_publish_verify_archived($hookObject) {
$state = 'archived';
$limit = 20;
$nid = $hookObject->nid;
$bundle = $hookObject->bundle;
$show_button = $hookObject->show_button;
$markup = $hookObject->markup;
$error_message = $hookObject->error_message;
AdminHelper::checkNodeReferencesMessage($nid, $bundle, $show_button, $markup, $state, $limit);
// Pass references back.
$hookObject->show_button = $show_button;
$hookObject->markup = $markup;
// Echo print_r($hookObject, TRUE);.
if (!$hookObject->show_button) {
$hookObject->error_message = t('Unable to archive due to other content linking to this content. See warning message(s)');
}
}
/**
* Implements hook_moderated_content_bulk_publish_verify_publish().
*
* @param \Drupal\moderated_content_bulk_publish\HookObject.
* An object with properties by reference.
*/
function safedelete_moderated_content_bulk_publish_verify_publish($hookObject) {
$state = 'publish';
$body_field_val = $hookObject->body_field_val;
$nid = $hookObject->nid;
$enforce_bulk_publish_verify_checkpagepublished = \Drupal::config('safedelete.settings')->get('enforce_bulk_publish_verify_checkpagepublished');
$found_unpublished_node = FALSE;
$msgdetail_isPublished = '';
if ($enforce_bulk_publish_verify_checkpagepublished) {
AdminHelper::checkPagePublished($body_field_val, $found_unpublished_node, $msgdetail_isPublished);
AdminHelper::checkAbsoluteUrlPlus($body_field_val, $found_absolute_url, $msgdetail_isAbsoluteURL);
if ($found_absolute_url) {
$msgdetail_isPublished .= $msgdetail_isAbsoluteURL;
}
AdminHelper::checkInternalAnchors($body_field_val, $found_internal_anchor, $msgdetail_isInternal);
if ($found_internal_anchor) {
$msgdetail_isPublished .= $msgdetail_isInternal;
}
}
AdminHelper::checkUrlToken($body_field_val, $found_token_href, $msgdetail_isToken);
AdminHelper::checkAbsoluteUrl($body_field_val, $found_absolute_url, $msgdetail_isAbsoluteURL);
if ($found_token_href || $found_unpublished_node || $found_absolute_url) {
$msgtitle = t('Unable to publish node %nid. See warning message(s).', ['%nid' => $nid]);
$hookObject->error_message = $msgtitle;
$hookObject->msgdetail_isToken = $msgdetail_isToken;
$hookObject->msgdetail_isPublished = $msgdetail_isPublished;
$hookObject->msgdetail_isAbsoluteURL = $msgdetail_isAbsoluteURL;
$hookObject->validate_failure = TRUE;
}
}
/**
* Implements hook_preprocess_status_messages().
*
* Updates or removes the status message (type = status),
* and displays the warning message (type = warning) if bulk action process fails.
*/
function safedelete_preprocess_status_messages(&$variables) {
$msg = '';
$msgCount = 0;
$action = 'Archive current revision';
if (isset($variables['message_list']['status'])) {
$msgCount = count($variables['message_list']['status']);
if ($msgCount > 0) {
// Get the last item of Status Message for the archive bulk action.
$msg = $variables['message_list']['status'][($msgCount - 1)];
};
}
// Only handles Archive current revision action.
if (isset($variables['message_list']['warning']) && (stripos($msg, $action) !== FALSE)) {
$warning_messages = $variables['message_list']['warning'];
$warningCount = count($variables['message_list']['warning']);
$totalCount = 0;
if ($warningCount > 0) {
$int_str = preg_replace('/[^0-9]/', '', $msg);
$totalCount = intval($int_str);
$diffCount = $totalCount - ($warningCount - 1);
$diff_str = strval($diffCount);
if ($diffCount == 0) {
unset($variables['message_list']['status']);
}
elseif ($diffCount == 1) {
$new_msg = t(str_replace($int_str . ' items', $diff_str . ' item', $msg));
$variables['message_list']['status'][($msgCount - 1)] = Markup::create($new_msg);
}
else {
$diff_str = strval($diffCount);
$new_msg = t(str_replace($int_str, $diff_str, $msg));
$variables['message_list']['status'][($msgCount - 1)] = Markup::create($new_msg);
}
}
}
$bulk_publish_action = 'Publish latest revision';
// Only handles Publish latest revision action.
if (isset($variables['message_list']['warning']) && (stripos($msg, $bulk_publish_action) !== FALSE)) {
$warningCount = 0;
$warning_messages = $variables['message_list']['warning'];
foreach ($warning_messages as $delta => $message) {
$pattern = '/Unable to publish node \<em class=\"placeholder\"\>\d*\<\/em\>./';
if (preg_match_all($pattern, $message, $matches)) {
$warningCount = $warningCount + 1;
}
}
if ($warningCount > 0) {
$int_str = preg_replace('/[^0-9]/', '', $msg);
$totalCount = intval($int_str);
$diffCount = $totalCount - $warningCount;
$diff_str = strval($diffCount);
if ($diffCount == 0) {
unset($variables['message_list']['status']);
}
elseif ($diffCount == 1) {
$new_msg = t(str_replace($int_str . ' items', $diff_str . ' item', $msg));
$variables['message_list']['status'][0] = Markup::create($new_msg);
}
else {
$diff_str = strval($diffCount);
$new_msg = t(str_replace($int_str, $diff_str, $msg));
$variables['message_list']['status'][0] = Markup::create($new_msg);
}
}
}
}
