Commit 673529bd authored by Fran Garcia-Linares's avatar Fran Garcia-Linares Committed by Tim Plunkett
Browse files

Issue #3281218 by fjgarlin: Restrict the plugin's ability to control which filters are available

parent 7fd0de00
Loading
Loading
Loading
Loading
+35 −17
Original line number Diff line number Diff line
@@ -4,8 +4,7 @@ namespace Drupal\project_browser_devel\Plugin\ProjectBrowserSource;

use Drupal\Component\Utility\Random;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\project_browser\Plugin\ProjectBrowserSourceInterface;
use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
use Drupal\project_browser\ProjectBrowser\Project;
use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -22,7 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 *   description = @Translation("Gets random project and filters information"),
 * )
 */
class RandomDataPlugin extends PluginBase implements ProjectBrowserSourceInterface, ContainerFactoryPluginInterface {
class RandomDataPlugin extends ProjectBrowserSourceBase implements ContainerFactoryPluginInterface {

  /**
   * Utility to create random data.
@@ -99,26 +98,14 @@ class RandomDataPlugin extends PluginBase implements ProjectBrowserSourceInterfa
   * {@inheritdoc}
   */
  public function getMaintenanceStatuses(): array {
    $data = $this->getRandomIdsAndNames(6);
    // To test a special case in the front end.
    $data[] = [
      'id' => uniqid(),
      'name' => 'Actively maintained',
    ];
    return $data;
    return $this->getRandomIdsAndNames(6);
  }

  /**
   * {@inheritdoc}
   */
  public function getSecurityCoverages(): array {
    $data = $this->getRandomIdsAndNames(2);
    // To test a special case in the front end.
    $data[] = [
      'id' => 'covered',
      'name' => 'Covered',
    ];
    return $data;
    return $this->getRandomIdsAndNames(2);
  }

  /**
@@ -128,6 +115,13 @@ class RandomDataPlugin extends PluginBase implements ProjectBrowserSourceInterfa
    return $this->getRandomIdsAndNames(20);
  }

  /**
   * {@inheritdoc}
   */
  protected function convertQueryOptions(array $query = []): array {
    return $query;
  }

