g2-8.x-1.x-dev/g2.module

g2.module
<?php

/**
 * @file
 * This defines a node-based glossary module, vs the term-based glossary.
 *
 * @todo Test wipes, rss
 *
 * @todo For D8, in decreasing priorities
 *  - implement SettingsForm::validateForm() using Requirements
 *  - make g2_requirements() less verbose, at least on success.
 *  - find a way to add the title to the node.add route for ease of creation.
 * @copyright 2005-2024 Frédéric G. Marand, for Ouest Systèmes Informatiques.
 *
 * @link http://wiki.audean.com/g2/choosing @endlink
 */

declare(strict_types=1);

use Drupal\block\BlockInterface;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\GeneratedLink;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\g2\G2;
use Drupal\g2\Top;
use Drupal\node\NodeInterface;
use Drupal\views\ViewExecutable;

/**
 * XML-RPC callback : returns alphabar data.
 *
 * @return string[]
 *   The alphabar data, to be serialized in XML.
 */
function _g2_alphabar(): array {
  /** @var \Drupal\g2\Alphabar $alphabar */
  $alphabar = Drupal::service(G2::SVC_ALPHABAR);
  $ret = array_map(fn(GeneratedLink $link) => "$link", $alphabar->getLinks());
  return $ret;
}

/**
 * XML-RPC callback : returns a list of the latest n nodes.
 *
 * "Latest" nodes are identified by time of latest update.
 *
 * @param int $count
 *   The maximum number of entries to return.
 *
 * @return string[]
 *   Note that the results are NOT filtered, and must be filtered when used.
 *
 * @throws \Drupal\Core\Entity\EntityMalformedException
 */
function _g2_latest(int $count = 0): array {
  $config = Drupal::config(G2::CONFIG_NAME);
  /** @var int $service_max */
  $service_max = $config->get(G2::VARLATESTMAXCOUNT);
  /** @var float $api_throttle */
  $api_throttle = $config->get(G2::VARAPITHROTTLE);
  $actual_max = (int) ceil($api_throttle * $service_max);

  // Limit extraction.
  if (empty($count) || ($count > $actual_max)) {
    $count = $actual_max;
  }

  /** @var \Drupal\g2\Latest $latest */
  $latest = Drupal::service(G2::SVC_LATEST);
  $links = $latest->getLinks($count);
  $result = [];

  /** @var \Drupal\Core\Link $link */
  foreach ($links as $link) {
    $result[] = "{$link->toString()}";
  }
  return $result;
}

/**
 * XML-RPC callback: return a random G2 entry.
 *
 * @return array<string,mixed[]>
 *   The node as an array, hiding the "internal use" g2_complement and g2_origin
 *   fields.
 */
function _g2_random(): array {
  /** @var \Drupal\g2\Random $random */
  $random = \Drupal::service(G2::SVC_RANDOM);
  $node = $random->get();
  $res = _g2_xmlrpc($node);
  return $res;
}

/**
 * Extract statistics from the G2 glossary.
 *
 * @param int $tid
 *   Taxonomy term id.
 * @param string $initial
 *   Initial segment.
 *
 * @return array<string,int>
 *   - g2 entries having chosen taxonomy term
 *   - g2 entries starting with chosen initial segment
 */
function _g2_stats(int $tid = 0, string $initial = ''): array {
  $db = Drupal::database();

  $qUnpublished = $db->select('node', 'n');
  $unPubAlias = $qUnpublished->innerJoin('node_field_data', 'nfd', 'nfd.nid = n.nid');
  $qUnpublished
    ->condition('n.type', G2::BUNDLE)
    ->condition("{$unPubAlias}.status", (string) NodeInterface::NOT_PUBLISHED)
    ->addTag('node_access');

  $qPub = $db->select('node', 'n');
  $pubAlias = $qPub->innerJoin('node_field_data', 'nfd', 'nfd.nid = n.nid');
  $qPub
    ->condition('n.type', G2::BUNDLE)
    ->condition("{$pubAlias}.status", (string) NodeInterface::PUBLISHED)
    ->addTag('node_access');

  if (!empty($tid)) {
    $qUnpublished->innerJoin('taxonomy_index', 'ti', 'n.nid = ti.nid');
    $qPub->innerJoin('taxonomy_index', 'ti', 'n.nid = ti.nid');
    $qUnpublished = $qUnpublished->condition('ti.tid', (string) $tid);
    $qPub = $qPub->condition('ti.tid', (string) $tid);
  }

  if (!empty($initial)) {
    $qUnpublished = $qUnpublished->condition("{$unPubAlias}.title", "$initial%", 'LIKE');
    $qPub = $qPub->condition("{$pubAlias}.title", "$initial%", 'LIKE');
  }

  $unpublished = $qUnpublished->countQuery()->execute();
  $pub = $qPub->countQuery()->execute();

  // Avoid empty returns.
  $unpublishedCount = $unpublished?->fetchField();
  assert(is_numeric($unpublishedCount));
  $pubCount = $pub?->fetchField();
  assert(is_numeric($pubCount));
  $ret = [
    'unpublished' => (int) $unpublishedCount,
    'published' => (int) $pubCount,
  ];

  return $ret;
}

