media_duplicate_check-1.0.0/media_duplicate_check.module
media_duplicate_check.module
<?php
/**
* @file
* Primary module hooks for Media Duplicate Check module.
*/
declare(strict_types=1);
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Render\Markup;
use Drupal\file\Entity\File;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Helper function to translate strings.
*/
function _media_duplicate_check_t($string, array $args = [], array $options = []) {
return new TranslatableMarkup($string, $args, $options);
}
/**
* Implements hook_form_alter().
*/
function media_duplicate_check_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Check if the module is enabled.
$config = \Drupal::config('media_duplicate_check.settings');
$enabled = $config->get('enabled');
// Skip if module is disabled
if (empty($enabled)) {
return;
}
// Target media add/edit forms - check specific form IDs
$media_form_ids = [
'media_image_add_form',
'media_image_edit_form',
'media_document_add_form',
'media_document_edit_form',
'media_svg_image_add_form',
'media_svg_image_edit_form',
];
if (in_array($form_id, $media_form_ids)) {
// Check if this media type should be checked.
$media_type = NULL;
if (preg_match('/^media_([^_]+)_/', $form_id, $matches)) {
$media_type = $matches[1];
}
$check_media_types = $config->get('check_media_types') ?? ['image', 'document', 'svg_image'];
// Handle both array formats (indexed and associative)
if (is_array($check_media_types)) {
// If it's an associative array from config, get the keys
$check_media_types = array_filter(array_keys($check_media_types));
}
if ($media_type && in_array($media_type, $check_media_types)) {
_media_duplicate_check_alter_media_form($form, $form_state, $form_id);
}
}
// Also target inline entity forms for media.
if ($form_id === 'media_library_add_form_upload') {
_media_duplicate_check_alter_media_library_form($form, $form_state);
}
// Also target the media library upload form variants
if (strpos($form_id, 'media_library_add_form') === 0) {
_media_duplicate_check_alter_media_library_form($form, $form_state);
}
}
/**
* Alter media entity forms to add duplicate checking.
*/
function _media_duplicate_check_alter_media_form(&$form, FormStateInterface $form_state, $form_id) {
// Attach our library.
$form['#attached']['library'][] = 'media_duplicate_check/duplicate_check';
// Add JavaScript translations.
$form['#attached']['drupalSettings']['media_duplicate_check']['translations'] = [
'existing_media' => _media_duplicate_check_t('Existing media:')->render(),
'warning_message' => _media_duplicate_check_t('Warning message')->render(),
'duplicate_file_detected' => _media_duplicate_check_t('Duplicate file detected: @filename')->render(),
'yes_upload_anyway' => _media_duplicate_check_t('Yes, Upload Anyway')->render(),
'cancel' => _media_duplicate_check_t('Cancel')->render(),
'status_message' => _media_duplicate_check_t('Status message')->render(),
'upload_confirmed_auto_name' => _media_duplicate_check_t('Upload confirmed. The file will be uploaded with an automatically generated name to avoid conflicts.')->render(),
'information_message' => _media_duplicate_check_t('Information message')->render(),
'upload_cancelled' => _media_duplicate_check_t('Upload cancelled. Please select a different file.')->render(),
'upload_confirmed_save' => _media_duplicate_check_t('Upload confirmed. You may now save the media item.')->render(),
];
// Find the file upload field.
$file_fields = [
'field_media_image',
'field_media_document',
'field_media_svg_image',
];
foreach ($file_fields as $field_name) {
if (isset($form[$field_name])) {
// Add a container for duplicate warnings.
$form['duplicate_check_container'] = [
'#type' => 'container',
'#attributes' => ['id' => 'duplicate-check-wrapper'],
'#weight' => -100,
];
// Add hidden field to track user confirmation.
$form['duplicate_confirmed'] = [
'#type' => 'hidden',
'#default_value' => 0,
'#attributes' => ['id' => 'duplicate-confirmed'],
];
// Store the field name in form state for the validator
$form_state->set('media_file_field', $field_name);
break;
}
}
}
/**
* Alter media library upload form.
*/
function _media_duplicate_check_alter_media_library_form(&$form, FormStateInterface $form_state) {
// Attach our library.
$form['#attached']['library'][] = 'media_duplicate_check/duplicate_check';
// Add JavaScript translations.
$form['#attached']['drupalSettings']['media_duplicate_check']['translations'] = [
'existing_media' => _media_duplicate_check_t('Existing media:')->render(),
'warning_message' => _media_duplicate_check_t('Warning message')->render(),
'duplicate_file_detected' => _media_duplicate_check_t('Duplicate file detected: @filename')->render(),
'yes_upload_anyway' => _media_duplicate_check_t('Yes, Upload Anyway')->render(),
'cancel' => _media_duplicate_check_t('Cancel')->render(),
'status_message' => _media_duplicate_check_t('Status message')->render(),
'upload_confirmed_auto_name' => _media_duplicate_check_t('Upload confirmed. The file will be uploaded with an automatically generated name to avoid conflicts.')->render(),
'information_message' => _media_duplicate_check_t('Information message')->render(),
'upload_cancelled' => _media_duplicate_check_t('Upload cancelled. Please select a different file.')->render(),
'upload_confirmed_save' => _media_duplicate_check_t('Upload confirmed. You may now save the media item.')->render(),
];
// Add a container for duplicate warnings.
$form['duplicate_check_container'] = [
'#type' => 'container',
'#attributes' => ['id' => 'duplicate-check-wrapper'],
'#weight' => -100,
];
// Add hidden field to track user confirmation.
$form['duplicate_confirmed'] = [
'#type' => 'hidden',
'#default_value' => 0,
'#attributes' => ['id' => 'duplicate-confirmed'],
];
// The media library uses different field structures
// Look for upload fields in various possible locations
if (isset($form['container']['upload'])) {
// This is for the main upload field in media library
$form['media_library_file_field'] = [
'#type' => 'value',
'#value' => 'container][upload',
];
}
// Also check for media type specific uploads
$media_types = ['image', 'document', 'video', 'audio'];
foreach ($media_types as $type) {
if (isset($form['container'][$type])) {
$form['media_library_file_field'] = [
'#type' => 'value',
'#value' => "container][$type",
];
break;
}
}
}
/**
* AJAX callback for checking duplicate files.
*/
function media_duplicate_check_ajax_callback(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
// Get the uploaded file information.
$triggering_element = $form_state->getTriggeringElement();
// Try to find the field name from the triggering element's parents
$field_name = NULL;
if (isset($triggering_element['#parents'])) {
$parents = $triggering_element['#parents'];
if (!empty($parents[0])) {
$field_name = $parents[0];
}
}
// If we still don't have a field name, check common media fields
if (!$field_name) {
$file_fields = ['field_media_image', 'field_media_document', 'field_media_svg_image'];
foreach ($file_fields as $field) {
if ($form_state->hasValue([$field, 0, 'fids'])) {
$field_name = $field;
break;
}
}
}
if (!$field_name) {
return $response;
}
// Get the file from form state.
$file_ids = $form_state->getValue([$field_name, 0, 'fids']);
if (empty($file_ids)) {
// Clear any previous warnings.
$response->addCommand(new HtmlCommand('#duplicate-check-wrapper', ''));
return $response;
}
$file = File::load(reset($file_ids));
if (!$file) {
return $response;
}
$filename = $file->getFilename();
$file_uri = $file->getFileUri();
// Check for duplicates by hash (if the file exists) or by filename.
$duplicate_checker = \Drupal::service('media_duplicate_check.duplicate_checker');
$duplicates = $duplicate_checker->findDuplicatesByFilename($filename, NULL, $file_uri);
if (!empty($duplicates)) {
// Build the warning message.
$warning_content = _media_duplicate_check_build_warning($duplicates, $filename);
// Add the warning to the form.
$response->addCommand(new HtmlCommand('#duplicate-check-wrapper', $warning_content));
// Add visual warning to file upload area
$response->addCommand(new InvokeCommand('.file-upload-js', 'addClass', ['file-upload-duplicate-warning']));
// Show modal with duplicate media preview.
$modal_content = _media_duplicate_check_build_modal_content($duplicates, $filename);
$response->addCommand(new OpenModalDialogCommand(
_media_duplicate_check_t('Duplicate File Detected'),
$modal_content,
[
'width' => 700,
'height' => 'auto',
'dialogClass' => 'duplicate-check-modal',
'modal' => TRUE,
'draggable' => FALSE,
'resizable' => FALSE,
]
));
// Disable form submission until confirmed
$response->addCommand(new InvokeCommand('form input[type="submit"], form button[type="submit"]', 'prop', ['disabled', true]));
}
else {
// Clear any previous warnings.
$response->addCommand(new HtmlCommand('#duplicate-check-wrapper', ''));
$response->addCommand(new InvokeCommand('#duplicate-confirmed', 'val', ['1']));
$response->addCommand(new InvokeCommand('.file-upload-js', 'removeClass', ['file-upload-duplicate-warning']));
// Re-enable form submission
$response->addCommand(new InvokeCommand('form input[type="submit"], form button[type="submit"]', 'prop', ['disabled', false]));
}
return $response;
}
/**
* AJAX callback for media library upload.
*/
function media_duplicate_check_library_ajax_callback(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
// Get uploaded files.
$files = $form_state->getValue(['container', 'upload']);
if (empty($files)) {
return $response;
}
$duplicate_checker = \Drupal::service('media_duplicate_check.duplicate_checker');
$all_duplicates = [];
foreach ($files as $file_id) {
$file = File::load($file_id);
if ($file) {
$filename = $file->getFilename();
$duplicates = $duplicate_checker->findDuplicatesByFilename($filename);
if (!empty($duplicates)) {
$all_duplicates[$filename] = $duplicates;
}
}
}
if (!empty($all_duplicates)) {
$warning_content = '';
foreach ($all_duplicates as $filename => $duplicates) {
$warning_content .= _media_duplicate_check_build_warning($duplicates, $filename);
}
$response->addCommand(new HtmlCommand('#duplicate-check-wrapper', $warning_content));
// Build combined modal content.
$modal_content = '<div class="duplicate-check-modal-content">';
foreach ($all_duplicates as $filename => $duplicates) {
$modal_content .= _media_duplicate_check_build_modal_content($duplicates, $filename);
}
$modal_content .= '</div>';
$response->addCommand(new OpenModalDialogCommand(
_media_duplicate_check_t('Duplicate Files Detected'),
$modal_content,
[
'width' => 700,
'height' => 'auto',
'dialogClass' => 'duplicate-check-modal',
]
));
}
else {
$response->addCommand(new HtmlCommand('#duplicate-check-wrapper', ''));
$response->addCommand(new InvokeCommand('#duplicate-confirmed', 'val', ['1']));
}
return $response;
}
/**
* Build warning message for duplicate files.
*/
function _media_duplicate_check_build_warning(array $duplicates, string $filename) {
$count = count($duplicates);
$message = \Drupal::translation()->formatPlural(
$count,
'A media item with the filename "@filename" already exists.',
'@count media items with the filename "@filename" already exist.',
['@filename' => $filename]
);
// Build existing media links
$existing_media_links = [];
foreach ($duplicates as $media) {
$duplicate_checker = \Drupal::service('media_duplicate_check.duplicate_checker');
$preview_data = $duplicate_checker->getMediaPreviewData($media);
$existing_media_links[] = [
'#type' => 'link',
'#title' => $preview_data['name'] . ' (' . $preview_data['type'] . ', ' . $preview_data['created_formatted'] . ')',
'#url' => \Drupal\Core\Url::fromUri($preview_data['url']),
'#attributes' => [
'target' => '_blank',
'class' => ['existing-media-link'],
],
];
}
$build = [
'#type' => 'html_tag',
'#tag' => 'details',
'#attributes' => [
'class' => ['duplicate-warning', 'claro-details'],
'open' => TRUE,
'data-once' => 'details',
],
'summary' => [
'#type' => 'html_tag',
'#tag' => 'summary',
'#value' => _media_duplicate_check_t('Duplicate file detected: @filename', ['@filename' => $filename]),
'#attributes' => [
'class' => ['claro-details__summary'],
'role' => 'button',
'aria-expanded' => 'true',
],
],
'wrapper' => [
'#type' => 'container',
'#attributes' => [
'class' => ['claro-details__wrapper', 'details-wrapper'],
],
'message' => [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $message,
'#attributes' => [
'class' => ['duplicate-warning__message'],
],
],
'existing_media' => [
'#type' => 'container',
'#attributes' => [
'class' => ['existing-media-links', 'duplicate-warning__existing-media'],
],
'header' => [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => _media_duplicate_check_t('Existing media:'),
'#attributes' => [
'class' => ['existing-media-links__header'],
],
],
'list' => [
'#type' => 'html_tag',
'#tag' => 'ul',
'#attributes' => [
'class' => ['existing-media-links__list'],
],
'items' => array_map(function($link) {
return [
'#type' => 'html_tag',
'#tag' => 'li',
'#value' => \Drupal::service('renderer')->render($link),
'#attributes' => [
'class' => ['existing-media-links__item'],
],
];
}, $existing_media_links),
],
],
'actions' => [
'#type' => 'container',
'#attributes' => [
'class' => ['duplicate-warning-actions', 'form-actions'],
],
'proceed' => [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => _media_duplicate_check_t('Yes, Upload Anyway'),
'#attributes' => [
'type' => 'button',
'class' => ['button', 'button--danger', 'duplicate-proceed-btn'],
'data-filename' => $filename,
],
],
'cancel' => [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => _media_duplicate_check_t('Cancel'),
'#attributes' => [
'type' => 'button',
'class' => ['button', 'button--primary', 'duplicate-cancel-btn' ],
],
],
],
],
];
return \Drupal::service('renderer')->render($build);
}
/**
* Build modal content showing existing media.
*/
function _media_duplicate_check_build_modal_content(array $duplicates, string $filename) {
$duplicate_checker = \Drupal::service('media_duplicate_check.duplicate_checker');
// Build existing media items using render arrays
$existing_media_items = [];
foreach ($duplicates as $media) {
$preview_data = $duplicate_checker->getMediaPreviewData($media);
$media_item = [
'#type' => 'container',
'#attributes' => [
'class' => ['existing-media-item', 'claro-details__item'],
],
'content' => [
'#type' => 'container',
'#attributes' => [
'class' => ['existing-media-item__content'],
],
'thumbnail' => NULL,
'info' => [
'#type' => 'container',
'#attributes' => [
'class' => ['existing-media-item__info'],
],
'name' => [
'#type' => 'html_tag',
'#tag' => 'h4',
'#value' => $preview_data['name'],
'#attributes' => [
'class' => ['existing-media-item__name'],
],
],
'details' => [
'#type' => 'container',
'#attributes' => [
'class' => ['existing-media-item__details'],
],
'type' => [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => _media_duplicate_check_t('Type: @type', ['@type' => $preview_data['type']]),
'#attributes' => [
'class' => ['existing-media-item__type'],
],
],
'created' => [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => _media_duplicate_check_t('Created: @date', ['@date' => $preview_data['created_formatted']]),
'#attributes' => [
'class' => ['existing-media-item__created'],
],
],
],
'actions' => [
'#type' => 'container',
'#attributes' => [
'class' => ['existing-media-item__actions'],
],
'view_link' => [
'#type' => 'link',
'#title' => _media_duplicate_check_t('View Media'),
'#url' => \Drupal\Core\Url::fromUri($preview_data['url']),
'#attributes' => [
'class' => ['button', 'button--small', 'button--danger'],
'target' => '_blank',
],
],
],
],
],
];
// Add thumbnail if available
if (!empty($preview_data['thumbnail'])) {
$media_item['content']['thumbnail'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['existing-media-item__thumbnail'],
],
'image' => [
'#type' => 'html_tag',
'#tag' => 'img',
'#attributes' => [
'src' => $preview_data['thumbnail'],
'alt' => $preview_data['name'],
'class' => ['existing-media-item__image'],
],
],
];
}
// Add file size if available
if (!empty($preview_data['file_size'])) {
$media_item['content']['info']['details']['size'] = [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => _media_duplicate_check_t('Size: @size', ['@size' => $preview_data['file_size']]),
'#attributes' => [
'class' => ['existing-media-item__size'],
],
];
}
$existing_media_items[] = $media_item;
}
// Build the complete modal content using render arrays
$build = [
'#type' => 'container',
'#attributes' => [
'class' => ['duplicate-media-preview'],
'data-filename' => $filename,
],
'description' => [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => _media_duplicate_check_t('The following media items already exist with the filename "@filename":', ['@filename' => $filename]),
'#attributes' => [
'class' => ['duplicate-media-preview__description'],
],
],
'existing_media' => [
'#type' => 'container',
'#attributes' => [
'class' => ['existing-media-list', 'claro-details'],
],
'items' => $existing_media_items,
],
'actions' => [
'#type' => 'container',
'#attributes' => [
'class' => ['duplicate-actions', 'form-actions'],
],
'question' => [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => _media_duplicate_check_t('Do you want to continue uploading this file?'),
'#attributes' => [
'class' => ['duplicate-actions__question'],
],
],
'buttons' => [
'#type' => 'container',
'#attributes' => [
'class' => ['duplicate-actions__buttons'],
],
'confirm' => [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => _media_duplicate_check_t('Yes, Continue Upload'),
'#attributes' => [
'type' => 'button',
'class' => ['button', 'button--danger', 'duplicate-confirm-btn'],
'onclick' => 'mediaDuplicateCheck.confirmUpload()',
],
],
'cancel' => [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => _media_duplicate_check_t('Cancel'),
'#attributes' => [
'type' => 'button',
'class' => ['button', 'button--primary', 'duplicate-cancel-btn' ],
'onclick' => 'mediaDuplicateCheck.cancelUpload()',
],
],
],
],
];
return \Drupal::service('renderer')->render($build);
}