editoria11y-1.0.0-alpha8/src/Api.php

src/Api.php
<?php

namespace Drupal\editoria11y;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\editoria11y\Exception\Editoria11yApiException;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Handles reporting and dismissals.
 *
 * @phpstan-consistent-constructor
 */
class Api {

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

  /**
   * The current database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected Connection $connection;

  /**
   * The manager property.
   */
  protected EntityTypeManager $manager;

  /**
   * The configuration factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * The path validator service.
   *
   * @var \Drupal\Core\Path\PathValidatorInterface
   */
  protected PathValidatorInterface $pathValidator;

  /**
   * Constructs an Api object.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The current user.
   * @param \Drupal\Core\Database\Connection $connection
   *   The database connection.
   * @param \Drupal\Core\Entity\EntityTypeManager $manager
   *   The entity type manager.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory.
   * @param \Drupal\Core\Path\PathValidatorInterface $path_validator
   *   The path validator service.
   */
  public function __construct(AccountInterface $account, Connection $connection, EntityTypeManager $manager, ConfigFactoryInterface $config_factory, PathValidatorInterface $path_validator) {
    $this->account = $account;
    $this->connection = $connection;
    $this->manager = $manager;
    $this->configFactory = $config_factory;
    $this->pathValidator = $path_validator;
  }

  /**
   * Creates an instance of the Api class.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The service container.
   *
   * @return static
   *   A new instance of the Api class.
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('current_user'),
      $container->get('database'),
      $container->get('entity_type.manager'),
      $container->get('config.factory'),
      $container->get('path.validator')
    );
  }

  /**
   * Get the pid ID.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *    Invalid data.
   */
  public function getPage($path, $entity_type, $entity_id, $language, $findStale = FALSE): ?StatementInterface {
    // Confirm result_names is array?
    $this->validateNumber($entity_id);
    $this->validatePath($path);

    // Get back the page ID.
    $query = $this->connection->select('editoria11y_pages');
    $query->fields('editoria11y_pages', ['pid']);
    if ($findStale) {
      $query->condition('page_path', $path, '!=');
    }
    else {
      $query->condition('page_path', $path);
    }
    $query->condition('entity_id', $entity_id);
    $query->condition('entity_type', $entity_type);
    $query->condition('page_language', $language);
    return $query->execute();
  }

  /**
   * Get the existing results and pid ID.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *    Invalid data.
   */
  public function getPageData($path, $type, $id, $language): ?StatementInterface {
    $this->validateNumber($id);
    $this->validatePath($path);

    // Get back the page ID.
    $query = $this->connection->select('editoria11y_pages', 'p');
    $query->fields('p', ['pid']);
    $query->condition('p.page_path', $path);
    $query->condition('p.entity_id', $id);
    $query->condition('p.entity_type', $type);
    $query->condition('p.page_language', $language);
    $query->leftJoin('editoria11y_results', 'r', 'p.pid = r.pid');
    $query->leftJoin('editoria11y_dismissals', 'd', 'p.pid = d.pid');
    $query->fields('r', ['result_key']);
    $query->fields('d', ['result_key']);
    $query->fields('d', ['element_id']);
    return $query->execute();
  }

  /**
   * Set and return an pid ID.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *    Invalid data.
   */
  public function updatePage($results, $pid, $now): void {
    // Confirm result_names is array?
    $this->validateNotNull($results["page_title"]);

    // @todo 3.0 can we multi-write?
    $update = $this->connection->update("editoria11y_pages");
    // Track the type and count of issues detected on this page.
    // Update the "last seen" date of the page.
    $update->fields(
      [
        'page_title' => $results["page_title"],
        'page_result_count' => $results["page_count"],
        'entity_type' => $results["entity_type"],
        'route_name' => $results["route_name"],
        'updated' => $now,
      ]
    );
    $update->condition('pid', $pid);

    $update->execute();
  }

  /**
   * Set and return an pid ID.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *    Validation errors.
   * @throws \Exception
   *    Invalid data.
   */
  public function insertPage($results, $now): int|string {
    // Confirm result_names is array?
    $this->validateNotNull($results["page_title"]);
    $this->validateNumber($results["page_count"]);
    $this->validateNumber($results["entity_id"]);
    $this->validatePath($results["page_path"]);

    // @todo 3.0 can we multi-write?
    $insert = $this->connection->insert("editoria11y_pages");
    // Track the type and count of issues detected on this page.
    $insert->fields(
      [
        'page_title' => $results["page_title"],
        'page_path' => $results["page_path"],
        'entity_id' => $results["entity_id"],
        'page_language' => $results["language"],
        'page_result_count' => $results["page_count"],
        'entity_type' => $results["entity_type"],
        'route_name' => $results["route_name"],
        'updated' => $now,
      ]
    );

    // Get back the page ID.
    return $insert->execute();
  }

