Commit eb58eb66 authored by Fran Garcia-Linares's avatar Fran Garcia-Linares Committed by Chris Wells
Browse files

Issue #3278352 by fjgarlin, chrisfromredfin, tim.plunkett: Scaffolding and...

Issue #3278352 by fjgarlin, chrisfromredfin, tim.plunkett: Scaffolding and refactoring to use source plugins consistently
parent 2d78db42
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
project_browser.api_get_categories:
  path: '/drupal-org-proxy/categories'
  methods: [GET]
  defaults:
    _controller: '\Drupal\project_browser\Controller\ProjectBrowserEndpointController::getAllCategories'
    _title: 'Get all categories'
  requirements:
    _permission: 'administer modules'
  #options:
  #  no_cache: 'TRUE'
project_browser.api_project_get_all:
  path: '/drupal-org-proxy/project'
  methods: [GET]
+47 −65
Original line number Diff line number Diff line
@@ -5,12 +5,8 @@ namespace Drupal\project_browser\Controller;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\State\StateInterface;
use Drupal\project_browser\DrupalOrg\DrupalOrgClient;
use Drupal\project_browser\DrupalOrg\DrupalOrgReleases;
use Drupal\project_browser\plugin\ProjectBrowserSourceManager;
use GuzzleHttp\ClientInterface;
use Drupal\project_browser\Plugin\ProjectBrowserSourceInterface;
use Drupal\project_browser\Plugin\ProjectBrowserSourceManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -43,25 +39,6 @@ class ProjectBrowserEndpointController extends ControllerBase {
   */
  private $pluginManager;

  /**
   * The HTTP client.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;
  /**
   * The state object.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;
  /**
   * The database service.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * ProjectBrowserEndpointController constructor.
   *
@@ -71,20 +48,11 @@ class ProjectBrowserEndpointController extends ControllerBase {
   *   The config factory.
   * @param \Drupal\project_browser\plugin\ProjectBrowserSourceManager $plugin_manager
   *   The plugin manager.
   * @param \GuzzleHttp\ClientInterface $http_client
   *   A Guzzle client object.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state object.
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection to be used.
   */
  public function __construct(LoggerInterface $logger, ConfigFactoryInterface $config_factory, ProjectBrowserSourceManager $plugin_manager, ClientInterface $http_client, StateInterface $state, Connection $database) {
  public function __construct(LoggerInterface $logger, ConfigFactoryInterface $config_factory, ProjectBrowserSourceManager $plugin_manager) {
    $this->logger = $logger;
    $this->configFactory = $config_factory;
    $this->pluginManager = $plugin_manager;
    $this->httpClient = $http_client;
    $this->state = $state;
    $this->database = $database;
  }

  /**
@@ -95,12 +63,27 @@ class ProjectBrowserEndpointController extends ControllerBase {
      $container->get('logger.factory')->get('project_browser'),
      $container->get('config.factory'),
      $container->get('plugin.manager.project_browser.source'),
      $container->get('http_client'),
      $container->get('state'),
      $container->get('database'),
    );
  }

  /**
   * Returns a plugin instance corresponding to the enabled_source config.
   *
   * @return \Drupal\project_browser\Plugin\ProjectBrowserSourceInterface|null
   *   The Project Browser source plugin, or NULL.
   */
  protected function getCurrentSource(): ?ProjectBrowserSourceInterface {
    $config = $this->configFactory->get('project_browser.admin_settings');
    $plugin_id = $config->get('enabled_source');
    if (!$this->pluginManager->hasDefinition($plugin_id)) {
      // Ignore if the plugin does not exist, but log it.
      $this->logger->warning('Project browser tried to load the enabled source %source, but the plugin does not exist. Make sure you have run update.php after updating the Project Browser module.', ['%source' => $plugin_id]);
      return NULL;
    }

    return $this->pluginManager->createInstance($plugin_id);
  }

  /**
   * Responds to GET requests.
   *
@@ -113,23 +96,10 @@ class ProjectBrowserEndpointController extends ControllerBase {
   *   Typically a project listing.
   */
  public function getAllProjects(Request $request) {
    // Access our config.
    $config = $this->configFactory->get('project_browser.admin_settings');

    // Get the enabled backend or mockapi.
    $plugin_id = $config->get('enabled_source');
    // It's possible our config is outdated, so smartly skip the plugin
    // that cannot be found, and log a warning in our channel.
    if (!$this->pluginManager->hasDefinition($plugin_id)) {
      // Ignore if the plugin does not exist, but log it.
      $this->logger->warning('Project browser tried to load the enabled source %source, but the plugin does not exist. Make sure you have run update.php after updating the Project Browser module.', ['%source' => $plugin_id]);
      return new JsonResponse([
        'list' => [],
        'total_results' => 0,
      ]);
    $current_source = $this->getCurrentSource();
    if (!$current_source) {
      return new JsonResponse([], Response::HTTP_ACCEPTED);
    }

    $current_source = $this->pluginManager->createInstance($plugin_id);
    $all_projects = $current_source->getProjects($request->query->all());
    return new JsonResponse($all_projects);
  }
@@ -143,12 +113,14 @@ class ProjectBrowserEndpointController extends ControllerBase {
   *   The request.
   */
  public function getProjectReleases(Request $request) {
    try {
      $drupal_org_client = new DrupalOrgClient($this->httpClient, $this->state, $this->database);
      // Forward query parameters from request to Drupal.org Client.
      $drupal_org_response = $drupal_org_client->getProjectReleases($request->query->get('project'));
      $releases = new DrupalOrgReleases($drupal_org_response['releases']);
    $current_source = $this->getCurrentSource();
    if (!$current_source) {
      return new JsonResponse([], Response::HTTP_ACCEPTED);
    }

    $project = $request->query->get('project');
    if ($project) {
      $releases = $current_source->getProjectReleases($project);
      if (count($releases)) {
        $response = new JsonResponse((array) $releases, Response::HTTP_ACCEPTED);
        if ($response instanceof CacheableResponseInterface) {
@@ -157,15 +129,25 @@ class ProjectBrowserEndpointController extends ControllerBase {

        return $response;
      }
      else {
        return new JsonResponse([], Response::HTTP_ACCEPTED);
    }

    return new JsonResponse([], Response::HTTP_ACCEPTED);
  }
    catch (\Exception $exception) {
      $this->logger->error($exception->getMessage());

      return new Response($exception->getMessage(), Response::HTTP_BAD_REQUEST);
  /**
   * Returns a list of categories.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   */
  public function getAllCategories(Request $request) {
    $current_source = $this->getCurrentSource();
    if (!$current_source) {
      return new JsonResponse([], Response::HTTP_ACCEPTED);
    }

    $categories = $current_source->getCategories();
    return new JsonResponse($categories, Response::HTTP_ACCEPTED);
  }

}
+6 −0
Original line number Diff line number Diff line
@@ -13,6 +13,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Retrieves releases and information about releases from Drupal.org.
 *
 * @todo Review the whole class in https://www.drupal.org/i/3279510 as it should
 *   probably go away since the introduction of plugins.
 */
class DrupalOrgClient implements ContainerInjectionInterface {

@@ -65,6 +68,9 @@ class DrupalOrgClient implements ContainerInjectionInterface {
  /**
   * Get a list of all Drupal.org nodes of type 'project_module'.
   *
   * Only used during module install or to populate things. It should probably use
   * the enabled source.
   *
   * @param array $query
   *   An array of query parameters. See https://www.drupal.org/i/3218285.
   *
+6 −0
Original line number Diff line number Diff line
@@ -49,16 +49,22 @@ class DrupalOrgProject {

  /**
   * Module categories.
   *
   * @todo need to change this to field_module_categories.
   */
  public $taxonomy_vocabulary_3;

  /**
   * Maintenance status.
   *
   * @todo need to change this to field_maintenance_status.
   */
  public $taxonomy_vocabulary_44;

  /**
   * Development status.
   *
   * @todo need to change this to field_development_status.
   */
  public $taxonomy_vocabulary_46;

+81 −1
Original line number Diff line number Diff line
@@ -2,9 +2,11 @@

namespace Drupal\project_browser\Plugin\ProjectBrowserSource;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Database\Connection;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\project_browser\DrupalOrg\DrupalOrgReleases;
use Drupal\project_browser\DrupalOrg\Taxonomy\MaintenanceStatus;
use Drupal\project_browser\Plugin\ProjectBrowserSourceInterface;
use Drupal\project_browser\ProjectBrowser\Project;
@@ -15,6 +17,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
 * The source that mocks the Drupal.org API that's still to-be-built.
 *
 * To enable this source (this is the default for the module):
 * - `drush config:set project_browser.admin_settings enabled_source drupalorg_mockapi`
 *
 * @ProjectBrowserSource(
 *   id = "drupalorg_mockapi",
 *   label = @Translation("Drupal.org (mocked)"),
@@ -70,6 +75,44 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getCategories(): array {
    $module_path = \Drupal::service('module_handler')->getModule('project_browser')->getPath();
    return Json::decode(file_get_contents($module_path . '/fixtures/category_list.json'));
  }

  /**
   * {@inheritdoc}
   */
  public function getProjectReleases(string $project): DrupalOrgReleases {
    if ($project === 'drupal/core') {
      $project = 'drupal';
    }
    else {
      $project = str_replace('drupal/', '', $project);
    }

    $response = $this->requestProjectReleases($project);
    if (array_key_exists('releases', $response)) {
      // Only one release.
      if (array_key_exists('name', $response['releases']['release'])) {
        $response['releases'] = [$response['releases']['release']];
      }
      // Multiple releases.
      else {
        $response['releases'] = $response['releases']['release'];
      }
    }
    // No releases.
    else {
      $response['releases'] = [];
    }

    return new DrupalOrgReleases($response['releases']);
  }

  /**
   * {@inheritdoc}
   */
@@ -81,6 +124,12 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa
    $returned_list = [];
    if ($api_response) {
      foreach ($api_response['list'] as $project) {
        if (is_object($project)) {
          $project = (array) $project;
        }
        // @todo Map any properties from the mock to the expected in Project.
        $project['field_maintenance_status'] = $project['taxonomy_vocabulary_44'];
        $project['field_module_categories'] = $project['taxonomy_vocabulary_3'];
        $returned_list[] = new Project($project);
      }
    }
@@ -96,7 +145,7 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa
   * Ultimately, in the real implementation, this would be fetching over
   * the Drupal.org (JSON?) API (TBD).
   */
  private function fetchProjects($query) {
  protected function fetchProjects($query) {
    try {
      $db_query = $this->database->select('project_browser_projects', 'pbp')
        ->fields('pbp')
@@ -190,4 +239,35 @@ class MockDrupalDotOrg extends PluginBase implements ProjectBrowserSourceInterfa
    }
  }

  /**
   * Requests a node from the Drupal.org API.
   *
   * @param string $project
   *   The Drupal.org project name.
   *
   * @return array
   *   The response object.
   *
   * @throws \GuzzleHttp\Exception\GuzzleException
   *   Thrown if request is unsuccessful.
   *
   * @see https://www.drupal.org/drupalorg/docs/apis/rest-and-other-apis#s-releases
   *
   * @see https://www.drupal.org/drupalorg/docs/apis/update-status-xml
   */
  protected function requestProjectReleases(string $project): array {
    $url = "https://updates.drupal.org/release-history/$project/current";
    $response = \Drupal::httpClient()->request('GET', $url);
    if ($response->getStatusCode() !== 200) {
      throw new \RuntimeException("Request to $url failed, returned {$response->getStatusCode()} with reason: {$response->getReasonPhrase()}");
    }
    $body = $response->getBody()->getContents();
    if (strpos($body, 'No release history was found for the requested project') !== FALSE) {
      return [];
    }

    $xml = \simplexml_load_string($body);
    return Json::decode(Json::encode($xml), TRUE);
  }

}
Loading