Skip to content
Snippets Groups Projects

Allow input of csv import for redirects

3 files
+ 237
0
Compare changes
  • Side-by-side
  • Inline
Files
3
+ 222
0
<?php
namespace Drupal\redirect_bulk\Form;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\redirect\Entity\Redirect;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form to import csv.
*/
class CsvForm extends FormBase {
/**
* Config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new CsvForm.
*/
public function __construct(ConfigFactoryInterface $config_factory, MessengerInterface $messenger) {
$this->configFactory = $config_factory;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'redirect_bulk_import_csv';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['import'] = [
'#type' => 'file',
'#title' => $this->t('Import CSV'),
'#description' => $this->t('Upload a CSV file with redirects. Format: from, to, code, langcode (langcode is optional).'),
'#required' => TRUE,
];
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Import'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$validators = ['file_validate_extensions' => ['csv']];
$file = file_save_upload('import', $validators, FALSE, 0, FileSystemInterface::EXISTS_REPLACE);
if ($file) {
$file->setPermanent();
$file->save();
$file_path = $file->getFileUri();
$content = file_get_contents($file_path);
$redirects = $this->parseCsv($content);
if (empty($redirects)) {
$this->messenger->addError($this->t('The uploaded CSV file is empty or invalid.'));
return;
}
$success_count = 0;
$error_count = 0;
foreach ($redirects as $redirect) {
if ($this->validateRedirect($redirect)) {
$to = $this->clearUrl($redirect['to']);
if (!UrlHelper::isExternal($to)) {
$to = 'internal:/' . ltrim($to, '/');
}
$source = UrlHelper::parse($redirect['from']);
$path = $this->clearUrl($source['path']);
$query = $source['query'] ?? [];
$status_code = $redirect['code'];
$langcode = $redirect['langcode'];
$default_code = $this->configFactory->get('redirect.settings')->get('default_status_code') ?? 301;
Redirect::create([
'redirect_source' => [
'path' => $path,
'query' => $query,
],
'redirect_redirect' => [
'uri' => $to,
'title' => '',
],
'status_code' => (!empty($status_code) && is_numeric($status_code)) ? (int) trim($status_code) : $default_code,
'language' => !empty($langcode) ? $langcode : 'und',
])->save();
$success_count++;
}
else {
$error_count++;
}
}
if ($success_count > 0) {
$this->messenger->addStatus($this->t('@count redirects successfully imported.', ['@count' => $success_count]));
}
if ($error_count > 0) {
$this->messenger->addWarning($this->t('@count redirects could not be processed. Please ensure all entries follow the correct format.', ['@count' => $error_count]));
}
}
else {
$this->messenger->addError($this->t('Failed to upload the file.'));
}
}
/**
* Parses CSV content into an array of redirects.
*/
private function parseCsv(string $content): array {
$lines = array_filter(explode("\n", $content));
$redirects = [];
foreach ($lines as $line) {
$data = str_getcsv($line);
if (count($data) >= 2) {
$redirects[] = [
'from' => isset($data[0]) ? trim($data[0]) : '',
'to' => isset($data[1]) ? trim($data[1]) : '',
'code' => isset($data[2]) ? trim($data[2]) : '',
'langcode' => isset($data[3]) ? trim($data[3]) : '',
];
}
}
return $redirects;
}
/**
* Validates a single redirect entry.
*/
private function validateRedirect(array $redirect): bool {
$from = $this->clearUrl($redirect['from']);
$to = $this->clearUrl($redirect['to']);
$langcode = !empty($redirect['langcode']) ? $redirect['langcode'] : 'und';
$source = UrlHelper::parse($redirect['from']);
$path = $this->clearUrl($source['path']);
$query = $source['query'];
$available_languages = \Drupal::languageManager()->getLanguages();
$hash = Redirect::generateHash($path, $query, $langcode);
$existing_redirects = \Drupal::entityTypeManager()
->getStorage('redirect')
->loadByProperties(['hash' => $hash]);
if (!empty($existing_redirects)) {
$existing_redirect = reset($existing_redirects);
$this->messenger->addError($this->t('The source path %source is already being redirected. Edit the existing redirect <a href="@edit-url">here</a>.', [
'%source' => $path,
'@edit-url' => $existing_redirect->toUrl('edit-form')->toString(),
]));
return FALSE;
}
if (empty($from) || empty($to)) {
$this->messenger->addError($this->t('Source and destination paths cannot be empty.'));
return FALSE;
}
if ($from === $to) {
$this->messenger->addError($this->t('Source and destination paths cannot be the same.'));
return FALSE;
}
if (!empty($redirect['code']) && !in_array((int) $redirect['code'], range(300, 307), TRUE)) {
$this->messenger->addError($this->t('Invalid redirect code: @code', ['@code' => $redirect['code']]));
return FALSE;
}
if (!empty($redirect['langcode']) && !array_key_exists($langcode, $available_languages)) {
$this->messenger->addError($this->t('Invalid language code: @code', ['@code' => $redirect['langcode']]));
return FALSE;
}
return TRUE;
}
/**
* Clears and sanitizes the URL.
*/
private function clearUrl(string $url): string {
return ltrim(trim($url), '/');
}
}
Loading