  /**
   * Function to test the results.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *    Invalid data.
   */
  public function testResults($results): void {
    $now = time();

    // Get or create EID, and return any existing results or dismissals.
    $page_data = $this->getPageData($results["page_path"], $results["entity_type"], $results["entity_id"], $results["language"]);
    // $page_data = $this->setPage($results, $now);
    // Stash existing information to reduce DB write-backs later.
    $old_results = [];
    $new_results = [];
    $old_dismissals = FALSE;
    $pid = FALSE;
    foreach ($page_data as $result) {
      if (!$pid) {
        $pid = $result->pid;
      }
      if (!empty($result->result_key)) {
        $old_results[] = $result->result_key;
      }
      if (!empty($result->d_result_key)) {
        $old_dismissals = TRUE;
      }
    }
    if (!$pid) {
      // There was no page at this address. Make a new one.
      if (count($results["results"]) > 0 || count($results["oks"]) > 0) {
        $pid = $this->insertPage($results, $now);
      }
    }
    else {
      $this->updatePage($results, $pid, $now);
    }

    // Remove old results at this route.
    // Should we move this to the dashboard?
    // @todo 3.0 not yet tested
    $incorrectData = $this->getPage($results["page_path"], $results["entity_type"], $results["entity_id"], $results["language"], TRUE);
    $incorrectPage = $incorrectData->fetchField();
    if ($incorrectPage) {
      $this->purgePage($incorrectPage, $results["page_path"]);
    }
    if (!$pid) {
      // Nothing to report, nothing to remove.
      return;
    }

    // Update last seen.
    if ($results["page_count"] > 0) {
      foreach ($results["results"] as $key => $value) {
        $this->validateNumber($value['count']);
        // @todo 3.0: we need to handle page parameters that change content
        $new_results[] = $key;
        $this->validateNotNull($key);
        $this->validateNotNull($value['result_name']);
        $updatePage = $this->connection->merge("editoria11y_results");
        $updatePage->insertFields(
          [
            'result_name_count' => $value['count'],
            'result_name' => $value['result_name'],
            'result_key' => $key,
            'created' => $now,
          ]
          );
        $updatePage->updateFields(
          [
            'result_name_count' => $value['count'],
            'result_name' => $value['result_name'],
          ]
          );
        $updatePage->keys(
          [
            'pid' => $pid,
            'result_key' => $key,
          ]
          );
        $updatePage->execute();
      }
    }
    elseif (!$old_dismissals) {
      // Drop page with no remaining records.
      $this->purgePage($pid, $results['page_path']);
    }

    // Remove results no longer in the result set.
    $stale_results = array_diff($old_results, $new_results);
    foreach ($stale_results as $result) {
      $this->connection->delete('editoria11y_results')
        ->condition('result_key', $result)
        ->condition('pid', $pid)
        ->execute();
    }

    // Update stale dates.
    // Marked-as-ok issues are not in the main results foreach.
    // Note: v2.1.0 added entity_id; old entries may be missing it.
    // Note: v2.2.10 changed entity type; old entries have a different format.
    if ($old_dismissals) {
      foreach ($results["oks"] as $ok) {
        if (!in_array($ok["resultKey"], $new_results)) {
          $new_results[] = $ok["resultKey"];
        }
      }
      if (count($new_results) > 0) {
        $this->connection->update('editoria11y_dismissals')
          ->fields(['stale_date' => NULL])
          ->condition('result_key', $new_results, 'IN')
          ->condition('pid', $pid)
          ->execute();
        // Set stale records if the alert disappeared.
        $this->connection->update('editoria11y_dismissals')
          ->fields(['stale_date' => $now])
          ->condition('result_key', $new_results, 'NOT IN')
          ->condition('pid', $pid)
          ->isNull('stale_date')
          ->execute();
      }
      else {
        // If there are no new results, mark all old dismissals as stale.
        $this->connection->update('editoria11y_dismissals')
          ->fields(['stale_date' => $now])
          ->condition('pid', $pid)
          ->isNull('stale_date')
          ->execute();
      }

    }
  }

