ckeditor_taxonomy_glossary-1.0.0-alpha1/src/Controller/GlossaryAutocompleteController.php

src/Controller/GlossaryAutocompleteController.php
<?php

declare(strict_types=1);

namespace Drupal\ckeditor_taxonomy_glossary\Controller;

use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\taxonomy\TermInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Render\RendererInterface;

/**
 * Controller for glossary autocomplete and term description endpoints.
 */
final class GlossaryAutocompleteController extends ControllerBase {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * The form builder.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected $formBuilder;

  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Constructs a new GlossaryAutocompleteController object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
   *   The form builder.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, FormBuilderInterface $form_builder, RendererInterface $renderer, AccountInterface $current_user) {
    $this->entityTypeManager = $entity_type_manager;
    $this->languageManager = $language_manager;
    $this->formBuilder = $form_builder;
    $this->renderer = $renderer;
    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('language_manager'),
      $container->get('form_builder'),
      $container->get('renderer'),
      $container->get('current_user')
    );
  }

  /**
   * Returns autocomplete suggestions for glossary terms.
   *
   * @param string $text
   *   The text to search for.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   A JSON response with matching terms.
   */
  public function autocomplete(string $text): CacheableJsonResponse {
    $matches = [];

    // Sanitize input text
    $text = trim($text);
    
    // Only search if text is long enough and not too long
    if (strlen($text) >= 2 && strlen($text) <= 100) {
      /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
      $term_storage = $this->entityTypeManager()->getStorage('taxonomy_term');

      try {
        $query = $term_storage->getQuery()
          ->accessCheck(TRUE)
          ->condition('vid', 'glossary')
          ->condition('name', $text, 'CONTAINS')
          ->range(0, 20)
          ->sort('name');

        // Include all languages for multilingual support
        // Users can see language indicators in the autocomplete dropdown

        $tids = $query->execute();

        if (!empty($tids)) {
          /** @var \Drupal\taxonomy\TermInterface[] $terms */
          $terms = $term_storage->loadMultiple($tids);

          foreach ($terms as $term) {
            if (!$term instanceof TermInterface) {
              continue;
            }
            
            $description = '';
            if ($term->hasField('description') && !$term->get('description')->isEmpty()) {
              $description = strip_tags($term->get('description')->value);
              $description = mb_substr($description, 0, 100) . (mb_strlen($description) > 100 ? '...' : '');
            }

            $matches[] = [
              'id'          => $term->id(),
              'label'       => $term->getName(),
              'description' => $description,
              'langcode'    => $term->language()->getId(),
              'language_name' => $term->language()->getName(),
            ];
          }
        }
      } catch (\Exception $e) {
        // Log the error but don't expose it to the user
        \Drupal::logger('ckeditor_taxonomy_glossary')->error('Autocomplete search failed: @message', ['@message' => $e->getMessage()]);
      }
    }

    return $this->createCachedAutocompleteResponse($matches);
  }

  /**
   * Creates a cached autocomplete response with proper cache metadata.
   *
   * @param array $matches
   *   The autocomplete matches array.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   A cached JSON response.
   */
  private function createCachedAutocompleteResponse(array $matches): CacheableJsonResponse {
    $response = new CacheableJsonResponse($matches);
    $cache_metadata = new CacheableMetadata();
    $cache_metadata->addCacheTags(['taxonomy_term_list:glossary']);
    $cache_metadata->addCacheContexts(['url.path']);
    $response->addCacheableDependency($cache_metadata);

    // Apply cache duration setting from configuration
    $config = $this->config('ckeditor_taxonomy_glossary.settings');
    $cache_duration = $config->get('cache_duration') ?? '3600';

    if ($cache_duration === '0') {
      // No cache - set headers to prevent caching
      $response->setMaxAge(0);
      $response->setSharedMaxAge(0);
      $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
      $response->headers->set('Pragma', 'no-cache');
      $response->headers->set('Expires', '0');
    } else {
      // Set cache duration
      $cache_seconds = (int) $cache_duration;
      $response->setMaxAge($cache_seconds);
      $response->setSharedMaxAge($cache_seconds);
    }

    return $response;
  }

  /**
   * Returns the description for a specific glossary term.
   *
   * @param int $tid
   *   The term ID.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   A JSON response with the term description.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
   *   If the term is not found or not in the glossary vocabulary.
   */
  public function termDescription(int $tid): CacheableJsonResponse {
    /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
    $term_storage = $this->entityTypeManager()->getStorage('taxonomy_term');

    /** @var \Drupal\taxonomy\TermInterface|null $term */
    $term = $term_storage->load($tid);

    if (!$term instanceof TermInterface || $term->bundle() !== 'glossary') {
      throw new NotFoundHttpException();
    }

    // Use translated version if available
    $current_language = $this->languageManager()->getCurrentLanguage()->getId();
    if ($term->hasTranslation($current_language)) {
      $term = $term->getTranslation($current_language);
    }

    $description = '';
    if ($term->hasField('description') && !$term->get('description')->isEmpty()) {
      $description = $term->get('description')->processed;
    }

    $data = [
      'id'          => $term->id(),
      'name'        => $term->getName(),
      'description' => $description,
      'langcode'    => $term->language()->getId(),
      'language_name' => $term->language()->getName(),
    ];

    $response = new CacheableJsonResponse($data);
    $response->addCacheableDependency($term);

    // Apply cache duration setting from configuration
    $config = $this->config('ckeditor_taxonomy_glossary.settings');
    $cache_duration = $config->get('cache_duration') ?? '3600';

    if ($cache_duration === '0') {
      // No cache - set headers to prevent caching
      $response->setMaxAge(0);
      $response->setSharedMaxAge(0);
      $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
      $response->headers->set('Pragma', 'no-cache');
      $response->headers->set('Expires', '0');
    } else {
      // Set cache duration
      $cache_seconds = (int) $cache_duration;
      $response->setMaxAge($cache_seconds);
      $response->setSharedMaxAge($cache_seconds);
    }

    return $response;
  }

  /**
   * Returns basic term information for a specific glossary term.
   *
   * @param int $tid
   *   The term ID.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   A JSON response with basic term information.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
   *   If the term is not found or not in the glossary vocabulary.
   */
  public function termInfo(int $tid): CacheableJsonResponse {
    /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
    $term_storage = $this->entityTypeManager()->getStorage('taxonomy_term');

    /** @var \Drupal\taxonomy\TermInterface|null $term */
    $term = $term_storage->load($tid);

    if (!$term instanceof TermInterface || $term->bundle() !== 'glossary') {
      throw new NotFoundHttpException();
    }

    // Use translated version if available
    $current_language = $this->languageManager()->getCurrentLanguage()->getId();
    if ($term->hasTranslation($current_language)) {
      $term = $term->getTranslation($current_language);
    }

    $data = [
      'id'   => $term->id(),
      'name' => $term->getName(),
      'langcode' => $term->language()->getId(),
      'language_name' => $term->language()->getName(),
    ];

    $response = new CacheableJsonResponse($data);
    $response->addCacheableDependency($term);

    // Apply cache duration setting from configuration
    $config = $this->config('ckeditor_taxonomy_glossary.settings');
    $cache_duration = $config->get('cache_duration') ?? '3600';

    if ($cache_duration === '0') {
      // No cache - set headers to prevent caching
      $response->setMaxAge(0);
      $response->setSharedMaxAge(0);
      $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
      $response->headers->set('Pragma', 'no-cache');
      $response->headers->set('Expires', '0');
    } else {
      // Set cache duration
      $cache_seconds = (int) $cache_duration;
      $response->setMaxAge($cache_seconds);
      $response->setSharedMaxAge($cache_seconds);
    }

    return $response;
  }

  /**
   * Returns autocomplete suggestions for glossary terms in a specific language.
   *
   * @param string $text
   *   The text to search for.
   * @param string $langcode
   *   The language code to filter by.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   A JSON response with matching terms.
   */
  public function autocompleteByLanguage(string $text, string $langcode): CacheableJsonResponse {
    $matches = [];

    // Only search if text is long enough
    if (strlen($text) >= 2) {
      /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
      $term_storage = $this->entityTypeManager()->getStorage('taxonomy_term');

      $query = $term_storage->getQuery()
        ->accessCheck(TRUE)
        ->condition('vid', 'glossary')
        ->condition('name', $text, 'CONTAINS')
        ->condition('langcode', $langcode)
        ->range(0, 10)
        ->sort('name');

      $tids = $query->execute();

      if (!empty($tids)) {
        /** @var \Drupal\taxonomy\TermInterface[] $terms */
        $terms = $term_storage->loadMultiple($tids);

        foreach ($terms as $term) {
          $description = '';
          if ($term->hasField('description') && !$term->get('description')->isEmpty()) {
            $description = strip_tags($term->get('description')->value);
            $description = mb_substr($description, 0, 100) . (mb_strlen($description) > 100 ? '...' : '');
          }

          $matches[] = [
            'id'            => $term->id(),
            'label'         => $term->getName(),
            'description'   => $description,
            'langcode'      => $term->language()->getId(),
            'language_name' => $term->language()->getName(),
          ];
        }
      }
    }

    return $this->createCachedAutocompleteResponse($matches);
  }

  /**
   * Returns the description for a specific glossary term in a specific language.
   *
   * @param int $tid
   *   The term ID.
   * @param string $langcode
   *   The language code.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   A JSON response with the term description.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
   *   If the term is not found or not in the glossary vocabulary.
   */
  public function termDescriptionByLanguage(int $tid, string $langcode): CacheableJsonResponse {
    /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
    $term_storage = $this->entityTypeManager()->getStorage('taxonomy_term');

    /** @var \Drupal\taxonomy\TermInterface|null $term */
    $term = $term_storage->load($tid);

    if (!$term instanceof TermInterface || $term->bundle() !== 'glossary') {
      throw new NotFoundHttpException();
    }

    // Use specific language version if available
    if ($term->hasTranslation($langcode)) {
      $term = $term->getTranslation($langcode);
    } else {
      // If translation doesn't exist, throw not found
      throw new NotFoundHttpException();
    }

    $description = '';
    if ($term->hasField('description') && !$term->get('description')->isEmpty()) {
      $description = $term->get('description')->processed;
    }

    $data = [
      'id'            => $term->id(),
      'name'          => $term->getName(),
      'description'   => $description,
      'langcode'      => $term->language()->getId(),
      'language_name' => $term->language()->getName(),
    ];

    $response = new CacheableJsonResponse($data);
    $response->addCacheableDependency($term);

    // Apply cache duration setting from configuration
    $config = $this->config('ckeditor_taxonomy_glossary.settings');
    $cache_duration = $config->get('cache_duration') ?? '3600';

    if ($cache_duration === '0') {
      // No cache - set headers to prevent caching
      $response->setMaxAge(0);
      $response->setSharedMaxAge(0);
      $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
      $response->headers->set('Pragma', 'no-cache');
      $response->headers->set('Expires', '0');
    } else {
      // Set cache duration
      $cache_seconds = (int) $cache_duration;
      $response->setMaxAge($cache_seconds);
      $response->setSharedMaxAge($cache_seconds);
    }

    return $response;
  }

  /**
   * Creates a new glossary term via AJAX.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   A JSON response with the created term data or error message.
   */
  public function createTerm(Request $request): JsonResponse {
    // Check permission
    if (!$this->currentUser->hasPermission('create glossary terms via editor')) {
      return new JsonResponse(['error' => 'Access denied'], 403);
    }

    $data = json_decode($request->getContent(), TRUE);
    
    // Validate JSON decode was successful
    if (json_last_error() !== JSON_ERROR_NONE) {
      return new JsonResponse(['error' => 'Invalid JSON data'], 400);
    }
    
    if (empty($data['name'])) {
      return new JsonResponse(['error' => 'Term name is required'], 400);
    }

    // Sanitize and validate input data
    $name = trim($data['name']);
    $description = isset($data['description']) ? trim($data['description']) : '';
    $langcode = isset($data['langcode']) ? trim($data['langcode']) : '';
    
    // Validate name length
    if (strlen($name) > 255) {
      return new JsonResponse(['error' => 'Term name is too long (maximum 255 characters)'], 400);
    }
    
    // Validate description length
    if (strlen($description) > 500) {
      return new JsonResponse(['error' => 'Description is too long (maximum 500 characters)'], 400);
    }
    
    // Validate language code format if provided
    if (!empty($langcode) && !preg_match('/^[a-z]{2}(-[a-z]{2})?$/', $langcode)) {
      return new JsonResponse(['error' => 'Invalid language code format'], 400);
    }

    try {
      /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
      $term_storage = $this->entityTypeManager()->getStorage('taxonomy_term');
      
      // Determine language code first
      $langcode = !empty($langcode) ? $langcode : $this->languageManager()->getCurrentLanguage()->getId();
      
      // Check if term already exists in the same language
      $existing_terms = $term_storage->loadByProperties([
        'vid' => 'glossary',
        'name' => $name,
        'langcode' => $langcode,
      ]);
      
      if (!empty($existing_terms)) {
        $existing_term = reset($existing_terms);
        return new JsonResponse([
          'error' => 'Term already exists',
          'existing_term' => [
            'id' => $existing_term->id(),
            'name' => $existing_term->getName(),
          ]
        ], 409);
      }

      // Create new term
      
      $term_data = [
        'vid' => 'glossary',
        'name' => $name,
        'langcode' => $langcode,
      ];
      
      // Add description if provided
      if (!empty($description)) {
        $term_data['description'] = [
          'value' => $description,
          'format' => 'basic_html', // Use a safe format
        ];
      }

      /** @var \Drupal\taxonomy\TermInterface $term */
      $term = $term_storage->create($term_data);
      $term->save();

      return new JsonResponse([
        'success' => TRUE,
        'term' => [
          'id' => $term->id(),
          'name' => $term->getName(),
          'description' => !empty($description) ? $description : '',
          'langcode' => $term->language()->getId(),
          'language_name' => $term->language()->getName(),
        ]
      ]);
      
    } catch (\Exception $e) {
      \Drupal::logger('ckeditor_taxonomy_glossary')->error('Failed to create term: @message', ['@message' => $e->getMessage()]);
      return new JsonResponse(['error' => 'Failed to create term'], 500);
    }
  }

  /**
   * Returns a simple form for creating new terms.
   *
   * @param int|null $tid
   *   Optional term ID for editing (not implemented yet).
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   A JSON response with form HTML.
   */
  public function getTermForm($tid = NULL): JsonResponse {
    // Check permission
    if (!$this->currentUser->hasPermission('create glossary terms via editor')) {
      return new JsonResponse(['error' => 'Access denied'], 403);
    }

    // For now, just return a simple form structure
    // In the future, this could return a full Drupal form
    $form_html = '
      <div class="glossary-term-form">
        <div class="form-item">
          <label for="term-name">Term Name *</label>
          <input type="text" id="term-name" name="name" required maxlength="255" />
        </div>
        <div class="form-item">
          <label for="term-description">Description</label>
          <textarea id="term-description" name="description" rows="3" maxlength="500"></textarea>
        </div>
        <div class="form-actions">
          <button type="button" class="btn btn-primary" id="create-term-btn">Create Term</button>
          <button type="button" class="btn btn-secondary" id="cancel-term-btn">Cancel</button>
        </div>
      </div>
    ';

    return new JsonResponse([
      'form_html' => $form_html,
      'title' => 'Create New Glossary Term'
    ]);
  }
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc