countdown-8.x-1.8/countdown.module

countdown.module
<?php

/**
 * @file
 * Countdown module for integrating countdown timers.
 */

declare(strict_types=1);

use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\countdown\CountdownConstants;
use Drupal\countdown\Utility\ConfigAccessor;

/**
 * Implements hook_help().
 */
function countdown_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.countdown':
      return _countdown_help_page();

    case 'countdown.settings':
      return '<p>' . t('Configure the default countdown library and display settings. The module will automatically detect installed libraries.') . '</p>';
  }
}

/**
 * Implements hook_theme().
 */
function countdown_theme($existing, $type, $theme, $path): array {
  return [
    'countdown_form' => [
      'render element' => 'form',
    ],
    'countdown_preview' => [
      'variables' => [
        'attributes' => [],
        'config' => [],
        'countdown_markup' => [],
        'debug_info' => NULL,
      ],
      'template' => 'countdown-preview',
      'path' => $path . '/templates',
    ],
    'countdown_meta' => [
      'variables' => [
        'attributes' => [],
        'meta' => [],
      ],
      'template' => 'countdown-meta',
      'path' => $path . '/templates',
    ],
    'countdown' => [
      'variables' => [
        'library' => NULL,
        'target_date' => NULL,
        'countdown_url' => NULL,
        'countdown_event_name' => NULL,
        'format' => NULL,
        'attributes' => [],
        'settings' => [],
      ],
      'template' => 'countdown',
      'path' => $path . '/templates',
    ],
  ];
}

/**
 * Implements hook_preprocess_HOOK() for HTML.
 */
function countdown_preprocess_html(&$variables) {
  // Get the current route match object.
  $route_match = \Drupal::routeMatch();

  // Check if the current route is the core countdown settings form.
  if ($route_match->getRouteName() === 'countdown.settings') {
    $variables['attributes']['class'][] = 'countdown-form';
  }
}

/**
 * Implements hook_library_info_build().
 */
function countdown_library_info_build() {
  // Skip during installation.
  if (InstallerKernel::installationAttempted()) {
    return [];
  }

  try {
    $library_manager = \Drupal::service('countdown.library_manager');
    $active_library = $library_manager->getActiveLibrary();

    // Core library is defined statically in countdown.libraries.yml.
    if ($active_library !== 'countdown') {
      $definition = $library_manager->getLibraryDefinition();
      return !empty($definition) ? ['countdown.active' => $definition] : [];
    }
  }
  catch (\Exception $e) {
    \Drupal::logger('countdown')->error('Failed to build library definition: @message', [
      '@message' => $e->getMessage(),
    ]);
  }

  return [];
}

/**
 * Implements hook_library_info_alter().
 */
function countdown_library_info_alter(&$libraries, $extension) {
  // Only process countdown libraries.
  if ($extension !== 'countdown') {
    return;
  }

  try {
    $config_accessor = new ConfigAccessor(\Drupal::configFactory());
    $method = $config_accessor->getLoadingMethod();
    $plugin_manager = \Drupal::service('plugin.manager.countdown_library');
    $debug_mode = $config_accessor->isDebugMode();

    // Process each installed plugin.
    foreach ($plugin_manager->getInstalledPlugins() as $plugin_id => $plugin) {
      // Skip core library.
      if ($plugin->getType() === 'core') {
        continue;
      }

      // Handle LOCAL method - update paths if library is in non-default folder.
      if ($method === 'local') {
        // Get the actual installation path.
        $library_path = $plugin->getLibraryPath();
        if (!$library_path) {
          continue;
        }

        // Extract the actual folder name from path.
        // Examples:
        // - /libraries/tick-master -> tick-master
        // - /sites/default/libraries/tick-master -> tick-master.
        preg_match('#/libraries/([^/]+)#', $library_path, $matches);
        $actual_folder = $matches[1] ?? NULL;

        // Skip if folder name can't be extracted or matches plugin_id.
        if (!$actual_folder || $actual_folder === $plugin_id) {
          continue;
        }

        // Replace all occurrences of /libraries/{plugin_id}/
        // with /libraries/{actual_folder}/.
        $search = '/libraries/' . $plugin_id . '/';
        $replace = '/libraries/' . $actual_folder . '/';

        foreach ($libraries as $library_name => &$library_def) {
          // Accept this plugin's own libraries with a proper name boundary.
          // Boundaries: '.', '_', '-', or end of string.
          $belongs = (bool) preg_match(
            '/^' . preg_quote($plugin_id, '/') . '(?:[._-]|$)/',
            $library_name
          );

          // Accept Tick→Flip cross-view only when current plugin is "flip".
          // Matches: tick_flip, tick.view.flip, tick-flip, tick.flip.min, etc.
          if (!$belongs && $plugin_id === 'flip') {
            if (preg_match('/^tick(?:[._-]?flip)(?:[._-]|$)/', $library_name)) {
              $belongs = TRUE;
            }
          }

          if (!$belongs) {
            continue;
          }

          // Skip CDN libraries in local mode.
          if (strpos($library_name, '_cdn') !== FALSE) {
            continue;
          }

          // Update JS paths.
          if (isset($library_def['js'])) {
            $new_js = [];
            foreach ($library_def['js'] as $path => $options) {
              // Skip external URLs.
              if (isset($options['type']) && $options['type'] === 'external') {
                $new_js[$path] = $options;
              }
              else {
                $new_js[str_replace($search, $replace, $path)] = $options;
              }
            }
            $library_def['js'] = $new_js;
          }

          // Update CSS paths.
          if (isset($library_def['css'])) {
            foreach ($library_def['css'] as $category => &$css_files) {
              $new_css = [];
              foreach ($css_files as $path => $options) {
                // Skip external URLs.
                if (isset($options['type']) && $options['type'] === 'external') {
                  $new_css[$path] = $options;
                }
                else {
                  $new_css[str_replace($search, $replace, $path)] = $options;
                }
              }
              $library_def['css'][$category] = $new_css;
            }
          }
        }

        // Log the change if debug mode is enabled.
        if ($debug_mode) {
          \Drupal::logger('countdown')->debug('Updated local paths for @plugin: "@search" to "@replace"', [
            '@plugin' => $plugin_id,
            '@search' => $search,
            '@replace' => $replace,
          ]);
        }
      }
      // Handle CDN method - update CDN URLs if provider is not jsdelivr.
      elseif ($method === 'cdn') {
        $cdn_provider = $config_accessor->getCdnProvider();

        // Skip if using default jsdelivr (libraries use it by default).
        if ($cdn_provider === 'jsdelivr') {
          continue;
        }

        // Get CDN configuration from plugin.
        $cdn_config = $plugin->getCdnConfig();
        if (!$cdn_config || !isset($cdn_config[$cdn_provider])) {
          continue;
        }

        $provider_urls = $cdn_config[$cdn_provider];

        // Process all CDN libraries for this plugin.
        foreach ($libraries as $library_name => &$library_def) {
          // Process only CDN libraries related to this plugin.
          if (strpos($library_name, $plugin_id . '_cdn') !== 0) {
            continue;
          }

          // Determine if this is the main library or an extension.
          $is_minified = strpos($library_name, '.min') !== FALSE;

          // Update all external JS entries.
          if (isset($library_def['js'])) {
            $new_js = [];
            foreach ($library_def['js'] as $old_url => $options) {
              if (isset($options['type']) && $options['type'] === 'external') {
                // Find appropriate replacement URL.
                $new_url = $provider_urls['js'] ?? $old_url;
                if ($is_minified && isset($provider_urls['js_min'])) {
                  $new_url = $provider_urls['js_min'];
                }
                $new_js[$new_url] = $options;
              }
              else {
                $new_js[$old_url] = $options;
              }
            }
            $library_def['js'] = $new_js;
          }

          // Update all external CSS entries.
          if (isset($library_def['css'])) {
            foreach ($library_def['css'] as $category => &$css_files) {
              $new_css = [];
              foreach ($css_files as $old_url => $options) {
                if (isset($options['type']) && $options['type'] === 'external') {
                  // Find appropriate replacement URL.
                  $new_url = $provider_urls['css'] ?? $old_url;
                  if ($is_minified && isset($provider_urls['css_min'])) {
                    $new_url = $provider_urls['css_min'];
                  }
                  $new_css[$new_url] = $options;
                }
                else {
                  $new_css[$old_url] = $options;
                }
              }
              $library_def['css'][$category] = $new_css;
            }
          }

          // Handle Tick extensions specially.
          if ($plugin_id === 'tick' && strpos($library_name, 'tick_') === 0) {
            // Extract extension name (e.g., tick_boom_cdn -> boom).
            preg_match('/tick_([^_]+)(?:_cdn)?/', $library_name, $ext_matches);
            $ext_name = $ext_matches[1] ?? NULL;

            if ($ext_name && method_exists($plugin, 'getAvailableExtensions')) {
              $extensions = $plugin->getAvailableExtensions();

              // Map library extension names to extension IDs.
              $ext_id_map = [
                'boom' => 'view_boom',
                'dots' => 'view_dots',
                'line' => 'view_line',
                'swap' => 'view_swap',
                'font' => [
                  'highres' => 'font_highres',
                  'lowres' => 'font_lowres',
                ],
              ];

              // Determine the actual extension ID.
              $ext_id = NULL;
              if (isset($ext_id_map[$ext_name])) {
                if (is_array($ext_id_map[$ext_name])) {
                  // For font extensions, check which one.
                  foreach ($ext_id_map[$ext_name] as $key => $id) {
                    if (strpos($library_name, $key) !== FALSE) {
                      $ext_id = $id;
                      break;
                    }
                  }
                }
                else {
                  $ext_id = $ext_id_map[$ext_name];
                }
              }

              // Get extension CDN URLs.
              if ($ext_id && isset($extensions[$ext_id]['cdn'][$cdn_provider])) {
                $ext_cdn = $extensions[$ext_id]['cdn'][$cdn_provider];

                // Update extension JS URL.
                if (isset($library_def['js'])) {
                  $new_js = [];
                  foreach ($library_def['js'] as $old_url => $options) {
                    if (isset($options['type']) && $options['type'] === 'external') {
                      $new_url = is_array($ext_cdn) && isset($ext_cdn['js']) ? $ext_cdn['js'] : (is_string($ext_cdn) ? $ext_cdn : $old_url);
                      $new_js[$new_url] = $options;
                    }
                    else {
                      $new_js[$old_url] = $options;
                    }
                  }
                  $library_def['js'] = $new_js;
                }

                // Update extension CSS URL if exists.
                if (isset($library_def['css']['theme']) && is_array($ext_cdn) && isset($ext_cdn['css'])) {
                  $new_css = [];
                  foreach ($library_def['css']['theme'] as $old_url => $options) {
                    if (isset($options['type']) && $options['type'] === 'external') {
                      $new_css[$ext_cdn['css']] = $options;
                    }
                    else {
                      $new_css[$old_url] = $options;
                    }
                  }
                  $library_def['css']['theme'] = $new_css;
                }
              }
            }
          }
        }

        // Log the CDN provider change if debug mode is enabled.
        if ($debug_mode) {
          \Drupal::logger('countdown')->debug('Updated CDN URLs for @plugin to use @provider', [
            '@plugin' => $plugin_id,
            '@provider' => $cdn_provider,
          ]);
        }
      }
    }
  }
  catch (\Exception $e) {
    \Drupal::logger('countdown')->error('Error in library_info_alter: @message', [
      '@message' => $e->getMessage(),
    ]);
  }
}