/**
 * Returns a list of the top n nodes as counted by statistics.module.
 *
 * - Unpublished nodes are not listed.
 * - Stickiness is ignored for ordering, but returned in the results for
 *   client-side ordering if needed.
 *
 * @param int|null $count
 *   Number or entries to return.
 * @param bool|null $daily_top
 *   Order by daily views if TRUE, otherwise by total views (default).
 *
 * @return string[]
 *   Statistics will be empty without statistics module.
 *   Note that the title of the nodes is NOT filtered.
 */
function _g2_top($count = NULL, $daily_top = FALSE) {
  $config = Drupal::config(G2::CONFIG_NAME);
  $service_max = $config->get(G2::VARLATESTMAXCOUNT);
  $api_throttle = $config->get(G2::VARAPITHROTTLE);
  $actual_max = $api_throttle * $service_max;

  // Limit extraction.
  if (empty($count) || ($count > $actual_max)) {
    $count = $actual_max;
  }
  $count = (int) $count;

  /** @var \Drupal\g2\Top $top */
  $top = Drupal::service(G2::SVC_TOP);
  $statistic = $daily_top ? Top::STATISTICS_DAY : Top::STATISTICS_TOTAL;
  $links = $top->getLinks($count, $statistic);
  $result = [];

  /** @var \Drupal\Core\GeneratedLink $link */
  foreach ($links as $link) {
    $result[] = $link->__toString();
  }
  return $result;
}

/**
 * Returns a structure for the WOTD.
 *
 * @param int $bodySize
 *   The maximum length for the body. Entails truncation.
 *
 * @return array<string,mixed>
 *   An array serialization of the WOTD, not rendered.
 *
 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 */
function _g2_wotd($bodySize = 0) {
  /** @var \Drupal\g2\WOTD $wotd */
  $wotd = \Drupal::service(G2::SVC_WOTD);
  $node = $wotd->get();
  $res = _g2_xmlrpc($node);
  return $res;
}

/**
 * Represent a node as an array for the XML-RPC API.
 *
 * This implies stripping its internal use fields.
 *
 * @param \Drupal\node\NodeInterface|null $node
 *   The node to display.
 *
 * @return array<string,mixed[]>
 *   The simplified node.
 */
function _g2_xmlrpc(?NodeInterface $node): array {
  if (empty($node) || !$node->isPublished()) {
    return [];
  }
  /** @var array<string,mixed[]> $res */
  $res = $node->toArray();
  unset($res['g2_complement'], $res['g2_origin']);
  return $res;
}

/**
 * Implements hook_cron().
 *
 * In G2's case, change the WOTD once a day if this feature is enabled,
 * which is the default case.
 */
function g2_cron(): void {
  /** @var \Drupal\g2\WOTD $wotd */
  $wotd = \Drupal::service(G2::SVC_WOTD);
  $wotd->cron();
}

/**
 * Implements hook_entity_operation().
 *
 * @phpstan-return array<string,mixed>
 */
function g2_entity_operation(EntityInterface $entity): array {
  if ($entity->getEntityTypeId() !== G2::TYPE || $entity->bundle() !== G2::BUNDLE) {
    return [];
  }
  $operations = [];
  $operations['g2-referers'] = [
    'title' => t('G2 Referers'),
    'url' => Url::fromRoute(G2::ROUTE_REFERERS, ['node' => $entity->id()]),
    // Delete & Devel: 100.
    'weight' => 75,
  ];

  return $operations;

}