  /**
   * The Purge page function.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *   Invalid data.
   */
  public function purgePage($page = FALSE, $path = FALSE): void {
    // Internal functions provide path, direct calls do not.
    if ($page) {
      $this->validateNotNull($page);
      $path = $this->connection->select("editoria11y_pages")
        ->fields('editoria11y_pages', ['pid'])
        ->condition('pid', $page)
        ->execute()->fetchField();
    }
    elseif ($path) {
      $this->validateNotNull($path);
      // Get back the page ID.
      $query = $this->connection->select('editoria11y_pages');
      $query->fields('editoria11y_pages', ['pid']);
      $query->condition('page_path', $path);
      $page = $query->execute()->fetchField();
    }
    if ($page && $path) {
      $this->connection->delete("editoria11y_dismissals")
        ->condition('pid', $page)
        ->execute();
      $this->connection->delete("editoria11y_results")
        ->condition('pid', $page)
        ->execute();
      $this->connection->delete("editoria11y_pages")
        ->condition('pid', $page)
        ->execute();
      // Clear cache for the referring page and dashboard.
      $invalidate = preg_replace('/[^a-zA-Z0-9]/', '', $path, -80);
      Cache::invalidateTags(
        ['editoria11y:dismissals_' . $invalidate]
      );
    }
  }

  /**
   * The purge dismissal function.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *   Invalid data.
   */
  public function purgeDismissal($data): void {
    $page_path = FALSE;
    if ($data['dismissal_id']) {
      $this->validateNumber($data['dismissal_id']);
      $this->validateNumber($data['pid']);
      $page_path = $this->connection->select('editoria11y_pages')
        ->fields('editoria11y_pages', ['page_path'])
        ->condition('pid', $data['pid'])
        ->execute()->fetchField();
      if (!empty($page_path)) {
        // This deletes ALL dismissals of this type on the page.
        $this->connection->delete("editoria11y_dismissals")
          ->condition('pid', $data['pid'])
          ->condition('id', $data["dismissal_id"])
          ->execute();
      }
    }
    elseif ($data['page_path'] && $data['uid']) {
      $this->validateNumber($data['pid']);
      $this->validateNotNull($data['entity_type']);
      $this->validateNotNull($data['entity_id']);
      $this->validateNotNull($data['language']);
      $page = $this->getPage($data['page_path'], $data['entity_type'], $data['entity_id'], $data['language'])->fetchField();
      if ($page) {
        $this->connection->delete("editoria11y_dismissals")
          ->condition('pid', $data['pid'])
          ->condition('element_id', $data['element_id'])
          ->condition('result_key', $data["result_key"])
          ->execute();
        $page_path = $data['page_path'];
      }
      // Clear cache for the referring page.
      if (!empty($page_path)) {
        // This deletes ALL dismissals of this type on the page.
        $invalidate = preg_replace('/[^a-zA-Z0-9]/', '', substr($data["page_path"], -80));
        Cache::invalidateTags(
          ['editoria11y:dismissals_' . $invalidate]
        );
      }
    }
  }