/**
 * Implements hook_page_attachments().
 */
function countdown_page_attachments(array &$attachments) {
  // Don't add the JavaScript and CSS during installation.
  if (InstallerKernel::installationAttempted()) {
    return;
  }

  // Don't add the JavaScript and CSS on specified paths or themes.
  if (!_countdown_check_theme() || !_countdown_check_path()) {
    return;
  }

  try {
    // Load configuration via accessor.
    $config_accessor = new ConfigAccessor(\Drupal::configFactory());

    $library_manager = \Drupal::service('countdown.library_manager');
    $plugin_manager = \Drupal::service('plugin.manager.countdown_library');

    $active_library = $library_manager->getActiveLibrary();
    if (!$config_accessor->isAutoLoadEnabled() || !$active_library || $active_library === 'none') {
      return;
    }

    $plugin = $plugin_manager->getPlugin($active_library);
    if (!$plugin) {
      \Drupal::logger('countdown')->warning('Active library @library not found', [
        '@library' => $active_library,
      ]);
      return;
    }

    // Get attachment configuration from plugin.
    $attachment_config = [
      'method' => $config_accessor->getLoadingMethod(),
      'variant' => $config_accessor->getBuildVariant(),
      'cdn_provider' => $config_accessor->getCdnProvider(),
      'rtl' => $config_accessor->isRtlEnabled(),
      'debug' => $config_accessor->isDebugMode(),
    ];

    // Let plugin build its attachments.
    $plugin_attachments = $plugin->buildAttachments($attachment_config);

    if (!empty($plugin_attachments)) {
      $attachments = array_merge_recursive($attachments, $plugin_attachments);

      if ($attachment_config['debug']) {
        \Drupal::logger('countdown')->debug('Attached libraries: @libraries', [
          '@libraries' => implode(', ', $plugin_attachments['#attached']['library'] ?? []),
        ]);
      }
    }
  }
  catch (\Exception $e) {
    \Drupal::logger('countdown')->error('Failed to attach library: @message', [
      '@message' => $e->getMessage(),
    ]);
  }
}