/**
 * Implements hook_help().
 *
 * @phpstan-return array<string,string>|string|null|\Drupal\Core\StringTranslation\TranslatableMarkup
 */
function g2_help(string $route_name, RouteMatchInterface $route_match): mixed {
  $result = '';
  switch ($route_name) {
    case 'help.page.g2':
      $result = t(
        '<p>G2 defines a glossary service for Drupal sites.
       To compare it with the Glossary and Lexicon modules:</p>
       <ul>
         <li>G2 content is node-based, not term-based, allowing node access control</li>
         <li>G2 leverages existing code from glossary for input filtering and node marking</li>
         <li>G2 RAM use does not significantly increase with larger entry counts, which makes is more suitable for larger glossaries</li>
         <li>G2 requests much less from the database than the default glossary</li>
         <li>G2 uses a "G2 Context" taxonomy vocabulary by default, but does not require it.</li>
         <li>G2 defines optional blocks</li>
         <li>G2 provides a client and server XML-RPC API</li>
         <li>G2 does not provide term feeds</li>
         </ul>'
      );
      break;

    case 'entity.block.edit_form':
      // G2 block plugins contain a "help" annotation.
      $block = $route_match
        ->getParameter('block');
      assert($block instanceof BlockInterface);
      /** @var array<string,mixed> $definition */
      $definition = $block
        ->getPlugin()
        ->getPluginDefinition();
      if (G2::NAME !== ($definition['provider'] ?? '')) {
        return NULL;
      }
      /** @var string|\Drupal\Core\StringTranslation\TranslatableMarkup $result */
      $result = $definition['help'] ?? '';
      break;

    case 'entity.entity_view_display.node.view_mode':
      if ($route_match->getParameter('entity_type_id') != G2::TYPE) {
        break;
      }
      if ($route_match->getParameter('bundle') !== G2::BUNDLE) {
        break;
      }
      $name = $route_match->getParameter('view_mode_name');
      assert(is_scalar($name));
      $name = (string) $name;
      $args = ['%vm' => EntityViewMode::load(G2::TYPE . ".{$name}")?->label()];
      switch ($name) {
        case G2::VM_BLOCK:
          $result = t('The %vm display is used by the G2 Random and WOTD blocks.', $args);
          break;

        case G2::VM_ENTRY_LIST:
          $result = t('The %vm display is used by the G2 "terms by initial" page and the homonyms page in "plain node list" mode.', $args);
          break;

        case G2::VM_HOMONYMS_PAGE:
          $result = t('The %vm display is used by the G2 homonyms disambiguation page when it is configured to use a node (which is deprecated) instead of a route.', $args);
          break;

        case G2::VM_TOOLTIPS:
          $result = t('The %vm display is used by the G2 definition filter when tooltips are configured to use teasers');
          break;
      }
      if (!empty($result)) {
        $result = ['#markup' => "<p>{$result}</p>\n"];
      }
  }

  return $result;
}

/**
 * Implements hook_ENTITY_TYPE_delete() for nodes.
 */
function g2_node_delete(NodeInterface $node): void {
  /** @var \Drupal\g2\Matcher $matcher */
  $matcher = Drupal::service(G2::SVC_MATCHER);
  $matcher->rebuild();
}

/**
 * Implements hook_ENTITY_TYPE_insert() for nodes.
 */
function g2_node_insert(NodeInterface $node): void {
  /** @var \Drupal\g2\Matcher $matcher */
  $matcher = Drupal::service(G2::SVC_MATCHER);
  $matcher->rebuild();
}

/**
 * Implements hook_ENTITY_TYPE_update() for nodes.
 */
function g2_node_update(NodeInterface $node): void {
  /** @var \Drupal\g2\Matcher $matcher */
  $matcher = Drupal::service(G2::SVC_MATCHER);
  $matcher->rebuild();
}

/**
 * Implements hook_ENTITY_TYPE_view().
 *
 * @phpstan-param array<string,mixed> $build
 */
function g2_node_view(array &$build, NodeInterface $node): void {
  if ($node->bundle() != G2::BUNDLE) {
    return;
  }
  if (!Drupal::config(G2::CONFIG_NAME)->get(G2::VARLOGREFERERS)) {
    return;
  }

  /** @var \Drupal\g2\RefererTracker $tracker */
  $tracker = Drupal::service(G2::SVC_TRACKER);
  $tracker->upsertReferer($node);
}

