diff --git a/scripts/regenerate-drupalorg-jsonapi-fixture.php b/scripts/regenerate-drupalorg-jsonapi-fixture.php index 4f99e0a62258b5bb54a409e21386c334c5b19866..1d3d5460f6de2a3a857cfe3540893f019415e04b 100644 --- a/scripts/regenerate-drupalorg-jsonapi-fixture.php +++ b/scripts/regenerate-drupalorg-jsonapi-fixture.php @@ -9,22 +9,41 @@ require_once __DIR__ . '/../tests/modules/project_browser_test/src/DrupalOrgClie use Drupal\project_browser_test\DrupalOrgClientMiddleware; -$path_to_fixture = DrupalOrgClientMiddleware::ENDPOINT_TO_FIXTURE_MAP; -$endpoint_url = 'https://www.drupal.org/jsonapi'; +/** + * Generate the fixtures requested. + * + * @param array $map + * Map of path and destination file. + * @param string $endpoint_base_url + * Base URL of the endpoint. + * @param string $destination_folder + * Destination folder. + */ +function generate_fixtures($map, $endpoint_base_url, $destination_folder) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, 'Curl.ProjectBrowser'); + + foreach ($map as $jsonapi_path => $fixture_file_name) { + // Wait between requests as otherwise they could be blocked. + sleep(1); + curl_setopt($ch, CURLOPT_URL, $endpoint_base_url . $jsonapi_path); + $contents = curl_exec($ch); + if ($contents) { + file_put_contents($destination_folder . $fixture_file_name, $contents); + } + } + curl_close($ch); +} + +// Begin script. $destination_folder = __DIR__ . '/../tests/fixtures/drupalorg_jsonapi/'; + +// Make sure the folder exists. if (!is_dir($destination_folder)) { mkdir(rtrim($destination_folder, '/')); } -$ch = curl_init(); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); -curl_setopt($ch, CURLOPT_USERAGENT, 'Curl.ProjectBrowser'); -foreach ($path_to_fixture as $jsonapi_path => $fixture_file_name) { - // Wait between requests as otherwise they could be blocked. - sleep(1); - curl_setopt($ch, CURLOPT_URL, $endpoint_url . $jsonapi_path); - $contents = curl_exec($ch); - if ($contents) { - file_put_contents($destination_folder . $fixture_file_name, $contents); - } -} -curl_close($ch); + +// Generate the fixtures for both the json:api and non json:api paths. +generate_fixtures(DrupalOrgClientMiddleware::DRUPALORG_JSONAPI_ENDPOINT_TO_FIXTURE_MAP, 'https://www.drupal.org/jsonapi', $destination_folder); +generate_fixtures(DrupalOrgClientMiddleware::DRUPALORG_ENDPOINT_TO_FIXTURE_MAP, 'https://www.drupal.org', $destination_folder); diff --git a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php index 3ebcbf15df43fa0310897bcc9285576263fcff67..b4078ccca007ce22f67c2cd4def1bdc6303fb835 100644 --- a/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php +++ b/src/Plugin/ProjectBrowserSource/DrupalDotOrgJsonApi.php @@ -2,8 +2,10 @@ namespace Drupal\project_browser\Plugin\ProjectBrowserSource; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Html; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ExtensionVersion; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; @@ -62,32 +64,6 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { */ const COVERED_VALUES = ['covered']; - /** - * This is what drupal.org plugin understands as "Active" modules. - * - * @var array - */ - const ACTIVE_VALUES = [ - // 'Under active development' - '8042997b-8638-4ed7-992b-ca3581d9df2b', - // 'Maintenance fixes only' - '55f3e13b-29a9-472b-940a-c8fbd46ebfb5', - ]; - - /** - * This is what drupal.org plugin understands as "Maintained" modules. - * - * @var array - */ - const MAINTAINED_VALUES = [ - // 'Actively maintained'. - '49125cb6-2f35-451b-922d-3042cb1b4391', - // 'Minimally maintained' - 'de457d5a-2ce3-45b4-b88b-1c54e5e6d0e2', - // 'Seeking co-maintainer(s) - 'fd8b539f-a5e4-4577-9367-f119a252327b', - ]; - /** * Constructs a MockDrupalDotOrg object. * @@ -101,6 +77,10 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { * A logger instance. * @param \GuzzleHttp\ClientInterface $httpClient * A Guzzle client object. + * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBin + * The cache bin. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. */ public function __construct( array $configuration, @@ -108,6 +88,8 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { $plugin_definition, protected LoggerInterface $logger, protected ClientInterface $httpClient, + protected CacheBackendInterface $cacheBin, + protected TimeInterface $time, ) { parent::__construct($configuration, $plugin_id, $plugin_definition); } @@ -122,6 +104,8 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { $plugin_definition, $container->get('logger.factory')->get('project_browser'), $container->get(ClientInterface::class), + $container->get('cache.project_browser'), + $container->get('datetime.time'), ); } @@ -301,6 +285,11 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { public function getProjects(array $query = []): ProjectsResultsPage { $api_response = $this->fetchProjects($query); + // @todo Remove the next three lines when https://www.drupal.org/project/project_browser/issues/3309273 is merged. + $filter_values = $this->filterValues(); + $active_values = $filter_values['active'] ?? []; + $maintained_values = $filter_values['maintained'] ?? []; + $returned_list = []; if (is_array($api_response) && !empty($api_response['list'])) { $related = !empty($api_response['related']) ? $api_response['related'] : NULL; @@ -386,9 +375,9 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { $project_object = new Project( logo: $logo, isCompatible: $is_compatible, - isMaintained: in_array($maintenance_status['id'], self::MAINTAINED_VALUES), + isMaintained: in_array($maintenance_status['id'], $maintained_values), isCovered: in_array($project['attributes']['field_security_advisory_coverage'], self::COVERED_VALUES), - isActive: in_array($development_status['id'], self::ACTIVE_VALUES), + isActive: in_array($development_status['id'], $active_values), // Property not migrated to D9. starUserCount: 0, projectUsageTotal: $project_usage_total, @@ -630,10 +619,45 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { return $query_params; } + /** + * Returns the filter values from the www.drupal.org endpoint. + * + * @return array + * Filter values by taxonomy. + */ + protected function filterValues(): array { + $values = []; + $url = self::DRUPAL_ORG_ENDPOINT . '/drupalorg-api/project-browser-filters'; + $filter_values = $this->cacheBin->get('DrupalDotOrgJsonApi:filter_values'); + if ($filter_values) { + $values = $filter_values->data; + } + else { + $expiry_time = $this->time->getRequestTime() + 3600; + try { + $response = $this->httpClient->request('GET', $url); + $values = Json::decode($response->getBody()->getContents()); + $this->cacheBin->set('DrupalDotOrgJsonApi:filter_values', $values, $expiry_time); + } + catch (GuzzleException $exception) { + $this->logger->error($exception->getMessage()); + } + catch (\Throwable $exception) { + $this->logger->error($exception->getMessage()); + } + } + + return $values; + } + /** * {@inheritdoc} */ protected function convertQueryOptions(array $query = []): array { + $filter_values = $this->filterValues(); + $active_values = $filter_values['active'] ?? []; + $maintained_values = $filter_values['maintained'] ?? []; + // Sort options. $sort = NULL; if (!empty($query['sort'])) { @@ -650,7 +674,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { $maintenance = NULL; if (!empty($query['maintenance_status'])) { if ($query['maintenance_status'] == MaintenanceStatus::Maintained->value) { - $maintenance = implode(',', self::MAINTAINED_VALUES); + $maintenance = implode(',', $maintained_values); } } $query['maintenance_status'] = $maintenance; @@ -659,7 +683,7 @@ class DrupalDotOrgJsonApi extends ProjectBrowserSourceBase { $development = NULL; if (!empty($query['development_status'])) { if ($query['development_status'] == DevelopmentStatus::Active->value) { - $development = implode(',', self::ACTIVE_VALUES); + $development = implode(',', $active_values); } } $query['development_status'] = $development; diff --git a/tests/fixtures/drupalorg_jsonapi/project-browser-filters.json b/tests/fixtures/drupalorg_jsonapi/project-browser-filters.json new file mode 100644 index 0000000000000000000000000000000000000000..1d21cd3707970ba52a6c9cb576f322e3d53b9d5e --- /dev/null +++ b/tests/fixtures/drupalorg_jsonapi/project-browser-filters.json @@ -0,0 +1 @@ +{"active":["8042997b-8638-4ed7-992b-ca3581d9df2b","55f3e13b-29a9-472b-940a-c8fbd46ebfb5"],"maintained":["49125cb6-2f35-451b-922d-3042cb1b4391","de457d5a-2ce3-45b4-b88b-1c54e5e6d0e2","fd8b539f-a5e4-4577-9367-f119a252327b"]} \ No newline at end of file diff --git a/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php b/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php index aa173f1b74a2222ea3f2f22aa869b653bf538e83..2fccb7c7640b2727ae01646397f1387618ace05c 100644 --- a/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php +++ b/tests/modules/project_browser_test/src/DrupalOrgClientMiddleware.php @@ -18,12 +18,12 @@ use Psr\Http\Message\RequestInterface; class DrupalOrgClientMiddleware { /** - * Endpoint to fixture mapping. + * Json:API Endpoints to fixture mapping. * * These are the files used and what they contain: * - categories.json: List of available categories. Used in all tests. * - default_modules.json: List of modules while visiting - * - 'admin/modules/browse' first time. Used in all tests. + * 'admin/modules/browse' first time. Used in all tests. * - 1.json: 'Clear filters' clicked. * - 2.json: 'E-commerce' checked in testCategoryFiltering. * - 3.json: 'Media' checked in testCategoryFiltering. @@ -42,7 +42,7 @@ class DrupalOrgClientMiddleware { * * @const array */ - const ENDPOINT_TO_FIXTURE_MAP = [ + const DRUPALORG_JSONAPI_ENDPOINT_TO_FIXTURE_MAP = [ '/taxonomy_term/module_categories?sort=name&filter%5Bstatus%5D=1&fields%5Btaxonomy_term--module_categories%5D=name' => 'categories.json', '/index/project_modules?filter%5Bstatus%5D=1&filter%5Btype%5D=project_module&filter%5Bproject_type%5D=full&page%5Blimit%5D=12&page%5Boffset%5D=0&include=field_supporting_organizations%2Cfield_supporting_organizations.field_supporting_organization%2Cfield_module_categories%2Cfield_maintenance_status%2Cfield_development_status%2Cuid%2Cfield_project_images&filter%5Bcore_semver_minimum%5D%5Boperator%5D=%3C%3D&filter%5Bcore_semver_minimum%5D%5Bpath%5D=core_semver_minimum&filter%5Bcore_semver_maximum%5D%5Boperator%5D=%3E%3D&filter%5Bcore_semver_maximum%5D%5Bpath%5D=core_semver_maximum&filter%5Bmaintenance_status_uuid%5D%5Bvalue%5D%5B0%5D=49125cb6-2f35-451b-922d-3042cb1b4391&filter%5Bmaintenance_status_uuid%5D%5Bvalue%5D%5B1%5D=de457d5a-2ce3-45b4-b88b-1c54e5e6d0e2&filter%5Bmaintenance_status_uuid%5D%5Bvalue%5D%5B2%5D=fd8b539f-a5e4-4577-9367-f119a252327b&filter%5Bmaintenance_status_uuid%5D%5Boperator%5D=IN&filter%5Bmaintenance_status_uuid%5D%5Bpath%5D=maintenance_status_uuid&filter%5Bsecurity_coverage%5D%5Bvalue%5D%5B0%5D=covered&filter%5Bsecurity_coverage%5D%5Boperator%5D=IN&filter%5Bsecurity_coverage%5D%5Bpath%5D=security_coverage&filter%5Bn_security_coverage%5D%5Bvalue%5D%5B0%5D=revoked&filter%5Bn_security_coverage%5D%5Boperator%5D=NOT%20IN&filter%5Bn_security_coverage%5D%5Bpath%5D=security_coverage' => 'default_modules.json', '/index/project_modules?filter%5Bstatus%5D=1&filter%5Btype%5D=project_module&filter%5Bproject_type%5D=full&page%5Blimit%5D=12&page%5Boffset%5D=0&include=field_supporting_organizations%2Cfield_supporting_organizations.field_supporting_organization%2Cfield_module_categories%2Cfield_maintenance_status%2Cfield_development_status%2Cuid%2Cfield_project_images&filter%5Bcore_semver_minimum%5D%5Boperator%5D=%3C%3D&filter%5Bcore_semver_minimum%5D%5Bpath%5D=core_semver_minimum&filter%5Bcore_semver_maximum%5D%5Boperator%5D=%3E%3D&filter%5Bcore_semver_maximum%5D%5Bpath%5D=core_semver_maximum&filter%5Bn_security_coverage%5D%5Bvalue%5D%5B0%5D=revoked&filter%5Bn_security_coverage%5D%5Boperator%5D=NOT%20IN&filter%5Bn_security_coverage%5D%5Bpath%5D=security_coverage' => '1.json', @@ -62,6 +62,15 @@ class DrupalOrgClientMiddleware { "/index/project_modules?filter%5Bstatus%5D=1&filter%5Btype%5D=project_module&filter%5Bproject_type%5D=full&page%5Blimit%5D=12&page%5Boffset%5D=0&include=field_supporting_organizations%2Cfield_supporting_organizations.field_supporting_organization%2Cfield_module_categories%2Cfield_maintenance_status%2Cfield_development_status%2Cuid%2Cfield_project_images&filter%5Bcore_semver_minimum%5D%5Boperator%5D=%3C%3D&filter%5Bcore_semver_minimum%5D%5Bpath%5D=core_semver_minimum&filter%5Bcore_semver_maximum%5D%5Boperator%5D=%3E%3D&filter%5Bcore_semver_maximum%5D%5Bpath%5D=core_semver_maximum&filter%5Bmodule_categories_uuid%5D%5Bvalue%5D%5B0%5D=bafb1104-72cd-4a74-bdcd-3610be685fc5&filter%5Bmodule_categories_uuid%5D%5Bvalue%5D%5B1%5D=c69d3284-cf3c-4096-8124-33df86771e6f&filter%5Bmodule_categories_uuid%5D%5Bvalue%5D%5B2%5D=f70e387f-73be-4523-8af2-a7f7cab8caf6&filter%5Bmodule_categories_uuid%5D%5Boperator%5D=IN&filter%5Bmodule_categories_uuid%5D%5Bpath%5D=module_categories_uuid&filter%5Bn_security_coverage%5D%5Bvalue%5D%5B0%5D=revoked&filter%5Bn_security_coverage%5D%5Boperator%5D=NOT%20IN&filter%5Bn_security_coverage%5D%5Bpath%5D=security_coverage" => '9.json', ]; + /** + * Endpoints for non-jsonapi information. + * + * @const array + */ + const DRUPALORG_ENDPOINT_TO_FIXTURE_MAP = [ + '/drupalorg-api/project-browser-filters' => 'project-browser-filters.json', + ]; + /** * Constructor for settings form. * @@ -102,7 +111,7 @@ class DrupalOrgClientMiddleware { // Processed query will act as relevant path to fixtures. $relevant_path = preg_replace('/&filter%5Bcore_semver_minimum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path); $relevant_path = preg_replace('/&filter%5Bcore_semver_maximum%5D%5Bvalue%5D=[0-9]*/', '', $relevant_path); - $path_to_fixture = self::ENDPOINT_TO_FIXTURE_MAP; + $path_to_fixture = self::DRUPALORG_JSONAPI_ENDPOINT_TO_FIXTURE_MAP; if (isset($path_to_fixture[$relevant_path])) { $module_path = $this->moduleHandler->getModule('project_browser')->getPath(); $data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path]); @@ -112,6 +121,20 @@ class DrupalOrgClientMiddleware { throw new \Exception('Attempted call to the Drupal.org jsonapi endpoint that is not mocked in middleware: ' . $relevant_path); } + // Other queries to the non-jsonapi endpoints. + elseif (strpos($request->getUri(), DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT) !== FALSE) { + $relevant_path = str_replace(DrupalDotOrgJsonApi::DRUPAL_ORG_ENDPOINT, '', $request->getUri()); + $path_to_fixture = self::DRUPALORG_ENDPOINT_TO_FIXTURE_MAP; + + if (isset($path_to_fixture[$relevant_path])) { + $module_path = $this->moduleHandler->getModule('project_browser')->getPath(); + $data = file_get_contents($module_path . '/tests/fixtures/drupalorg_jsonapi/' . $path_to_fixture[$relevant_path]); + $json_response = new Response(200, [], $data); + return new FulfilledPromise($json_response); + } + + throw new \Exception('Attempted call to the Drupal.org endpoint that is not mocked in middleware: ' . $relevant_path); + } return $handler($request, $options); };