/**
 * Implements hook_cache_flush().
 *
 * Clears library discovery cache when Drupal caches are cleared.
 * This ensures fresh detection of newly installed/updated libraries.
 */
function countdown_cache_flush() {
  try {
    \Drupal::service('countdown.library_discovery')->clearCache();

    $config_accessor = new ConfigAccessor(\Drupal::configFactory());
    if ($config_accessor->isDebugMode()) {
      \Drupal::logger('countdown')->debug('Library discovery cache cleared.');
    }
  }
  catch (\Exception $e) {
    // Service might not be available during certain operations.
  }
}

/**
 * Prepare variables for countdown templates.
 */
function template_preprocess_countdown(&$variables) {
  $attributes = &$variables['attributes'];

  // Add library class.
  if (!empty($variables['library'])) {
    $attributes['class'][] = 'countdown-' . str_replace('_', '-', $variables['library']);
  }

  // Add data attributes for JavaScript initialization.
  foreach (['target_date' => 'target', 'format' => 'format'] as $var => $data) {
    if (!empty($variables[$var])) {
      $attributes['data-countdown-' . $data] = $variables[$var];
    }
  }

  // Add event data attributes if present.
  if (!empty($variables['countdown_event_name'])) {
    $attributes['data-countdown-event-name'] = $variables['countdown_event_name'];
  }

  if (!empty($variables['countdown_url'])) {
    $attributes['data-countdown-event-url'] = $variables['countdown_url'];

    // Process URL for safe output in template.
    try {
      // Try to create a URL object from the provided URL.
      if (strpos($variables['countdown_url'], 'http://') === 0 || strpos($variables['countdown_url'], 'https://') === 0) {
        // External URL.
        $url = Url::fromUri($variables['countdown_url']);
      }
      else {
        // Internal URL or path.
        $url = Url::fromUserInput($variables['countdown_url']);
      }
      $variables['countdown_url'] = $url->toString();
    }
    catch (\Exception $e) {
      // If URL parsing fails, sanitize it as a plain string.
      $variables['countdown_url'] = check_url($variables['countdown_url']);
    }
  }

  // Add settings as JSON.
  if (!empty($variables['settings'])) {
    $attributes['data-countdown-settings'] = json_encode($variables['settings']);
  }

  // Add the main countdown class.
  $attributes['class'][] = 'countdown';
}

/* ========================================================================
 * Internal helper functions
 * ======================================================================== */

/**
 * Generate help page content.
 */
function _countdown_help_page(): string {
  $output = '<h3>' . t('About') . '</h3>';
  $output .= '<p>' . t('The Countdown module provides flexible countdown timer functionality with support for multiple JavaScript libraries.') . '</p>';

  $output .= '<h3>' . t('Features') . '</h3>';
  $output .= '<ul>';
  $features = [
    t('Multiple countdown library support (FlipClock, FlipDown, etc.).'),
    t('Countdown blocks for easy placement.'),
    t('Field integration for content types.'),
    t('Flexible configuration per timer.'),
    t('Production/development build variants.'),
    t('Multi-site support.'),
  ];
  foreach ($features as $feature) {
    $output .= '<li>' . $feature . '</li>';
  }
  $output .= '</ul>';

  $output .= '<h3>' . t('Configuration') . '</h3>';
  $output .= '<p>' . t('Configure the module at <a href=":config">Countdown settings</a>.', [':config' => Url::fromRoute('countdown.settings')->toString()]) . '</p>';

  $output .= '<h3>' . t('Library Installation') . '</h3>';
  $output .= '<p>' . t('Place countdown libraries in:') . '</p>';
  $output .= '<ul>';
  $output .= '<li><code>/libraries/[library_name]</code> ' . t('(Global)') . '</li>';
  $output .= '<li><code>/sites/[site_name]/libraries/[library_name]</code> ' . t('(Site-specific)') . '</li>';
  $output .= '</ul>';

  return $output;
}