/**
 * Implements hook_preprocess_feed_icon().
 *
 * Remove after #3371937 is fixed.
 *
 * @see \Drupal\g2\Plugin\Block\WotdBlock::build()
 * @see https://www.drupal.org/project/drupal/issues/3371937
 * @see https://www.drupal.org/project/drupal/issues/3374891
 *
 * @phpstan-param array<string,mixed> $variables
 */
function g2_preprocess_feed_icon(&$variables): void {
  /** @var \Drupal\Core\Block\BlockManagerInterface $pm */
  $pm = Drupal::service(G2::SVC_PM_BLOCK);
  /** @var \Drupal\g2\Plugin\Block\WotdBlock $wotdBlock */
  $wotdBlock = $pm->createInstance(G2::DELTA_WOTD, []);
  $wotdBlock->preprocessFeedIcon($variables);
}

/**
 * Implements hook_preprocess_html().
 *
 * Append the glossary name to the page title on entry pages and G2 own pages,
 * possibly including the default site name.
 *
 * @phpstan-param array<string,mixed> $variables
 */
function g2_preprocess_html(&$variables): void {
  /** @var \Drupal\g2\RouteFilter $rf */
  $rf = \Drupal::service(G2::SVC_ROUTE_FILTER);
  if (!$rf->isG2Route()) {
    return;
  }
  $override = \Drupal::config(G2::CONFIG_NAME)
    ->get(G2::VARPAGETITLE);
  if (empty($override)) {
    return;
  }
  assert(is_string($override));
  $ht = $variables['head_title'] ?? [];
  assert(is_array($ht));
  $name = strtr($override, ['@title' => $ht['name']]);
  $ht['name'] = $name;
  $variables['head_title'] = $ht;
}

/**
 * Implements hook_preprocess_views_view_rss().
 *
 * @phpstan-param array<string,mixed> $variables
 */
function g2_preprocess_views_view_rss(&$variables): void {
  /** @var \Drupal\g2\WOTD $wotd */
  $wotd = Drupal::service(G2::SVC_WOTD);
  $wotd->preprocessViewsViewRss($variables);
}

/**
 * Implements hook_theme().
 *
 * @phpstan-return array<string,array{variables:array<string,mixed>}>
 */
function g2_theme(): array {
  $config = Drupal::config(G2::CONFIG_NAME);
  $ret = [
    // Checked for D8/9/10.
    'g2_alphabar' => [
      'variables' => [
        'alphabar' => [],
        'row_length' => $config->get(G2::VARALPHABARROWLENGTH),
      ],
    ],
    // Checked for D8/9/10.
    'g2_entries' => [
      'variables' => [
        'raw_entry' => '',
        'entries' => [],
        'message' => NULL,
        'offer' => NULL,
      ],
    ],
    // Not checked.
    'g2_initial' => [
      'variables' => [
        'initial' => NULL,
        'entries' => [],
      ],
    ],
    // Not checked.
    'g2_main' => [
      'variables' => [
        'alphabar' => $config->get(G2::VARALPHABARCONTENTS),
        'text' => '',
      ],
    ],
  ];

  return $ret;
}

/**
 * Implements hook_views_pre_render().
 */
function g2_views_pre_render(ViewExecutable $view): void {
  if ($view->id() === G2::VIEW_WOTD && $view->current_display === G2::VIEW_WOTD_DISPLAY) {
    /** @var \Drupal\g2\WOTD $wotd */
    $wotd = Drupal::service(G2::SVC_WOTD);
    $wotd->viewsPreRender($view);
  }
}

/**
 * Implements hook_xmlrpc().
 *
 * Note that functions returning node portions return them unfiltered.
 * It is the caller's responsibility to apply filtering depending on
 * its actual use of the data.
 *
 * @phpstan-return array<string,callable>
 */
function g2_xmlrpc(): array {
  $mapping = [
    // D10/9 OK.
    'g2.alphabar' => '_g2_alphabar',
    'g2.api' => [G2::class, 'api'],
    'g2.latest' => '_g2_latest',
    'g2.random' => '_g2_random',
    'g2.stats' => '_g2_stats',
    'g2.top' => '_g2_top',
    'g2.wotd' => '_g2_wotd',
  ];

  $enabled = Drupal::config(G2::CONFIG_NAME)->get(G2::VARAPIENABLED);
  if (!$enabled) {
    $mapping = [];
  }

  return $mapping;
}

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

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