  /**
   * The dismiss function.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *   Invalid data.
   */
  public function dismiss($dismissal): void {
    $this->validatePath($dismissal["page_path"]);
    $now = time();
    $get_page = $this->getPageData($dismissal['page_path'], $dismissal['entity_type'], $dismissal['entity_id'], $dismissal["language"],);
    $pid = $get_page->fetchField();
    if (!$pid) {
      $dismissal["page_count"] = 0;
      // Theoretically a dynamic update could send a dismissal as a new record.
      $pid = $this->insertPage($dismissal, $now);
    }

    $this->validateNumber($pid);

    foreach ($dismissal['dismissals'] as $item) {
      $operation = $item['dismissal_status'];
      $this->validateDismissalType($operation);
      $this->validateDismissalPermission($operation);
      if ($operation == "reset") {
        if ($this->account->hasPermission('mark as ok in editoria11y')) {
          // Reset all for this result.
          $this->connection->delete("editoria11y_dismissals")
            ->condition('pid', $pid)
            ->condition('result_key', $item["result_key"])
            ->condition('element_id', $item['element_id'])
            ->condition('dismissal_status', "ok")
            ->execute();
        }
        if ($this->account->hasPermission('mark as hidden in editoria11y')) {
          $this->connection->delete("editoria11y_dismissals")
            ->condition('pid', $pid)
            ->condition('dismissal_status', "hide")
            ->condition('uid', $this->account->id())
            ->condition('element_id', $item['element_id'])
            ->condition('result_key', $item["result_key"])
            ->execute();
        }
      }
      else {
        $this->validateNotNull($item["result_name"]);
        $this->validateNotNull($item["result_key"]);

        $this->connection->merge("editoria11y_dismissals")
          ->insertFields(
            [
              'pid' => $pid,
              'uid' => $this->account->id(),
              'element_id' => $item["element_id"],
              'result_name' => $item["result_name"],
              'result_key' => $item["result_key"],
              'dismissal_status' => $operation,
              'created' => $now,
            ]
          )
          ->updateFields(
            [
              'uid' => $this->account->id(),
              'element_id' => $item["element_id"],
              'result_name' => $item["result_name"],
              'result_key' => $item["result_key"],
              'dismissal_status' => $operation,
            ]
          )
          ->keys(
            [
              'pid' => $pid,
              'result_key' => $item["result_key"],
              'uid' => $this->account->id(),
              'element_id' => $item["element_id"],
            ]
          )
          ->execute();
      }
    }
    // Clear cache for the referring page and dashboard.
    $invalidate = preg_replace('/[^a-zA-Z0-9]/', '', substr($dismissal["page_path"], -80));
    Cache::invalidateTags(
      ['editoria11y:dismissals_' . $invalidate]
    );
  }

  /**
   * Remove deleted pages.
   */
  public function purgeDeletedRecords(): void {
    // @todo 3.0;
    // Delete nodes with invalid NIDS.
    $query = $this->connection->select('editoria11y_pages');
    // Delete terms with invalid TIDS.
    // Delete users with invalid UIDS.
  }

  /**
   * Throw exception for missing values.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *   Invalid data.
   */
  private function validateNotNull($user_input): void {
    if (empty($user_input)) {
      throw new Editoria11yApiException("Missing value");
    }
  }

  /**
   * Throw exception if user does not have permission for this dismissal type.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *   Invalid role.
   */
  private function validateDismissalPermission($operation): void {
    $canHide = $this->account->hasPermission('mark as hidden in editoria11y');
    $canOk = $this->account->hasPermission('mark as ok in editoria11y');
    if (
      (!($canHide || $canOk) ||
        (!$canHide && $operation === 'hide') ||
        (!$canOk && $operation === 'ok')
      )
    ) {
      throw new Editoria11yApiException("Not allowed");
    }
  }

  /**
   * This function is used to validate the requested path.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *   Invalid data.
   */
  private function validatePath($user_input): void {
    $config = $this->configFactory->get('editoria11y.settings');
    $prefix = $config->get('redundant_prefix');
    if (!empty($prefix) && strlen($prefix) < strlen($user_input) && str_starts_with($user_input, $prefix)) {
      // Replace ignorable subfolders.
      $altPath = substr_replace($user_input, "", 0, strlen($prefix));
      if (
        !(
          $this->pathValidator->getUrlIfValid($altPath) ||
          $this->pathValidator->getUrlIfValid($user_input)
        )
      ) {
        throw new Editoria11yApiException('Invalid page path on API report: "' . $user_input . '". If site is installed in subfolder, check Editoria11y config item "Syncing results to reports
--> Remove redundant base url from URLs"');
      }
    }
    else {
      if (!$this->pathValidator->getUrlIfValid($user_input)) {
        throw new Editoria11yApiException('Invalid page path on API report: "' . $user_input . '". If site is installed in subfolder, check Editoria11y config item "Syncing results to reports
--> Remove redundant base url from URLs"');
      }
    }
  }

  /**
   * Validate dismissal status function.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *   Invalid data.
   */
  private function validateDismissalType($user_input): void {
    if (!($user_input === 'ok' || $user_input === 'hide' || $user_input === 'reset')) {
      throw new Editoria11yApiException("Invalid dismissal operation: $user_input");
    }
  }

  /**
   * Validate number function.
   *
   * @throws \Drupal\editoria11y\Exception\Editoria11yApiException
   *   Invalid data.
   */
  private function validateNumber($user_input): void {
    if (!(is_numeric($user_input))) {
      throw new Editoria11yApiException("Nan: $user_input");
    }
  }

}

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

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