/**
 * Check if countdown should be active for the current URL.
 *
 * @param \Symfony\Component\HttpFoundation\Request|null $request
 *   The request to use if provided, otherwise \Drupal::request() will be used.
 * @param \Symfony\Component\HttpFoundation\RequestStack|null $request_stack
 *   The request stack.
 *
 * @return bool
 *   TRUE if countdown library should be active for the current page.
 */
function _countdown_check_path($request = NULL, $request_stack = NULL): bool {
  // Use the provided request or get the current request.
  if (!isset($request)) {
    $request = \Drupal::request();
  }

  // Initialize match status as FALSE.
  $page_match = FALSE;

  // Check for the ?countdown=no parameter in the URL to deactivate library.
  $query = $request->query;
  if ($query->get(CountdownConstants::QUERY_PARAM_DISABLE) !== NULL && $query->get(CountdownConstants::QUERY_PARAM_DISABLE) == 'no') {
    return $page_match;
  }

  // Load configuration via accessor.
  $config_accessor = new ConfigAccessor(\Drupal::configFactory());
  $page_visibility = $config_accessor->getPageVisibility();

  // Get the list of pages where countdown should be active.
  $pages = mb_strtolower(_countdown_array_to_string($page_visibility['pages']));

  // If no specific pages are configured, Countdown is active on all pages.
  if (!$pages) {
    return TRUE;
  }

  // Use the provided request stack or get the current request stack.
  if (!isset($request_stack)) {
    $request_stack = \Drupal::requestStack();
  }
  $current_request = $request_stack->getCurrentRequest();

  // Get the current path.
  $path = \Drupal::service('path.current')->getPath($current_request);
  // Do not trim the trailing slash if it is the complete path.
  $path = $path === '/' ? $path : rtrim($path, '/');

  // Get the current language code.
  $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
  // Get the path alias and convert to lowercase.
  $path_alias = mb_strtolower(\Drupal::service('path_alias.manager')->getAliasByPath($path, $langcode));

  // Check if the path alias matches the configured pages.
  $page_match = \Drupal::service('path.matcher')->matchPath($path_alias, $pages);
  // If the path alias is different from the internal path,
  // check the internal path as well.
  if ($path_alias != $path) {
    $page_match = $page_match || \Drupal::service('path.matcher')->matchPath($path, $pages);
  }

  // Negate the match if configured to load on all pages except those listed.
  $page_match = $page_visibility['negate'] == 0 ? !$page_match : $page_match;

  return $page_match;
}

/**
 * Verify if the current theme is selected in the module settings.
 *
 * @return bool
 *   TRUE if the current theme is selected, otherwise FALSE.
 */
function _countdown_check_theme(): bool {
  // Load configuration via accessor.
  $config_accessor = new ConfigAccessor(\Drupal::configFactory());
  $theme_visibility = $config_accessor->getThemeVisibility();

  // Get the name of the active theme.
  $active_theme = \Drupal::theme()->getActiveTheme()->getName();

  // Retrieve the list of valid themes from the configuration.
  $valid_themes = $theme_visibility['themes'];

  // If no themes are configured, countdown should be active.
  if (empty($valid_themes) || count($valid_themes) === 0) {
    return TRUE;
  }

  // Get the visibility setting from the configuration.
  $visibility = $theme_visibility['negate'];

  // Check if the active theme is in the list of valid themes.
  $theme_match = in_array($active_theme, $valid_themes);

  // Determine the final match based on the visibility setting.
  return !($visibility xor $theme_match);
}

/**
 * Converts a text with lines (\n) into an array of non-empty trimmed lines.
 *
 * @param string $text
 *   The text to be converted into an array.
 *
 * @return array|null
 *   An array with non-empty trimmed lines or NULL if input is not a string.
 */
function _countdown_string_to_array(string $text): ?array {
  // Normalize line endings to Unix style.
  $text = str_replace("\r\n", "\n", $text);

  // Split the text into lines and filter out empty lines.
  $lines = array_filter(array_map('trim', explode("\n", $text)));

  return $lines;
}

/**
 * Converts an array of lines into a text with lines (\r\n).
 *
 * @param array $array
 *   The array to be converted into a text string.
 *
 * @return string|null
 *   A text string with lines separated by \r\n or NULL
 *   if input is not an array.
 */
function _countdown_array_to_string(array $array): ?string {
  // Convert the array to a string with \r\n line endings.
  return implode("\r\n", array_map('trim', $array));
}

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

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