  /**
   * {@inheritdoc}
   */
@@ -173,10 +167,34 @@ class RandomDataPlugin extends PluginBase implements ProjectBrowserSourceInterfa
        'project_usage_total' => rand(0, 100000),
        'flag_project_star_user_count' => rand(0, 100),
      ];
      $project['is_covered'] = $this->projectIsCovered($project);
      $project['is_active'] = $this->projectIsActive($project);
      $project['is_maintained'] = $this->projectIsMaintained($project);
      $projects[] = new Project($project);
    }

    return new ProjectsResultsPage(count($projects), $projects);
  }

  /**
   * {@inheritdoc}
   */
  public function projectIsCovered(array $project): bool {
    return (bool) rand(0, 1);
  }

  /**
   * {@inheritdoc}
   */
  public function projectIsActive(array $project): bool {
    return (bool) rand(0, 1);
  }

  /**
   * {@inheritdoc}
   */
  public function projectIsMaintained(array $project): bool {
    return (bool) rand(0, 1);
  }

}
+17 −56
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ use Drupal\Core\Extension\InfoParserException;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\project_browser\EnabledSourceHandler;
use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\Messenger\MessengerInterface;
@@ -99,9 +100,9 @@ class BrowserController extends ControllerBase {
  public function browse() {
    $modules_status = $this->getModuleStatuses();
    $request = $this->requestStack->getCurrentRequest();

    $current_source = $this->enabledSource->getCurrentSource();
    if ($current_source && $current_source->getPluginId() === 'drupalorg_mockapi') {

    if ($current_source->getPluginId() === 'drupalorg_mockapi') {
      $this->messenger
        ->addStatus($this->t('Project Browser is currently a prototype, and the projects listed may not be up to date with Drupal.org. For the most updated list of projects, please visit <a href=":url">:url</a>', [':url' => 'https://www.drupal.org/project/project_module']))
        ->addStatus($this->t('Your feedback and input are welcome at <a href=":url">:url</a>', [':url' => 'https://www.drupal.org/project/issues/project_browser']));
@@ -121,6 +122,10 @@ class BrowserController extends ControllerBase {
            'module_path' => $this->moduleHandler->getModule('project_browser')->getPath(),
            'origin_url' => $request->getSchemeAndHttpHost() . $request->getBaseUrl(),
            'special_ids' => $this->getSpecialIds(),
            'sort_options' => array_values($current_source->getSortOptions()),
            'maintenance_options' => $current_source->getMaintenanceOptions(),
            'security_options' => $current_source->getSecurityOptions(),
            'development_options' => $current_source->getDevelopmentOptions(),
          ],
        ],
      ],
@@ -130,69 +135,25 @@ class BrowserController extends ControllerBase {
  /**
   * Return special IDs for some vocabularies.
   *
   * This is needed because these two vocabularies have a special term
   * in them that shows an icon next to the label, so we need to be
   * explicit about these special cases.
   *
   * @return array
   *   List of special IDs per vocabulary.
   */
  protected function getSpecialIds(): array {
    // All rendered labels could change at any time in the back-end.
    // For now, we assume that these strings will remain the same, regardless
    // of the plugin, as that has been the case for years but bear in mind that
    // the front-end might NOT preselect these entries if the labels ever
    // change.
    $special_ids = [
    return [
      'maintenance_status' => [
        'id' => '-1',
        'name' => 'Actively maintained',
        'id' => ProjectBrowserSourceBase::MAINTAINED_ID,
        'name' => $this->t('@maintained_label', ['@maintained_label' => ProjectBrowserSourceBase::MAINTAINED_LABEL]),
      ],
      'security_coverage' => [
        'id' => '-1',
        'name' => 'Covered',
        'id' => ProjectBrowserSourceBase::COVERED_ID,
        'name' => $this->t('@covered_label', ['@covered_label' => ProjectBrowserSourceBase::COVERED_LABEL]),
      ],
      'all_values' => ProjectBrowserSourceBase::ALL_VALUES_ID,
    ];

    $maintenance_status_values = $this->getDataFromPlugin('maintenance_status', 'getMaintenanceStatuses');
    foreach ($maintenance_status_values as $item) {
      if ($item['name'] == $special_ids['maintenance_status']['name']) {
        $special_ids['maintenance_status']['id'] = $item['id'];
      }
    }

    $security_coverage_values = $this->getDataFromPlugin('security_coverage', 'getSecurityCoverages');
    foreach ($security_coverage_values as $item) {
      if ($item['name'] == $special_ids['security_coverage']['name']) {
        $special_ids['security_coverage']['id'] = $item['id'];
      }
    }

    return $special_ids;
  }

  /**
   * Gets data from the enabled plugin.
   *
   * @param string $cache_key
   *   Cache key to check if available, otherwise set.
   * @param string $method
   *   Plugin method to use if the data is not cached.
   *
   * @return array
   *   Resulting information.
   */
  protected function getDataFromPlugin($cache_key, $method): array {
    $data = [];
    $current_source = $this->enabledSource->getCurrentSource();
    if ($current_source) {
      $cache_key = "project_browser:$cache_key";
      if ($values = $this->cacheBin->get($cache_key)) {
        $data = $values->data;
      }
      elseif (method_exists($current_source, $method)) {
        $data = $current_source->$method();
        $this->cacheBin->set($cache_key, $data);
      }
    }

    return $data;
  }

  /**
+1 −29
Original line number Diff line number Diff line
@@ -82,35 +82,7 @@ class ProjectBrowserEndpointController extends ControllerBase {

    $sort = $request->query->get('sort');
    if ($sort) {
      $direction = $request->query->get('direction');
      $direction = ($direction == 'ASC') ? 'ASC' : 'DESC';
      // Values offered in the front-end.
      $accepted_sort_values = [
        'usage_total',
        'a_z',
        'z_a',
        'created',
        'best_match',
      ];
      if (in_array($sort, $accepted_sort_values)) {
        // Transform to values accepted by the plugins. See options in the
        // comments here: ProjectBrowserSourceInterface::getProjects.
        switch ($sort) {
          case 'a_z':
            $sort = 'title';
            $direction = 'ASC';
            break;

          case 'z_a':
            $sort = 'title';
            $direction = 'DESC';
            break;

        }

      $query['sort'] = $sort;
        $query['direction'] = $direction;
      }
    }

    $title = $request->query->get('search');
+207 −25
Original line number Diff line number Diff line
@@ -6,9 +6,8 @@ use Composer\Semver\Semver;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Database\Connection;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\State\StateInterface;
use Drupal\project_browser\Plugin\ProjectBrowserSourceInterface;
use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
use Drupal\project_browser\ProjectBrowser\Project;
use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage;
use GuzzleHttp\ClientInterface;
@@ -29,7 +28,28 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
 *   description = @Translation("Gets project and filters information from a mock API"),
 * )
 */
class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterface, ContainerFactoryPluginInterface {
class MockDrupalDotOrg extends ProjectBrowserSourceBase implements ContainerFactoryPluginInterface {

  /**
   * This is what the Mock understands as "Covered" modules.
   *
   * @var array
   */
  const COVERED_VALUES = ['covered'];

  /**
   * This is what the Mock understands as "Active" modules.
   *
   * @var array
   */
  const ACTIVE_VALUES = [9988, 13030];

  /**
   * This is what the Mock understands as "Maintained" modules.
   *
   * @var array
   */
  const MAINTAINED_VALUES = [13028, 19370, 9990];

  /**
   * A logger instance.
@@ -135,6 +155,148 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa
    ];
  }

  /**
   * Convert the sort entry within the query from received to expected by DB.
   *
   * @param array $query
   *   Query array to transform.
   */
  protected function convertSort(array &$query) {
    if (!empty($query['sort'])) {
      $options_available = $this->getSortOptions();
      if (!in_array($query['sort'], array_keys($options_available))) {
        unset($query['sort']);
      }
      else {
        // Valid value.
        switch ($query['sort']) {
          case 'usage_total':
          case 'best_match':
            $query['sort'] = 'project_usage_total';
            $query['direction'] = 'DESC';
            break;

          case 'a_z':
            $query['sort'] = 'title';
            $query['direction'] = 'ASC';
            break;

          case 'z_a':
            $query['sort'] = 'title';
            $query['direction'] = 'DESC';
            break;

          case 'created':
            $query['sort'] = 'created';
            $query['direction'] = 'DESC';
            break;

        }
      }
    }
  }

  /**
   * Convert the maintenance entry within the query from received to expected by DB.
   *
   * @param array $query
   *   Query array to transform.
   */
  protected function convertMaintenance(array &$query) {
    if (!empty($query['maintenance_status'])) {
      $options_available = $this->getMaintenanceOptions();
      if (!in_array($query['maintenance_status'], array_keys($options_available))) {
        unset($query['maintenance_status']);
      }
      else {
        // Valid value.
        switch ($query['maintenance_status']) {
          case self::MAINTAINED_ID:
            $query['maintenance_status'] = self::MAINTAINED_VALUES;
            break;

          case 'all':
            unset($query['maintenance_status']);
            break;

        }
      }
    }
  }

  /**
   * Convert the development entry within the query from received to expected by DB.
   *
   * @param array $query
   *   Query array to transform.
   */
  protected function convertDevelopment(array &$query) {
    if (!empty($query['development_status'])) {
      $options_available = $this->getDevelopmentOptions();
      if (!in_array($query['development_status'], array_keys($options_available))) {
        unset($query['development_status']);
      }
      else {
        // Valid value.
        switch ($query['development_status']) {
          case self::ACTIVE_ID:
            $query['development_status'] = self::ACTIVE_VALUES;
            break;

          case 'all':
            unset($query['development_status']);
            break;

        }
      }
    }
  }

  /**
   * Convert the security entry within the query from received to expected by DB.
   *
   * @param array $query
   *   Query array to transform.
   */
  protected function convertSecurity(array &$query) {
    if (!empty($query['security_advisory_coverage'])) {
      $options_available = $this->getSecurityOptions();
      if (!in_array($query['security_advisory_coverage'], array_keys($options_available))) {
        unset($query['security_advisory_coverage']);
      }
      else {
        // Valid value.
        switch ($query['security_advisory_coverage']) {
          case self::COVERED_ID:
            $query['security_advisory_coverage'] = self::COVERED_VALUES;
            break;

          case 'all':
            $keys = [];
            $options = $this->getSecurityCoverages();
            foreach ($options as $option) {
              $keys[] = $option['id'];
            }
            $query['security_advisory_coverage'] = $keys;
            break;

        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function convertQueryOptions(array $query = []): array {
    $this->convertSort($query);
    $this->convertMaintenance($query);
    $this->convertDevelopment($query);
    $this->convertSecurity($query);

    return $query;
  }

  /**
   * {@inheritdoc}
   */
@@ -225,6 +387,9 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa
        }

        $project['flag_project_star_user_count'] = 0;
        $project['is_covered'] = $this->projectIsCovered($project);
        $project['is_active'] = $this->projectIsActive($project);
        $project['is_maintained'] = $this->projectIsMaintained($project);

        $returned_list[] = new Project($project);
      }
@@ -241,6 +406,7 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa
   * the Drupal.org (JSON?) API (TBD).
   */
  protected function fetchProjects($query) {
    $query = $this->convertQueryOptions($query);
    try {
      $db_query = $this->database->select('project_browser_projects', 'pbp')
        ->fields('pbp')
@@ -248,44 +414,27 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa

      if (array_key_exists('sort', $query) && !empty($query['sort'])) {
        $sort = $query['sort'];
        $accepted_sort_values = [
          'usage_total',
          'created',
          'title',
          'best_match',
        ];
        $direction = (array_key_exists('direction', $query) && $query['direction'] == 'ASC') ? 'ASC' : 'DESC';
        if (in_array($sort, $accepted_sort_values)) {
          // Map any sort values from the controller to the actual data.
          if ($sort == 'usage_total') {
            $sort = 'project_usage_total';
          }
          elseif ($sort == 'best_match') {
            $sort = 'project_usage_total';
          }
        $db_query->orderBy($sort, $direction);
      }
      }
      else {
        // Default order.
        $db_query->orderBy('project_usage_total', 'DESC');
      }

      // Filter by maintenance status.
      if (array_key_exists('maintenance_status', $query)) {
        $tids = explode(',', $query['maintenance_status']);
        $db_query->condition('maintenance_status', $tids, 'IN');
        $db_query->condition('maintenance_status', $query['maintenance_status'], 'IN');
      }

      // Filter by development status.
      if (array_key_exists('development_status', $query)) {
        $tids = explode(',', $query['development_status']);
        $db_query->condition('development_status', $tids, 'IN');
        $db_query->condition('development_status', $query['development_status'], 'IN');
      }

      // Filter by security advisory coverage.
      if (array_key_exists('security_advisory_coverage', $query)) {
        $tids = explode(',', $query['security_advisory_coverage']);
        $db_query->condition('field_security_advisory_coverage', $tids, 'IN');
        $db_query->condition('field_security_advisory_coverage', $query['security_advisory_coverage'], 'IN');
      }

      // Filter by category.
@@ -606,4 +755,37 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa
    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function projectIsCovered(array $project): bool {
    if (!empty($project['field_security_advisory_coverage'])) {
      return in_array($project['field_security_advisory_coverage'], self::COVERED_VALUES);
    }

    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function projectIsActive(array $project): bool {
    if (!empty($project['field_development_status'])) {
      return in_array($project['field_development_status']['id'], self::ACTIVE_VALUES);
    }

    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function projectIsMaintained(array $project): bool {
    if (!empty($project['field_maintenance_status'])) {
      return in_array($project['field_maintenance_status']['id'], self::MAINTAINED_VALUES);
    }

    return FALSE;
  }

}
+175 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\project_browser\Plugin;

use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Defines an abstract base class for a Project Browser source.
 *
 * @see \Drupal\project_browser\Annotation\ProjectBrowserSource
 * @see \Drupal\project_browser\Plugin\ProjectBrowserSourceManager
 * @see plugin_api
 */
abstract class ProjectBrowserSourceBase extends PluginBase implements ProjectBrowserSourceInterface {

  use StringTranslationTrait;

  /**
   * Label for maintained status.
   *
   * @var string
   */
  const MAINTAINED_LABEL = 'Maintained';

  /**
   * ID for maintained status.
   *
   * @var string
   */
  const MAINTAINED_ID = 'maintained';

  /**
   * Label for covered status.
   *
   * @var string
   */
  const COVERED_LABEL = 'Covered by a security policy';

  /**
   * ID for covered status.
   *
   * @var string
   */
  const COVERED_ID = 'covered';

  /**
   * Label for active status.
   *
   * @var string
   */
  const ACTIVE_LABEL = 'Active';

  /**
   * ID for active status.
   *
   * @var string
   */
  const ACTIVE_ID = 'active';

  /**
   * Label for all values.
   *
   * @var string
   */
  const ALL_VALUES_LABEL = 'Show all';

  /**
   * ID for all values.
   *
   * @var string
   */
  const ALL_VALUES_ID = 'all';

  /**
   * {@inheritdoc}
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * Returns the available security options that plugins will parse.
   *
   * @return array
   *   Options offered.
   */
  final public function getSecurityOptions(): array {
    return [
      self::COVERED_ID => $this->t('@covered_label', ['@covered_label' => self::COVERED_LABEL]),
      self::ALL_VALUES_ID => $this->t('@all_values_label', ['@all_values_label' => self::ALL_VALUES_LABEL]),
    ];
  }

  /**
   * Returns the available maintenance options that plugins will parse.
   *
   * @return array
   *   Options offered.
   */
  final public function getMaintenanceOptions(): array {
    return [
      self::MAINTAINED_ID => $this->t('@maintained_label', ['@maintained_label' => self::MAINTAINED_LABEL]),
      self::ALL_VALUES_ID => $this->t('@all_values_label', ['@all_values_label' => self::ALL_VALUES_LABEL]),
    ];
  }

  /**
   * Returns the available maintenance options that plugins will parse.
   *
   * @return array
   *   Options offered.
   */
  final public function getDevelopmentOptions(): array {
    return [
      self::ACTIVE_ID => $this->t('@active_label', ['@active_label' => self::ACTIVE_LABEL]),
      self::ALL_VALUES_ID => $this->t('@all_values_label', ['@all_values_label' => self::ALL_VALUES_LABEL]),
    ];
  }

  /**
   * Returns the available sort options that plugins will parse.
   *
   * @return array
   *   Options offered.
   */
  final public function getSortOptions(): array {
    return [
      'usage_total' => [
        'id' => 'usage_total',
        'text' => $this->t('Project Usage'),
      ],
      'a_z' => [
        'id' => 'a_z',
        'text' => $this->t('A-Z'),
      ],
      'z_a' => [
        'id' => 'z_a',
        'text' => $this->t('Z-A'),
      ],
      'created' => [
        'id' => 'created',
        'text' => $this->t('Date Created (Most Recent)'),
      ],
      'best_match' => [
        'id' => 'best_match',
        'text' => $this->t('Best match'),
      ],
    ];
  }

  /**
   * Convert the search values from available ones to expected ones.
   *
   * The values that were given as available for the search need to be the
   * actual values that will be queried within the search function.
   *
   * @param array $query
   *   Query parameters to check.
   *
   *   The only accepted values for the following fields are limited by the
   *   following methods within this class:
   *   - sort: getSortOptions()
   *   - maintenance_status: getMaintenanceOptions()
   *   - development_status: getDevelopmentOptions()
   *   - security_advisory_coverage: getSecurityOptions()
   *
   *   Any other keys inside the array will be left intact.
   *
   * @return array
   *   Query parameters converted to the values expected by the search function.
   */
  abstract protected function convertQueryOptions(array $query = []): array;

}
Loading