From cdadac2b353af83db15d72491458de8ae7b58b9f Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Sat, 16 Mar 2019 16:05:11 -0700 Subject: [PATCH 01/48] DRUP-624 Add revisions to API Docs. --- src/Entity/ApiDoc.php | 88 +++++++++++++++++++++++++++++----- src/Entity/ApiDocInterface.php | 21 -------- 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 4b8a25d..b83d7c6 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -20,8 +20,10 @@ namespace Drupal\apigee_api_catalog\Entity; +use Drupal\Core\Entity\EditorialContentEntityBase; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityTypeInterface; @@ -34,6 +36,8 @@ * label_singular = @Translation("API Doc"), * label_plural = @Translation("API Docs"), * handlers = { + * + * "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\apigee_api_catalog\Entity\ListBuilder\ApiDocListBuilder", * "views_data" = "Drupal\views\EntityViewsData", @@ -47,10 +51,14 @@ * "access" = "Drupal\apigee_api_catalog\Entity\Access\ApiDocAccessControlHandler", * "route_provider" = { * "html" = "Drupal\apigee_api_catalog\Entity\Routing\ApiDocHtmlRouteProvider", + * "revision" = "Drupal\entity\Routing\RevisionRouteProvider", * }, * }, * base_table = "apidoc", * data_table = "apidoc_field_data", + * revision_table = "apidoc_revision", + * revision_data_table = "apidoc_field_revision", + * show_revision_ui = TRUE, * translatable = TRUE, * admin_permission = "administer apigee api catalog", * entity_keys = { @@ -58,19 +66,28 @@ * "label" = "name", * "uuid" = "uuid", * "langcode" = "langcode", - * "status" = "status", + * "published" = "status", + * "revision" = "revision_id", + * }, + * revision_metadata_keys = { + * "revision_user" = "revision_user", + * "revision_created" = "revision_created", + * "revision_log_message" = "revision_log_message", * }, * links = { * "canonical" = "/api/{apidoc}", * "add-form" = "/admin/content/api/add", * "edit-form" = "/admin/content/api/{apidoc}/edit", * "delete-form" = "/admin/content/api/{apidoc}/delete", + * "version-history" = "/admin/content/api/{apidoc}/revisions", + * "revision" = "/admin/content/api/{apidoc}/revisions/{apidoc_revision}/view", + * "revision-revert-form" = "/admin/content/api/{apidoc}/revisions/{apidoc_revision}/revert", * "collection" = "/admin/content/apis", * }, * field_ui_base_route = "entity.apidoc.settings" * ) */ -class ApiDoc extends ContentEntityBase implements ApiDocInterface { +class ApiDoc extends EditorialContentEntityBase implements ApiDocInterface { use EntityChangedTrait; @@ -122,16 +139,43 @@ public function setCreatedTime(int $timestamp) : ApiDocInterface { /** * {@inheritdoc} */ - public function isPublished() : bool { - return (bool) $this->getEntityKey('status'); + public function getRevisionUser() { + return $this->get('revision_user')->entity; } /** * {@inheritdoc} */ - public function setPublished(bool $published) : ApiDocInterface { - $this->set('status', $published); - return $this; + public function save() { + return parent::save(); + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // If no revision author has been set explicitly, make the current user + // the revision author. + if (!$this->getRevisionUser()) { + $this->setRevisionUserId(\Drupal::currentUser()->id()); + } + } + + /** + * {@inheritdoc} + */ + public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { + parent::preSaveRevision($storage, $record); + + if (!$this->isNewRevision() && isset($this->original) && empty($record->revision_log_message)) { + // If we are updating an existing entity without adding a new revision, we + // need to make sure $entity->revision_log is reset whenever it is empty. + // Therefore, this code allows us to avoid clobbering an existing log + // entry with an empty one. + $record->revision_log_message = $this->original->revision_log_message->value; + } } /** @@ -143,6 +187,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['name'] = BaseFieldDefinition::create('string') ->setLabel(t('Name')) ->setDescription(t('The name of the API.')) + ->setRevisionable(TRUE) ->setSettings([ 'max_length' => 50, 'text_processing' => 0, @@ -164,6 +209,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')) ->setDescription(t('Description of the API.')) + ->setRevisionable(TRUE) ->setDisplayOptions('view', [ 'label' => 'hidden', 'type' => 'text_default', @@ -179,6 +225,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['spec'] = BaseFieldDefinition::create('file') ->setLabel('OpenAPI specification') ->setDescription('The spec snapshot.') + ->setRevisionable(TRUE) ->setSettings([ 'file_directory' => 'apidoc_specs', 'file_extensions' => 'yml yaml json', @@ -204,7 +251,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['api_product'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('API Product')) - ->setDescription(t('The API Product this API is associated to.')) + ->setDescription(t('The API Product this API is associated with.')) + ->setRevisionable(TRUE) ->setSetting('target_type', 'api_product') ->setDisplayOptions('form', [ 'label' => 'above', @@ -214,10 +262,9 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE); - $fields['status'] = BaseFieldDefinition::create('boolean') + $fields['status'] ->setLabel(t('Publishing status')) ->setDescription(t('A boolean indicating whether the API Doc is published.')) - ->setDefaultValue(TRUE) ->setDisplayOptions('form', [ 'type' => 'boolean_checkbox', 'weight' => 1, @@ -225,13 +272,28 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['created'] = BaseFieldDefinition::create('created') ->setLabel(t('Created')) - ->setDescription(t('The time that the entity was created.')); + ->setDescription(t('The time that the entity was created.')) + ->setRevisionable(TRUE); $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')) - ->setDescription(t('The time that the entity was last edited.')); + ->setDescription(t('The time that the entity was last edited.')) + ->setRevisionable(TRUE); return $fields; } + /** + * {@inheritdoc} + */ + protected function urlRouteParameters($rel) { + $uri_route_parameters = parent::urlRouteParameters($rel); + + if ($rel === 'revision-revert-form' && $this instanceof RevisionableInterface) { + $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId(); + } + + return $uri_route_parameters; + } + } diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 0157fff..740e061 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -85,25 +85,4 @@ public function getCreatedTime() : int; */ public function setCreatedTime(int $timestamp) : self; - /** - * Returns the API Doc published status indicator. - * - * Unpublished API Doc are only visible to restricted users. - * - * @return bool - * TRUE if the API Doc is published. - */ - public function isPublished() : bool; - - /** - * Sets the published status of a API Doc. - * - * @param bool $published - * TRUE to set this API Doc to published, FALSE to set it to unpublished. - * - * @return \Drupal\apigee_api_catalog\Entity\ApiDocInterface - * The called API Doc entity. - */ - public function setPublished(bool $published) : self; - } From 09322c701978d0f616ec002f24037bbc4f98ebd2 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 19 Mar 2019 12:33:31 -0700 Subject: [PATCH 02/48] DRUP-624 Add revisioning tests to API Docs. --- tests/src/Kernel/ApidocEntityTest.php | 72 ++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/src/Kernel/ApidocEntityTest.php b/tests/src/Kernel/ApidocEntityTest.php index ffb1fb3..7686e97 100755 --- a/tests/src/Kernel/ApidocEntityTest.php +++ b/tests/src/Kernel/ApidocEntityTest.php @@ -30,6 +30,13 @@ */ class ApidocEntityTest extends KernelTestBase { + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + protected static $modules = [ 'user', 'text', @@ -39,11 +46,22 @@ class ApidocEntityTest extends KernelTestBase { 'apigee_api_catalog', ]; + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('user'); + $this->installEntitySchema('apidoc'); + + $this->entityTypeManager = $this->container->get('entity_type.manager'); + } + /** * Basic CRUD operations on a ApiDoc entity. */ public function testEntity() { - $this->installEntitySchema('apidoc'); $entity = ApiDoc::create([ 'name' => 'API 1', 'description' => 'Test API 1', @@ -59,4 +77,56 @@ public function testEntity() { $this->assertNull(ApiDoc::load($entity_id)); } + /** + * Test revisioning functionality on an apidocs entity. + */ + public function testRevisions() { + $description_v1 = 'Test API'; + $entity = ApiDoc::create([ + 'name' => 'API 1', + 'description' => $description_v1, + 'spec' => NULL, + 'api_product' => NULL, + ]); + + // Test saving a revision. + $entity->setNewRevision(); + $entity->setRevisionLogMessage('v1'); + $entity->save(); + $v1_id = $entity->getRevisionId(); + $this->assertNotNull($v1_id); + + // Test saving a new revision. + $new_log = 'v2'; + $entity->setDescription('Test API v2'); + $entity->setNewRevision(); + $entity->setRevisionLogMessage($new_log); + $entity->save(); + $v2_id = $entity->getRevisionId(); + $this->assertTrue($v2_id > $v1_id); + + // Test saving without a new revision. + $entity->setDescription('Test API v3'); + $entity->save(); + $this->assertTrue($v2_id === $entity->getRevisionId()); + + // Test that the revision log message wasn't overriden. + $this->assertEquals($new_log, $entity->getRevisionLogMessage()); + + // Revert to the first revision. + $entity_v1 = $this->entityTypeManager->getStorage('apidoc') + ->loadRevision($v1_id); + $entity_v1->setNewRevision(); + $entity_v1->isDefaultRevision(TRUE); + $entity_v1->setRevisionLogMessage('Copy of revision ' . $v1_id); + $entity_v1->save(); + + // Load and check reverted values. + $this->entityTypeManager->getStorage('apidoc')->resetCache(); + $reverted = ApiDoc::load($entity->id()); + $this->assertTrue($reverted->getRevisionId() > $v1_id); + $this->assertTrue($reverted->isDefaultRevision()); + $this->assertEquals($description_v1, $reverted->getDescription()); + } + } From c689b4cf6fddb0522226d39a811623726074ebde Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Wed, 20 Mar 2019 11:04:40 -0700 Subject: [PATCH 03/48] DRUP-624 Add revisioning permissions with tests to API Docs. --- apigee_api_catalog.permissions.yml | 8 + .../Access/ApiDocAccessControlHandler.php | 74 +++++++ .../ApidocEntityRevisionsAccessTest.php | 200 ++++++++++++++++++ tests/src/Kernel/ApidocEntityTest.php | 2 +- 4 files changed, 283 insertions(+), 1 deletion(-) create mode 100755 tests/src/Kernel/ApidocEntityRevisionsAccessTest.php diff --git a/apigee_api_catalog.permissions.yml b/apigee_api_catalog.permissions.yml index a1816c8..671e211 100755 --- a/apigee_api_catalog.permissions.yml +++ b/apigee_api_catalog.permissions.yml @@ -17,3 +17,11 @@ view published apidoc entities: view unpublished apidoc entities: title: 'View unpublished API docs' + +view all apidoc revisions: + title: 'View API Docs revisions' + description: 'To view a revision, you also need permission to view the item.' + +revert all apidoc revisions: + title: 'Revert API Docs revisions' + description: 'To revert a revision, you also need permission to edit the item.' diff --git a/src/Entity/Access/ApiDocAccessControlHandler.php b/src/Entity/Access/ApiDocAccessControlHandler.php index 35f83e0..601453d 100755 --- a/src/Entity/Access/ApiDocAccessControlHandler.php +++ b/src/Entity/Access/ApiDocAccessControlHandler.php @@ -22,6 +22,7 @@ use Drupal\Core\Entity\EntityAccessControlHandler; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessResult; @@ -32,6 +33,20 @@ */ class ApiDocAccessControlHandler extends EntityAccessControlHandler { + /** + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + public function __construct(EntityTypeInterface $entity_type) { + parent::__construct($entity_type); + + $this->entityTypeManager = \Drupal::entityTypeManager(); + } + /** * {@inheritdoc} */ @@ -40,6 +55,11 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter if (!$parent_access->isAllowed()) { /** @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ + + // Access control for revisions. + if (!$entity->isDefaultRevision()) { + return $this->checkAccessRevisions($entity, $operation, $account); + } switch ($operation) { case 'view': if (!$entity->isPublished()) { @@ -59,6 +79,60 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter return $parent_access; } + /** + * Additional access control for revisions. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param $operation + * @param \Drupal\Core\Session\AccountInterface $account + * + * @return bool + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function checkAccessRevisions(EntityInterface $entity, $operation, AccountInterface $account) { + $entity_access = $this->entityTypeManager->getAccessControlHandler($this->entityTypeId); + + /** @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */ + $entity_storage = $this->entityTypeManager->getStorage($this->entityTypeId); + + // Must have access to the same operation on the default revision. + $default_revision = $entity_storage->load($entity->id()); + $entity_access_default = $default_revision->access($operation, $account); + if (!$entity_access_default) { + return AccessResult::forbidden(); + } + + $map = [ + 'view' => "view all {$this->entityTypeId} revisions", + 'update' => "revert all {$this->entityTypeId} revisions", + 'delete' => "delete all {$this->entityTypeId} revisions", + ]; + $bundle = $entity->bundle(); + $type_map = [ + 'view' => "view {$this->entityTypeId} $bundle revisions", + 'update' => "revert {$this->entityTypeId} $bundle revisions", + 'delete' => "delete {$this->entityTypeId} $bundle revisions", + ]; + + if (!$entity || !isset($map[$operation]) || !isset($type_map[$operation])) { + // If there was no entity to check against, or the $op was not one of the + // supported ones, we return access denied. + return AccessResult::forbidden(); + } + + $admin_permission = $this->entityType->getAdminPermission(); + + // Perform basic permission checks first. + if ($account->hasPermission($map[$operation]) || + $account->hasPermission($type_map[$operation]) || + ($admin_permission && $account->hasPermission($admin_permission))) { + return AccessResult::allowed(); + } + + return AccessResult::forbidden(); + } + /** * {@inheritdoc} */ diff --git a/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php b/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php new file mode 100755 index 0000000..c427b8f --- /dev/null +++ b/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php @@ -0,0 +1,200 @@ +installEntitySchema('user'); + $this->installEntitySchema('apidoc'); + $this->installSchema('system', ['sequences']); + + $this->entityTypeManager = $this->container->get('entity_type.manager'); + $this->entityTypeStorage = $this->entityTypeManager->getStorage('apidoc'); + + // Create a published apidoc. + $apidoc = ApiDoc::create([ + 'name' => 'API 1', + 'description' => 'Test API v1', + 'spec' => NULL, + 'api_product' => NULL, + 'status' => 1, + ]); + $apidoc->save(); + $this->apidocV1Id = $apidoc->getRevisionId(); + + // Create a new revision. + $apidoc->setDescription('Test API v2'); + $apidoc->setRevisionLogMessage('v2'); + $apidoc->setNewRevision(); + $apidoc->save(); + $this->apidocV2Id = $apidoc->getRevisionId(); + + $this->apidoc = $apidoc; + + // Discard user 1, we will not need it because it bypasses access control. + $this->createUser(); + } + + /** + * Test ApiDocs revision access as anonymous. + */ + public function testApiDocRevisionsAccessAnon() { + $entity_v1 = $this->entityTypeStorage->loadRevision($this->apidocV1Id); + + $tests = [ + 'view' => 'Anonymous should not be able to view an unpublished revision.', + 'update' => 'Anonymous should not be able to update a revision.', + ]; + + foreach ($tests as $op => $message) { + $this->assertFalse($entity_v1->access($op), $message); + } + } + + /** + * Test ApiDocs revision access a logged in user. + */ + public function testApiDocRevisionsAccessLoggedIn() { + $user = $this->createUser([]); + \Drupal::currentUser()->setAccount($user); + + $entity_v1 = $this->entityTypeStorage->loadRevision($this->apidocV1Id); + + $tests = [ + 'view' => 'LoggedIn should not be able to view an unpublished revision.', + 'update' => 'LoggedIn should not be able to update a revision.', + ]; + + foreach ($tests as $op => $message) { + $this->assertFalse($entity_v1->access($op, $user), $message); + } + } + + /** + * Test ApiDocs revision access as a logged in user with some permissions. + */ + public function testApiDocRevisionsAccessPermissions() { + $user = $this->createUser([ + 'view published apidoc entities', + 'view unpublished apidoc entities', + 'view all apidoc revisions', + 'edit apidoc entities', + 'revert all apidoc revisions', + ]); + \Drupal::currentUser()->setAccount($user); + + $entity_v1 = $this->entityTypeStorage->loadRevision($this->apidocV1Id); + + $tests = [ + 'view' => 'User should be able to view an unpublished revision.', + 'update' => 'User should be able to update a revision.', + ]; + + foreach ($tests as $op => $message) { + $this->assertTrue($entity_v1->access($op, $user), $message); + } + } + + /** + * Test ApiDocs revision access as a logged in user with admin permissions. + */ + public function testApiDocRevisionsAccessAdmin() { + $user = $this->createUser([ + 'administer apigee api catalog', + ]); + \Drupal::currentUser()->setAccount($user); + + $entity_v1 = $this->entityTypeStorage->loadRevision($this->apidocV1Id); + + $tests = [ + 'view' => 'User should be able to view an unpublished revision.', + 'update' => 'User should be able to update a revision.', + ]; + + foreach ($tests as $op => $message) { + $this->assertTrue($entity_v1->access($op, $user), $message); + } + } + +} diff --git a/tests/src/Kernel/ApidocEntityTest.php b/tests/src/Kernel/ApidocEntityTest.php index 7686e97..633b57d 100755 --- a/tests/src/Kernel/ApidocEntityTest.php +++ b/tests/src/Kernel/ApidocEntityTest.php @@ -18,7 +18,7 @@ * MA 02110-1301, USA. */ -namespace Drupal\Tests\content_entity_example\Kernel; +namespace Drupal\Tests\apigee_api_catalog\Kernel; use Drupal\apigee_api_catalog\Entity\ApiDoc; use Drupal\KernelTests\KernelTestBase; From da963d09d2e6231bbf9905dfb385d9d6adc754aa Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Wed, 27 Mar 2019 23:39:31 -0700 Subject: [PATCH 04/48] DRUP-624 Add ApiDoc entity settings for default revision. --- src/Entity/ApiDoc.php | 19 +++++++---- src/Entity/Form/ApiDocForm.php | 12 +++++++ src/Entity/Form/ApiDocSettingsForm.php | 45 +++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index b83d7c6..e138adb 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -143,13 +143,6 @@ public function getRevisionUser() { return $this->get('revision_user')->entity; } - /** - * {@inheritdoc} - */ - public function save() { - return parent::save(); - } - /** * {@inheritdoc} */ @@ -178,6 +171,18 @@ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $reco } } + /** + * Gets whether a new revision should be created by default. + * + * @return bool + * TRUE if a new revision should be created by default. + */ + public static function shouldCreateNewRevision() { + $config = \Drupal::config('apigee_api_catalog.settings'); + $default_revision = $config->get('default_revision'); + return is_null($default_revision) ? TRUE : (bool) $default_revision; + } + /** * {@inheritdoc} */ diff --git a/src/Entity/Form/ApiDocForm.php b/src/Entity/Form/ApiDocForm.php index d31c490..728df23 100755 --- a/src/Entity/Form/ApiDocForm.php +++ b/src/Entity/Form/ApiDocForm.php @@ -28,6 +28,18 @@ */ class ApiDocForm extends ContentEntityForm { + /** + * {@inheritdoc} + */ + protected function getNewRevisionDefault() { + /* @var \Drupal\apigee_api_catalog\Entity\ApiDoc $entity */ + $entity = $this->getEntity(); + + // Always use the default revision setting. + $new_revision_default = $entity::shouldCreateNewRevision(); + return $new_revision_default; + } + /** * {@inheritdoc} */ diff --git a/src/Entity/Form/ApiDocSettingsForm.php b/src/Entity/Form/ApiDocSettingsForm.php index 78f1d61..dee9c4f 100755 --- a/src/Entity/Form/ApiDocSettingsForm.php +++ b/src/Entity/Form/ApiDocSettingsForm.php @@ -20,13 +20,14 @@ namespace Drupal\apigee_api_catalog\Entity\Form; +use Drupal\apigee_api_catalog\Entity\ApiDoc; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; /** * Class ApiDocSettingsForm. * - * @todo Add settings or remove the class and the route. + * Settings for the ApiDoc entity type. */ class ApiDocSettingsForm extends FormBase { @@ -41,6 +42,18 @@ public function getFormId() { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { + $entity_type = \Drupal::entityTypeManager() + ->getStorage('apidoc') + ->getEntityType(); + $config = $this->configFactory()->getEditable('apigee_api_catalog.settings'); + + $options = $form_state->getValue('options'); + $config->set('default_revision', (bool) $options['new_revision'])->save(); + + $args = [ + '@type' => $entity_type->getLabel(), + ]; + drupal_set_message($this->t('@type settings have been updated.', $args)); } /** @@ -48,6 +61,36 @@ public function submitForm(array &$form, FormStateInterface $form_state) { */ public function buildForm(array $form, FormStateInterface $form_state) { $form['apigee_api_catalog_settings']['#markup'] = $this->t('Settings for Apigee API catalog. Manage field settings using the tabs above.'); + + $form['additional_settings'] = [ + '#type' => 'vertical_tabs', + ]; + + $form['workflow'] = [ + '#type' => 'details', + '#title' => t('Publishing options'), + '#group' => 'additional_settings', + ]; + $workflow_options = [ + 'new_revision' => ApiDoc::shouldCreateNewRevision(), + ]; + // Prepare workflow options to be used for 'checkboxes' form element. + $keys = array_keys(array_filter($workflow_options)); + $workflow_options = array_combine($keys, $keys); + $form['workflow']['options'] = [ + '#type' => 'checkboxes', + '#title' => t('Default options'), + '#default_value' => $workflow_options, + '#options' => [ + 'new_revision' => t('Create new revision'), + ], + ]; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + ]; + return $form; } From 84f586ea800021954c6ef3848d1668353b26826a Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Thu, 28 Mar 2019 13:20:24 -0700 Subject: [PATCH 05/48] DRUP-624 Fix code sniffer validations. --- src/Entity/Access/ApiDocAccessControlHandler.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Entity/Access/ApiDocAccessControlHandler.php b/src/Entity/Access/ApiDocAccessControlHandler.php index 601453d..73a9b4e 100755 --- a/src/Entity/Access/ApiDocAccessControlHandler.php +++ b/src/Entity/Access/ApiDocAccessControlHandler.php @@ -34,6 +34,8 @@ class ApiDocAccessControlHandler extends EntityAccessControlHandler { /** + * The entity type manager. + * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; @@ -82,17 +84,20 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter /** * Additional access control for revisions. * - * @param \Drupal\Core\Entity\EntityInterface $entity - * @param $operation + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which to check access. + * @param string $operation + * The entity operation. * @param \Drupal\Core\Session\AccountInterface $account + * The user for which to check access. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. * - * @return bool * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function checkAccessRevisions(EntityInterface $entity, $operation, AccountInterface $account) { - $entity_access = $this->entityTypeManager->getAccessControlHandler($this->entityTypeId); - /** @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */ $entity_storage = $this->entityTypeManager->getStorage($this->entityTypeId); From fed9ecbb196ef5513151e0a990d80af8dbbce211 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Sat, 6 Apr 2019 12:36:09 -0700 Subject: [PATCH 06/48] DRUP-624 Add revisions link to local tasks. --- apigee_api_catalog.links.task.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apigee_api_catalog.links.task.yml b/apigee_api_catalog.links.task.yml index ef0a668..1c132af 100755 --- a/apigee_api_catalog.links.task.yml +++ b/apigee_api_catalog.links.task.yml @@ -25,3 +25,9 @@ apidoc.admin: route_name: entity.apidoc.collection base_route: system.admin_content weight: 10 + +entity.apidoc.version_history: + route_name: entity.apidoc.version_history + base_route: entity.apidoc.canonical + title: 'Revisions' + weight: 15 From 6c4886cf6f37b92d0348dc263ffeb900ff48fa27 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 9 Apr 2019 17:01:17 -0700 Subject: [PATCH 07/48] DRUP-624 ApiDocAccessControlHandler implements EntityHandlerInterface. --- .../Access/ApiDocAccessControlHandler.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Entity/Access/ApiDocAccessControlHandler.php b/src/Entity/Access/ApiDocAccessControlHandler.php index 73a9b4e..a3a902e 100755 --- a/src/Entity/Access/ApiDocAccessControlHandler.php +++ b/src/Entity/Access/ApiDocAccessControlHandler.php @@ -21,17 +21,20 @@ namespace Drupal\apigee_api_catalog\Entity\Access; use Drupal\Core\Entity\EntityAccessControlHandler; +use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessResult; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Access controller for the API Doc entity. * * @see \Drupal\apigee_api_catalog\Entity\ApiDoc. */ -class ApiDocAccessControlHandler extends EntityAccessControlHandler { +class ApiDocAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface { /** * The entity type manager. @@ -41,12 +44,26 @@ class ApiDocAccessControlHandler extends EntityAccessControlHandler { protected $entityTypeManager; /** - * {@inheritdoc} + * Constructs an access control handler instance. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager. */ - public function __construct(EntityTypeInterface $entity_type) { + public function __construct(EntityTypeInterface $entity_type, EntityTypeManagerInterface $entityTypeManager) { parent::__construct($entity_type); + $this->entityTypeManager = $entityTypeManager; + } - $this->entityTypeManager = \Drupal::entityTypeManager(); + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity_type.manager') + ); } /** From 96a9f9fbfd09d5d2a74f8b695f1ea0dcb12ba80f Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 9 Apr 2019 17:23:30 -0700 Subject: [PATCH 08/48] DRUP-624 Improving code from PR feedback. --- src/Entity/Access/ApiDocAccessControlHandler.php | 7 ++----- src/Entity/ApiDoc.php | 13 +++---------- tests/src/Kernel/ApidocEntityTest.php | 4 ++-- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/Entity/Access/ApiDocAccessControlHandler.php b/src/Entity/Access/ApiDocAccessControlHandler.php index a3a902e..94f088c 100755 --- a/src/Entity/Access/ApiDocAccessControlHandler.php +++ b/src/Entity/Access/ApiDocAccessControlHandler.php @@ -110,9 +110,6 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter * * @return \Drupal\Core\Access\AccessResultInterface * The access result. - * - * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function checkAccessRevisions(EntityInterface $entity, $operation, AccountInterface $account) { /** @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */ @@ -120,8 +117,8 @@ protected function checkAccessRevisions(EntityInterface $entity, $operation, Acc // Must have access to the same operation on the default revision. $default_revision = $entity_storage->load($entity->id()); - $entity_access_default = $default_revision->access($operation, $account); - if (!$entity_access_default) { + $has_default_entity_rev_access = $default_revision->access($operation, $account); + if (!$has_default_entity_rev_access) { return AccessResult::forbidden(); } diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index e138adb..411f3c4 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -136,13 +136,6 @@ public function setCreatedTime(int $timestamp) : ApiDocInterface { return $this; } - /** - * {@inheritdoc} - */ - public function getRevisionUser() { - return $this->get('revision_user')->entity; - } - /** * {@inheritdoc} */ @@ -164,9 +157,9 @@ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $reco if (!$this->isNewRevision() && isset($this->original) && empty($record->revision_log_message)) { // If we are updating an existing entity without adding a new revision, we - // need to make sure $entity->revision_log is reset whenever it is empty. - // Therefore, this code allows us to avoid clobbering an existing log - // entry with an empty one. + // need to make sure $entity->revision_log_message is reset whenever it is + // empty. Therefore, this code allows us to avoid clobbering an existing + // log entry with an empty one. $record->revision_log_message = $this->original->revision_log_message->value; } } diff --git a/tests/src/Kernel/ApidocEntityTest.php b/tests/src/Kernel/ApidocEntityTest.php index 633b57d..77cad49 100755 --- a/tests/src/Kernel/ApidocEntityTest.php +++ b/tests/src/Kernel/ApidocEntityTest.php @@ -108,7 +108,7 @@ public function testRevisions() { // Test saving without a new revision. $entity->setDescription('Test API v3'); $entity->save(); - $this->assertTrue($v2_id === $entity->getRevisionId()); + $this->assertEquals($v2_id, $entity->getRevisionId()); // Test that the revision log message wasn't overriden. $this->assertEquals($new_log, $entity->getRevisionLogMessage()); @@ -124,7 +124,7 @@ public function testRevisions() { // Load and check reverted values. $this->entityTypeManager->getStorage('apidoc')->resetCache(); $reverted = ApiDoc::load($entity->id()); - $this->assertTrue($reverted->getRevisionId() > $v1_id); + $this->assertLessThan($reverted->getRevisionId(), $v1_id); $this->assertTrue($reverted->isDefaultRevision()); $this->assertEquals($description_v1, $reverted->getDescription()); } From 028ec8606bc0689062d72aa825d6e18ea8a7c0fe Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Wed, 10 Apr 2019 12:03:39 -0700 Subject: [PATCH 09/48] DRUP-624 Improving code from PR feedback. --- src/Entity/Form/ApiDocSettingsForm.php | 35 ++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/Entity/Form/ApiDocSettingsForm.php b/src/Entity/Form/ApiDocSettingsForm.php index dee9c4f..a6affd2 100755 --- a/src/Entity/Form/ApiDocSettingsForm.php +++ b/src/Entity/Form/ApiDocSettingsForm.php @@ -21,8 +21,10 @@ namespace Drupal\apigee_api_catalog\Entity\Form; use Drupal\apigee_api_catalog\Entity\ApiDoc; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Class ApiDocSettingsForm. @@ -31,6 +33,29 @@ */ class ApiDocSettingsForm extends FormBase { + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + /** * {@inheritdoc} */ @@ -42,7 +67,7 @@ public function getFormId() { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $entity_type = \Drupal::entityTypeManager() + $entity_type = $this->entityTypeManager ->getStorage('apidoc') ->getEntityType(); $config = $this->configFactory()->getEditable('apigee_api_catalog.settings'); @@ -53,7 +78,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $args = [ '@type' => $entity_type->getLabel(), ]; - drupal_set_message($this->t('@type settings have been updated.', $args)); + $this->messenger($this->t('@type settings have been updated.', $args)); } /** @@ -68,7 +93,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['workflow'] = [ '#type' => 'details', - '#title' => t('Publishing options'), + '#title' => $this->t('Publishing options'), '#group' => 'additional_settings', ]; $workflow_options = [ @@ -79,10 +104,10 @@ public function buildForm(array $form, FormStateInterface $form_state) { $workflow_options = array_combine($keys, $keys); $form['workflow']['options'] = [ '#type' => 'checkboxes', - '#title' => t('Default options'), + '#title' => $this->t('Default options'), '#default_value' => $workflow_options, '#options' => [ - 'new_revision' => t('Create new revision'), + 'new_revision' => $this->t('Create new revision'), ], ]; From a2f1bc83c11d584060cef59cfc91ad97f51d8796 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Wed, 10 Apr 2019 12:29:56 -0700 Subject: [PATCH 10/48] DRUP-624 Improving tests for revisioning. --- tests/src/Kernel/ApidocEntityRevisionsAccessTest.php | 8 ++++---- tests/src/Kernel/ApidocEntityTest.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php b/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php index c427b8f..df3ce7b 100755 --- a/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php +++ b/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php @@ -93,7 +93,7 @@ protected function setUp() { $this->entityTypeStorage = $this->entityTypeManager->getStorage('apidoc'); // Create a published apidoc. - $apidoc = ApiDoc::create([ + $apidoc = $this->entityTypeStorage->create([ 'name' => 'API 1', 'description' => 'Test API v1', 'spec' => NULL, @@ -137,7 +137,7 @@ public function testApiDocRevisionsAccessAnon() { */ public function testApiDocRevisionsAccessLoggedIn() { $user = $this->createUser([]); - \Drupal::currentUser()->setAccount($user); + $this->container->get('account_switcher')->switchTo($user); $entity_v1 = $this->entityTypeStorage->loadRevision($this->apidocV1Id); @@ -162,7 +162,7 @@ public function testApiDocRevisionsAccessPermissions() { 'edit apidoc entities', 'revert all apidoc revisions', ]); - \Drupal::currentUser()->setAccount($user); + $this->container->get('account_switcher')->switchTo($user); $entity_v1 = $this->entityTypeStorage->loadRevision($this->apidocV1Id); @@ -183,7 +183,7 @@ public function testApiDocRevisionsAccessAdmin() { $user = $this->createUser([ 'administer apigee api catalog', ]); - \Drupal::currentUser()->setAccount($user); + $this->container->get('account_switcher')->switchTo($user); $entity_v1 = $this->entityTypeStorage->loadRevision($this->apidocV1Id); diff --git a/tests/src/Kernel/ApidocEntityTest.php b/tests/src/Kernel/ApidocEntityTest.php index 77cad49..5ab2218 100755 --- a/tests/src/Kernel/ApidocEntityTest.php +++ b/tests/src/Kernel/ApidocEntityTest.php @@ -103,7 +103,7 @@ public function testRevisions() { $entity->setRevisionLogMessage($new_log); $entity->save(); $v2_id = $entity->getRevisionId(); - $this->assertTrue($v2_id > $v1_id); + $this->assertLessThan($v2_id, $v1_id); // Test saving without a new revision. $entity->setDescription('Test API v3'); From d938e71136e6015da49c3b9c3130abee23d38ea0 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Wed, 10 Apr 2019 12:46:52 -0700 Subject: [PATCH 11/48] DRUP-624 Fix settings form message for apidocs. --- src/Entity/Form/ApiDocSettingsForm.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Entity/Form/ApiDocSettingsForm.php b/src/Entity/Form/ApiDocSettingsForm.php index a6affd2..2d9adaf 100755 --- a/src/Entity/Form/ApiDocSettingsForm.php +++ b/src/Entity/Form/ApiDocSettingsForm.php @@ -75,10 +75,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $options = $form_state->getValue('options'); $config->set('default_revision', (bool) $options['new_revision'])->save(); - $args = [ + $this->messenger()->addStatus($this->t('@type settings have been updated.', [ '@type' => $entity_type->getLabel(), - ]; - $this->messenger($this->t('@type settings have been updated.', $args)); + ])); } /** From 9a33b38c1cc46206649aa14051c258fc649f6c30 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Thu, 11 Apr 2019 00:21:35 -0700 Subject: [PATCH 12/48] DRUP-624 Improve ApiDocSettingsForm. --- src/Entity/ApiDoc.php | 2 +- src/Entity/ApiDocInterface.php | 3 ++- src/Entity/Form/ApiDocForm.php | 2 +- src/Entity/Form/ApiDocSettingsForm.php | 9 +++++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 411f3c4..f615ac3 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -170,7 +170,7 @@ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $reco * @return bool * TRUE if a new revision should be created by default. */ - public static function shouldCreateNewRevision() { + public function shouldCreateNewRevision() { $config = \Drupal::config('apigee_api_catalog.settings'); $default_revision = $config->get('default_revision'); return is_null($default_revision) ? TRUE : (bool) $default_revision; diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 740e061..eb40952 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -22,11 +22,12 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityChangedInterface; +use Drupal\Core\Entity\EntityPublishedInterface; /** * Provides an interface for defining API Doc entities. */ -interface ApiDocInterface extends ContentEntityInterface, EntityChangedInterface { +interface ApiDocInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface { /** * Gets the API Doc name. diff --git a/src/Entity/Form/ApiDocForm.php b/src/Entity/Form/ApiDocForm.php index 728df23..d911471 100755 --- a/src/Entity/Form/ApiDocForm.php +++ b/src/Entity/Form/ApiDocForm.php @@ -36,7 +36,7 @@ protected function getNewRevisionDefault() { $entity = $this->getEntity(); // Always use the default revision setting. - $new_revision_default = $entity::shouldCreateNewRevision(); + $new_revision_default = $entity->shouldCreateNewRevision(); return $new_revision_default; } diff --git a/src/Entity/Form/ApiDocSettingsForm.php b/src/Entity/Form/ApiDocSettingsForm.php index 2d9adaf..e57ce52 100755 --- a/src/Entity/Form/ApiDocSettingsForm.php +++ b/src/Entity/Form/ApiDocSettingsForm.php @@ -20,7 +20,6 @@ namespace Drupal\apigee_api_catalog\Entity\Form; -use Drupal\apigee_api_catalog\Entity\ApiDoc; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; @@ -84,6 +83,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + /* @var \Drupal\apigee_api_catalog\Entity\ApiDoc $entity */ + $entity = $this->entityTypeManager->getStorage('apidoc')->create(); $form['apigee_api_catalog_settings']['#markup'] = $this->t('Settings for Apigee API catalog. Manage field settings using the tabs above.'); $form['additional_settings'] = [ @@ -96,11 +97,11 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#group' => 'additional_settings', ]; $workflow_options = [ - 'new_revision' => ApiDoc::shouldCreateNewRevision(), + 'new_revision' => $entity->shouldCreateNewRevision(), ]; // Prepare workflow options to be used for 'checkboxes' form element. - $keys = array_keys(array_filter($workflow_options)); - $workflow_options = array_combine($keys, $keys); + $workflow_options_keys = array_keys(array_filter($workflow_options)); + $workflow_options = array_combine($workflow_options_keys, $workflow_options_keys); $form['workflow']['options'] = [ '#type' => 'checkboxes', '#title' => $this->t('Default options'), From 993225a6f99db5af83b1734bf79b300a24ae56ae Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Fri, 24 May 2019 16:02:35 -0700 Subject: [PATCH 13/48] [DRUP-624] Remove the delete permission for `apidoc` revisions. --- apigee_api_catalog.permissions.yml | 4 ++-- src/Entity/Access/ApiDocAccessControlHandler.php | 14 +++----------- .../src/Kernel/ApidocEntityRevisionsAccessTest.php | 4 ++-- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/apigee_api_catalog.permissions.yml b/apigee_api_catalog.permissions.yml index 671e211..e963d8f 100755 --- a/apigee_api_catalog.permissions.yml +++ b/apigee_api_catalog.permissions.yml @@ -18,10 +18,10 @@ view published apidoc entities: view unpublished apidoc entities: title: 'View unpublished API docs' -view all apidoc revisions: +view apidoc revisions: title: 'View API Docs revisions' description: 'To view a revision, you also need permission to view the item.' -revert all apidoc revisions: +revert apidoc revisions: title: 'Revert API Docs revisions' description: 'To revert a revision, you also need permission to edit the item.' diff --git a/src/Entity/Access/ApiDocAccessControlHandler.php b/src/Entity/Access/ApiDocAccessControlHandler.php index 94f088c..1e8dba5 100755 --- a/src/Entity/Access/ApiDocAccessControlHandler.php +++ b/src/Entity/Access/ApiDocAccessControlHandler.php @@ -123,18 +123,11 @@ protected function checkAccessRevisions(EntityInterface $entity, $operation, Acc } $map = [ - 'view' => "view all {$this->entityTypeId} revisions", - 'update' => "revert all {$this->entityTypeId} revisions", - 'delete' => "delete all {$this->entityTypeId} revisions", - ]; - $bundle = $entity->bundle(); - $type_map = [ - 'view' => "view {$this->entityTypeId} $bundle revisions", - 'update' => "revert {$this->entityTypeId} $bundle revisions", - 'delete' => "delete {$this->entityTypeId} $bundle revisions", + 'view' => "view apidoc revisions", + 'update' => "revert apidoc revisions", ]; - if (!$entity || !isset($map[$operation]) || !isset($type_map[$operation])) { + if (!$entity || !isset($map[$operation])) { // If there was no entity to check against, or the $op was not one of the // supported ones, we return access denied. return AccessResult::forbidden(); @@ -144,7 +137,6 @@ protected function checkAccessRevisions(EntityInterface $entity, $operation, Acc // Perform basic permission checks first. if ($account->hasPermission($map[$operation]) || - $account->hasPermission($type_map[$operation]) || ($admin_permission && $account->hasPermission($admin_permission))) { return AccessResult::allowed(); } diff --git a/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php b/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php index df3ce7b..e7a9ea7 100755 --- a/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php +++ b/tests/src/Kernel/ApidocEntityRevisionsAccessTest.php @@ -158,9 +158,9 @@ public function testApiDocRevisionsAccessPermissions() { $user = $this->createUser([ 'view published apidoc entities', 'view unpublished apidoc entities', - 'view all apidoc revisions', + 'view apidoc revisions', 'edit apidoc entities', - 'revert all apidoc revisions', + 'revert apidoc revisions', ]); $this->container->get('account_switcher')->switchTo($user); From c3944fd26e80c172c9a172c57186f869f39e4c05 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Fri, 24 May 2019 21:51:12 -0700 Subject: [PATCH 14/48] Update apigee_api_catalog.permissions.yml Co-Authored-By: Chris Novak --- apigee_api_catalog.permissions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigee_api_catalog.permissions.yml b/apigee_api_catalog.permissions.yml index e963d8f..d8159b5 100755 --- a/apigee_api_catalog.permissions.yml +++ b/apigee_api_catalog.permissions.yml @@ -19,7 +19,7 @@ view unpublished apidoc entities: title: 'View unpublished API docs' view apidoc revisions: - title: 'View API Docs revisions' + title: 'View API Doc revisions' description: 'To view a revision, you also need permission to view the item.' revert apidoc revisions: From e45bfa15ef83aea3b6357b572dfa0b50b06fc936 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Fri, 24 May 2019 21:51:22 -0700 Subject: [PATCH 15/48] Update apigee_api_catalog.permissions.yml Co-Authored-By: Chris Novak --- apigee_api_catalog.permissions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigee_api_catalog.permissions.yml b/apigee_api_catalog.permissions.yml index d8159b5..043e71d 100755 --- a/apigee_api_catalog.permissions.yml +++ b/apigee_api_catalog.permissions.yml @@ -23,5 +23,5 @@ view apidoc revisions: description: 'To view a revision, you also need permission to view the item.' revert apidoc revisions: - title: 'Revert API Docs revisions' + title: 'Revert API Doc revisions' description: 'To revert a revision, you also need permission to edit the item.' From ca65b24ce78a6d6bd1925b0b5e86727ec74b4aa7 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Fri, 24 May 2019 22:18:36 -0700 Subject: [PATCH 16/48] [DRUP-624] Translate base field labels. --- src/Entity/ApiDoc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index f615ac3..224ef55 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -221,8 +221,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDisplayConfigurable('form', TRUE); $fields['spec'] = BaseFieldDefinition::create('file') - ->setLabel('OpenAPI specification') - ->setDescription('The spec snapshot.') + ->setLabel(t('OpenAPI specification')) + ->setDescription(t('The spec snapshot.')) ->setRevisionable(TRUE) ->setSettings([ 'file_directory' => 'apidoc_specs', From f7005aae0bec20ed0f807f501cbe5c88f848ae20 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Fri, 24 May 2019 22:31:37 -0700 Subject: [PATCH 17/48] =?UTF-8?q?[DRUP-624]=20There=20shouldn=E2=80=99t=20?= =?UTF-8?q?be=20a=20need=20to=20specify=20the=20default=20storage=20handle?= =?UTF-8?q?r.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Entity/ApiDoc.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 224ef55..86f3813 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -36,8 +36,6 @@ * label_singular = @Translation("API Doc"), * label_plural = @Translation("API Docs"), * handlers = { - * - * "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\apigee_api_catalog\Entity\ListBuilder\ApiDocListBuilder", * "views_data" = "Drupal\views\EntityViewsData", From 9eb78cc18e86d24d885bbd3e62dda010298c1209 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Fri, 3 May 2019 17:17:24 -0700 Subject: [PATCH 18/48] DRUP-610 Add url as source for apidocs spec file. --- apigee_api_catalog.info.yml | 1 + src/Entity/ApiDoc.php | 77 +++++++++++++++++++++++++++++++++- src/Entity/ApiDocInterface.php | 8 ++++ src/Entity/Form/ApiDocForm.php | 38 +++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/apigee_api_catalog.info.yml b/apigee_api_catalog.info.yml index d234069..95bb1b6 100644 --- a/apigee_api_catalog.info.yml +++ b/apigee_api_catalog.info.yml @@ -7,4 +7,5 @@ configure: entity.apidoc.settings dependencies: - drupal:text - drupal:file +- drupal:file_link - apigee_edge:apigee_edge diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 26194c8..e8d4413 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -20,10 +20,12 @@ namespace Drupal\apigee_api_catalog\Entity; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\link\LinkItemInterface; /** * Defines the API Doc entity. @@ -134,6 +136,18 @@ public function setPublished(bool $published) : ApiDocInterface { return $this; } + /** + * {@inheritdoc} + */ + public function getSpecAsFile() : bool { + // Default is to use spec as file. + if ($this->get('spec_as_file')->isEmpty()) { + return TRUE; + } + + return (bool) $this->get('spec_as_file')->getValue()[0]['value']; + } + /** * {@inheritdoc} */ @@ -176,8 +190,23 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ]) ->setDisplayConfigurable('form', TRUE); + $fields['spec_as_file'] = BaseFieldDefinition::create('boolean') + ->setLabel(t('Provide OpenAPI specification as a file')) + ->setDescription(t('Indicate if the OpenAPI spec will be provided as a + file (or a URL otherwise).')) + ->setDefaultValue(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'boolean_checkbox', + 'weight' => 0, + 'settings' => [ + 'display_label' => TRUE, + ], + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + $fields['spec'] = BaseFieldDefinition::create('file') - ->setLabel('OpenAPI specification') + ->setLabel('OpenAPI specification file') ->setDescription('The spec snapshot.') ->setSettings([ 'file_directory' => 'apidoc_specs', @@ -200,6 +229,20 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDisplayConfigurable('form', FALSE) ->setDisplayConfigurable('view', FALSE); + $fields['file_link'] = BaseFieldDefinition::create('file_link') + ->setLabel(t('URL to OpenAPI specification file')) + ->setDescription(t('The URL to an OpenAPI file spec.')) + ->setSettings([ + 'file_extensions' => 'yml yaml json', + 'link_type' => LinkItemInterface::LINK_GENERIC, + 'title' => DRUPAL_DISABLED, + ]) + ->setDisplayOptions('form', [ + 'weight' => 0, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + $fields['api_product'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('API Product')) ->setDescription(t('The API Product this API is associated to.')) @@ -232,4 +275,36 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { return $fields; } + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // If "spec_as_file", grab file from "file_link" and save it into the + // "spec" file field. The file_link field should already have validated that + // a valid file exists at that URL. + + if (!$this->getSpecAsFile()) { + $file_uri = $this->get('file_link')->getValue()[0]['uri']; + $data = file_get_contents($file_uri); + if (!$data) { + throw new \LogicException( + 'File could not be retrieved at specified URL.' + ); + } + + $filename = \Drupal::service('file_system')->basename($file_uri); + $specs_definition = $this->getFieldDefinition('spec')->getItemDefinition(); + $target_dir = $specs_definition->getSetting('file_directory'); + $uri_scheme = $specs_definition->getSetting('uri_scheme'); + $file = file_save_data($data, "$uri_scheme://$target_dir/$filename", FILE_EXISTS_RENAME); + $spec_value = [ + 'target_id' => $file->id(), + ] + $this->get('spec')->getValue()[0]; + $this->set('spec', $spec_value); + } + + } + } diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 0157fff..423bbef 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -106,4 +106,12 @@ public function isPublished() : bool; */ public function setPublished(bool $published) : self; + /** + * Indicates if OpenAPI specs will be provided as a file (otherwise a Url). + * + * @return bool + * TRUE if specs will be provided as a file (otherwise a Url). + */ + public function getSpecAsFile() : bool; + } diff --git a/src/Entity/Form/ApiDocForm.php b/src/Entity/Form/ApiDocForm.php index d31c490..a3c73e1 100755 --- a/src/Entity/Form/ApiDocForm.php +++ b/src/Entity/Form/ApiDocForm.php @@ -28,6 +28,44 @@ */ class ApiDocForm extends ContentEntityForm { + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + $form['specifications'] = [ + '#type' => 'fieldset', + '#title' => $this->t('OpenAPI Specifications File'), + '#weight' => $form['spec_as_file']['#weight'], + ]; + + $form['spec']['#states'] = [ + 'visible' => [ + ':input[name="spec_as_file[value]"]' => ['checked' => TRUE], + ], + 'required' => [ + ':input[name="spec_as_file[value]"]' => ['checked' => TRUE], + ], + ]; + $form['file_link']['#states'] = [ + 'visible' => [ + ':input[name="spec_as_file[value]"]' => ['checked' => FALSE], + ], + 'required' => [ + ':input[name="spec_as_file[value]"]' => ['checked' => FALSE], + ], + ]; + + $form['specifications']['spec_as_file'] = $form['spec_as_file']; + $form['specifications']['spec'] = $form['spec']; + $form['specifications']['file_link'] = $form['file_link']; + unset($form['spec_as_file'], $form['spec'], $form['file_link']); + + return $form; + } + /** * {@inheritdoc} */ From 971ee81c61cede8c044798fadfcef45dcde18604 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Fri, 3 May 2019 23:42:37 -0700 Subject: [PATCH 19/48] DRUP-610 Add url as source for apidocs spec file - avoid resaving same remote file. --- src/Entity/ApiDoc.php | 46 +++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index e8d4413..b16eec2 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -243,6 +243,15 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE); + $fields['spec_md5'] = BaseFieldDefinition::create('string') + ->setLabel(t('OpenAPI specification file MD5')) + ->setDescription(t('OpenAPI specification file MD5')) + ->setSettings([ + 'text_processing' => 0, + ]) + ->setDisplayConfigurable('form', FALSE) + ->setDisplayConfigurable('view', FALSE); + $fields['api_product'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('API Product')) ->setDescription(t('The API Product this API is associated to.')) @@ -285,24 +294,41 @@ public function preSave(EntityStorageInterface $storage) { // "spec" file field. The file_link field should already have validated that // a valid file exists at that URL. + $spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0]; + if (!$this->getSpecAsFile()) { $file_uri = $this->get('file_link')->getValue()[0]['uri']; $data = file_get_contents($file_uri); - if (!$data) { + if (empty($data)) { throw new \LogicException( 'File could not be retrieved at specified URL.' ); } - $filename = \Drupal::service('file_system')->basename($file_uri); - $specs_definition = $this->getFieldDefinition('spec')->getItemDefinition(); - $target_dir = $specs_definition->getSetting('file_directory'); - $uri_scheme = $specs_definition->getSetting('uri_scheme'); - $file = file_save_data($data, "$uri_scheme://$target_dir/$filename", FILE_EXISTS_RENAME); - $spec_value = [ - 'target_id' => $file->id(), - ] + $this->get('spec')->getValue()[0]; - $this->set('spec', $spec_value); + // Only save file if it hasn't been fetched previously. + $data_md5 = md5($data); + $prev_md5 = $this->get('spec_md5')->isEmpty() ? NULL : $this->get('spec_md5')->getValue()[0]['value']; + if ($prev_md5 != $data_md5) { + $filename = \Drupal::service('file_system')->basename($file_uri); + $specs_definition = $this->getFieldDefinition('spec')->getItemDefinition(); + $target_dir = $specs_definition->getSetting('file_directory'); + $uri_scheme = $specs_definition->getSetting('uri_scheme'); + $file = file_save_data($data, "$uri_scheme://$target_dir/$filename", FILE_EXISTS_RENAME); + $spec_value = [ + 'target_id' => $file->id(), + ] + $spec_value; + $this->set('spec', $spec_value); + $this->set('spec_md5', $data_md5); + } + } + elseif (!empty($spec_value['target_id'])) { + /* @var \Drupal\file\Entity\File $file */ + $file = \Drupal::entityTypeManager() + ->getStorage('file') + ->load($spec_value['target_id']); + if ($file) { + $this->set('spec_md5', md5_file($file->getFileUri())); + } } } From c5ff9e1192a9ce6e76173a5f56bb0d3d53cc55f5 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Sat, 4 May 2019 00:43:09 -0700 Subject: [PATCH 20/48] DRUP-610 Add url as source for apidocs spec file - constraint to check for valid remote file. --- src/Entity/ApiDoc.php | 13 +++- .../Constraint/ApiDocFileLinkConstraint.php | 41 ++++++++++ .../ApiDocFileLinkConstraintValidator.php | 78 +++++++++++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php create mode 100644 src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index b16eec2..bede29d 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -25,6 +25,7 @@ use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\link\LinkItemInterface; /** @@ -75,6 +76,7 @@ class ApiDoc extends ContentEntityBase implements ApiDocInterface { use EntityChangedTrait; + use StringTranslationTrait; /** * {@inheritdoc} @@ -232,6 +234,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['file_link'] = BaseFieldDefinition::create('file_link') ->setLabel(t('URL to OpenAPI specification file')) ->setDescription(t('The URL to an OpenAPI file spec.')) + ->addConstraint('ApiDocFileLink') ->setSettings([ 'file_extensions' => 'yml yaml json', 'link_type' => LinkItemInterface::LINK_GENERIC, @@ -300,9 +303,11 @@ public function preSave(EntityStorageInterface $storage) { $file_uri = $this->get('file_link')->getValue()[0]['uri']; $data = file_get_contents($file_uri); if (empty($data)) { - throw new \LogicException( - 'File could not be retrieved at specified URL.' - ); + \Drupal::messenger() + ->addMessage($this->t('Could not retrieve OpenAPI specifications file located at %url', [ + '%url' => $file_uri, + ]), 'error'); + return; } // Only save file if it hasn't been fetched previously. @@ -321,11 +326,13 @@ public function preSave(EntityStorageInterface $storage) { $this->set('spec_md5', $data_md5); } } + elseif (!empty($spec_value['target_id'])) { /* @var \Drupal\file\Entity\File $file */ $file = \Drupal::entityTypeManager() ->getStorage('file') ->load($spec_value['target_id']); + if ($file) { $this->set('spec_md5', md5_file($file->getFileUri())); } diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php new file mode 100644 index 0000000..ce0c30a --- /dev/null +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php @@ -0,0 +1,41 @@ +isEmpty()) { + continue; + } + + $uri = $item->getValue()['uri']; + + // Try to resolve the given URI to a URL. It may fail if it's schemeless. + try { + $url = Url::fromUri($uri, ['absolute' => TRUE])->toString(); + } + catch (\InvalidArgumentException $e) { + $this->context->addViolation("The following error occurred while getting the link URL: @error", ['@error' => $e->getMessage()]); + return; + } + + try { + $options = [ + 'exceptions' => TRUE, + 'allow_redirects' => [ + 'strict' => TRUE, + ], + ]; + + // Perform only a HEAD method to save bandwidth. + /* @var $response ResponseInterface */ + $response = \Drupal::httpClient()->head($url, $options); + } + catch (RequestException $request_exception) { + $this->context->addViolation($constraint->notValid, [ + '%value' => $url, + ]); + } + } + } +} From ca81fc32f51a4dd2a340189de9adde3c06247677 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Sat, 4 May 2019 13:19:43 -0700 Subject: [PATCH 21/48] DRUP-459 Add ApiDoc operation to re-fetch OpenAPI specification file from remote URL. --- apigee_api_catalog.links.task.yml | 6 ++ src/Entity/ApiDoc.php | 39 +++++++++- src/Entity/ApiDocInterface.php | 22 ++++++ src/Entity/ListBuilder/ApiDocListBuilder.php | 18 +++++ .../Routing/ApiDocHtmlRouteProvider.php | 41 ++++++++++ src/Form/ApiDocUpdateSpecForm.php | 76 +++++++++++++++++++ .../Constraint/ApiDocFileLinkConstraint.php | 2 +- .../ApiDocFileLinkConstraintValidator.php | 2 +- 8 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 src/Form/ApiDocUpdateSpecForm.php diff --git a/apigee_api_catalog.links.task.yml b/apigee_api_catalog.links.task.yml index ef0a668..518df81 100755 --- a/apigee_api_catalog.links.task.yml +++ b/apigee_api_catalog.links.task.yml @@ -25,3 +25,9 @@ apidoc.admin: route_name: entity.apidoc.collection base_route: system.admin_content weight: 10 + +entity.apidoc.update_spec_form: + route_name: entity.apidoc.update_spec_form + base_route: entity.apidoc.canonical + title: 'Update OpenAPI spec' + weight: 15 diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index bede29d..8cbfdd1 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -68,6 +68,7 @@ * "add-form" = "/admin/content/api/add", * "edit-form" = "/admin/content/api/{apidoc}/edit", * "delete-form" = "/admin/content/api/{apidoc}/delete", + * "update-spec-form" = "/admin/content/api/{apidoc}/update", * "collection" = "/admin/content/apis", * }, * field_ui_base_route = "entity.apidoc.settings" @@ -150,6 +151,14 @@ public function getSpecAsFile() : bool { return (bool) $this->get('spec_as_file')->getValue()[0]['value']; } + /** + * {@inheritdoc} + */ + public function isRevisionable() : bool { + // Entity types are revisionable if a revision key has been specified. + return (bool) $this->getEntityKey('revision'); + } + /** * {@inheritdoc} */ @@ -293,6 +302,15 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { public function preSave(EntityStorageInterface $storage) { parent::preSave($storage); + $this->updateOpenApiSpecFile(FALSE, FALSE); + } + + /** + * {@inheritdoc} + */ + public function updateOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { + $needs_save = FALSE; + // If "spec_as_file", grab file from "file_link" and save it into the // "spec" file field. The file_link field should already have validated that // a valid file exists at that URL. @@ -307,7 +325,7 @@ public function preSave(EntityStorageInterface $storage) { ->addMessage($this->t('Could not retrieve OpenAPI specifications file located at %url', [ '%url' => $file_uri, ]), 'error'); - return; + return FALSE; } // Only save file if it hasn't been fetched previously. @@ -324,6 +342,8 @@ public function preSave(EntityStorageInterface $storage) { ] + $spec_value; $this->set('spec', $spec_value); $this->set('spec_md5', $data_md5); + + $needs_save = TRUE; } } @@ -334,10 +354,25 @@ public function preSave(EntityStorageInterface $storage) { ->load($spec_value['target_id']); if ($file) { - $this->set('spec_md5', md5_file($file->getFileUri())); + $prev_md5 = $this->get('spec_md5')->isEmpty() ? NULL : $this->get('spec_md5')->getValue()[0]['value']; + $file_md5 = md5_file($file->getFileUri()); + if ($prev_md5 != $file_md5) { + $this->set('spec_md5', $file_md5); + + $needs_save = TRUE; + } + } + } + + // Only save if changes were made. + if ($save && $needs_save) { + if ($new_revision && $this->isRevisionable()) { + $this->setNewRevision(); } + $this->save(); } + return TRUE; } } diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 423bbef..03eefa7 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -114,4 +114,26 @@ public function setPublished(bool $published) : self; */ public function getSpecAsFile() : bool; + /** + * Check if entity is revisionable. + * + * @return bool + * TRUE if entity is revisionable. + */ + public function isRevisionable() : bool; + + /** + * Update OpenAPI specifications file from URL. + * + * @param bool $save + * Boolean indicating if method should save the entity. + * @param bool $new_revision + * Boolean indicating if method should create a new revision when saving + * the entity. + * + * @return bool + * Returned TRUE if the operation completed without errors. + */ + public function updateOpenApiSpecFile($save = TRUE, $new_revision = TRUE); + } diff --git a/src/Entity/ListBuilder/ApiDocListBuilder.php b/src/Entity/ListBuilder/ApiDocListBuilder.php index 5b75315..6d46bf6 100755 --- a/src/Entity/ListBuilder/ApiDocListBuilder.php +++ b/src/Entity/ListBuilder/ApiDocListBuilder.php @@ -82,4 +82,22 @@ public function buildRow(EntityInterface $entity) { return $row + parent::buildRow($entity); } + /** + * {@inheritdoc} + */ + public function getOperations(EntityInterface $entity) { + $operations = parent::getOperations($entity); + + // Add "Update OpenAPI spec" link. + if ($entity->access('update') && $entity->hasLinkTemplate('update-spec-form')) { + $operations['update_spec'] = [ + 'title' => $this->t('Update OpenAPI spec'), + 'weight' => 15, + 'url' => $this->ensureDestination($entity->toUrl('update-spec-form')), + ]; + } + + return $operations; + } + } diff --git a/src/Entity/Routing/ApiDocHtmlRouteProvider.php b/src/Entity/Routing/ApiDocHtmlRouteProvider.php index aa3d591..08462e1 100755 --- a/src/Entity/Routing/ApiDocHtmlRouteProvider.php +++ b/src/Entity/Routing/ApiDocHtmlRouteProvider.php @@ -41,6 +41,7 @@ class ApiDocHtmlRouteProvider extends AdminHtmlRouteProvider { */ public function getRoutes(EntityTypeInterface $entity_type) { $collection = parent::getRoutes($entity_type); + $entity_type_id = $entity_type->id(); if ($settings_form_route = $this->getSettingsFormRoute($entity_type)) { $collection->add('entity.apidoc.settings', $settings_form_route); @@ -50,6 +51,10 @@ public function getRoutes(EntityTypeInterface $entity_type) { $apidoc_collection_route->setDefault('_title', $this->t('@entity_type catalog', ['@entity_type' => $entity_type->getLabel()])->render()); } + if ($update_spec_route = $this->getUpdateSpecFormRoute($entity_type)) { + $collection->add("entity.{$entity_type_id}.update_spec_form", $update_spec_route); + } + return $collection; } @@ -77,4 +82,40 @@ protected function getSettingsFormRoute(EntityTypeInterface $entity_type) { } } + /** + * Gets the update-spec-form route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function getUpdateSpecFormRoute(EntityTypeInterface $entity_type) { + if ($entity_type->hasLinkTemplate('update-spec-form')) { + $entity_type_id = $entity_type->id(); + $route = new Route($entity_type->getLinkTemplate('update-spec-form')); + // Use the update_spec form handler. + if ($entity_type->getFormClass('update_spec')) { + $operation = 'update_spec'; + } + $route + ->setDefaults([ + '_entity_form' => "{$entity_type_id}.{$operation}", + '_title' => 'Update API Doc OpenAPI specification', + ]) + ->setRequirement('_entity_access', "{$entity_type_id}.update") + ->setOption('parameters', [ + $entity_type_id => ['type' => 'entity:' . $entity_type_id], + ]); + + // Entity types with serial IDs can specify this in their route + // requirements, improving the matching process. + if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') { + $route->setRequirement($entity_type_id, '\d+'); + } + return $route; + } + } + } diff --git a/src/Form/ApiDocUpdateSpecForm.php b/src/Form/ApiDocUpdateSpecForm.php new file mode 100644 index 0000000..decf2fa --- /dev/null +++ b/src/Form/ApiDocUpdateSpecForm.php @@ -0,0 +1,76 @@ +t('Are you sure you want to update the OpenAPI specification + file from URL on API Doc %name?', [ + '%name' => $this->entity->label(), + ]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.apidoc.collection'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This will replace the current OpenAPI specification file. + This action cannot be undone.'); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + /* @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ + $entity = $this->getEntity(); + $status = $entity->updateOpenApiSpecFile(TRUE, TRUE); + + if ($status) { + $this->messenger()->addStatus($this->t('API Doc %label: updated the OpenAPI + specification file from URL.', [ + '%label' => $this->entity->label(), + ])); + } + else { + $this->messenger()->addError($this->t('API Doc %label: could not update + the OpenAPI specification file from URL.', [ + '%label' => $this->entity->label(), + ])); + } + } + +} diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php index ce0c30a..3d0e310 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php @@ -18,7 +18,7 @@ * MA 02110-1301, USA. */ -namespace Drupal\apigee_edge_apidocs\Plugin\Validation\Constraint; +namespace Drupal\apigee_api_catalog\Plugin\Validation\Constraint; use Symfony\Component\Validator\Constraint; diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php index f375c1f..86066e8 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php @@ -18,7 +18,7 @@ * MA 02110-1301, USA. */ -namespace Drupal\apigee_edge_apidocs\Plugin\Validation\Constraint; +namespace Drupal\apigee_api_catalog\Plugin\Validation\Constraint; use Drupal\Core\Url; use GuzzleHttp\Exception\RequestException; From e24316dd106b48812e972a4b270d3b5907edafdc Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 12:20:52 -0700 Subject: [PATCH 22/48] DRUP-610 Add url as source for apidocs spec file - renaming operation "reimport". --- apigee_api_catalog.links.task.yml | 6 +++--- src/Entity/ApiDoc.php | 6 +++--- src/Entity/ApiDocInterface.php | 4 ++-- src/Entity/ListBuilder/ApiDocListBuilder.php | 10 +++++----- src/Entity/Routing/ApiDocHtmlRouteProvider.php | 18 +++++++++--------- src/Form/ApiDocUpdateSpecForm.php | 10 +++++----- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/apigee_api_catalog.links.task.yml b/apigee_api_catalog.links.task.yml index 518df81..6f1921e 100755 --- a/apigee_api_catalog.links.task.yml +++ b/apigee_api_catalog.links.task.yml @@ -26,8 +26,8 @@ apidoc.admin: base_route: system.admin_content weight: 10 -entity.apidoc.update_spec_form: - route_name: entity.apidoc.update_spec_form +entity.apidoc.reimport_spec_form: + route_name: entity.apidoc.reimport_spec_form base_route: entity.apidoc.canonical - title: 'Update OpenAPI spec' + title: 'Re-import OpenAPI spec' weight: 15 diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 8cbfdd1..a98edd7 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -68,7 +68,7 @@ * "add-form" = "/admin/content/api/add", * "edit-form" = "/admin/content/api/{apidoc}/edit", * "delete-form" = "/admin/content/api/{apidoc}/delete", - * "update-spec-form" = "/admin/content/api/{apidoc}/update", + * "reimport-spec-form" = "/admin/content/api/{apidoc}/reimport", * "collection" = "/admin/content/apis", * }, * field_ui_base_route = "entity.apidoc.settings" @@ -302,13 +302,13 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { public function preSave(EntityStorageInterface $storage) { parent::preSave($storage); - $this->updateOpenApiSpecFile(FALSE, FALSE); + $this->reimportOpenApiSpecFile(FALSE, FALSE); } /** * {@inheritdoc} */ - public function updateOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { + public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { $needs_save = FALSE; // If "spec_as_file", grab file from "file_link" and save it into the diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 03eefa7..5b11562 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -123,7 +123,7 @@ public function getSpecAsFile() : bool; public function isRevisionable() : bool; /** - * Update OpenAPI specifications file from URL. + * Re-import OpenAPI specifications file from URL. * * @param bool $save * Boolean indicating if method should save the entity. @@ -134,6 +134,6 @@ public function isRevisionable() : bool; * @return bool * Returned TRUE if the operation completed without errors. */ - public function updateOpenApiSpecFile($save = TRUE, $new_revision = TRUE); + public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE); } diff --git a/src/Entity/ListBuilder/ApiDocListBuilder.php b/src/Entity/ListBuilder/ApiDocListBuilder.php index 6d46bf6..537dab7 100755 --- a/src/Entity/ListBuilder/ApiDocListBuilder.php +++ b/src/Entity/ListBuilder/ApiDocListBuilder.php @@ -88,12 +88,12 @@ public function buildRow(EntityInterface $entity) { public function getOperations(EntityInterface $entity) { $operations = parent::getOperations($entity); - // Add "Update OpenAPI spec" link. - if ($entity->access('update') && $entity->hasLinkTemplate('update-spec-form')) { - $operations['update_spec'] = [ - 'title' => $this->t('Update OpenAPI spec'), + // Add "Re-import OpenAPI spec" link. + if ($entity->access('update') && $entity->hasLinkTemplate('reimport-spec-form')) { + $operations['reimport_spec'] = [ + 'title' => $this->t('Re-import OpenAPI spec'), 'weight' => 15, - 'url' => $this->ensureDestination($entity->toUrl('update-spec-form')), + 'url' => $this->ensureDestination($entity->toUrl('reimport-spec-form')), ]; } diff --git a/src/Entity/Routing/ApiDocHtmlRouteProvider.php b/src/Entity/Routing/ApiDocHtmlRouteProvider.php index 08462e1..9556b91 100755 --- a/src/Entity/Routing/ApiDocHtmlRouteProvider.php +++ b/src/Entity/Routing/ApiDocHtmlRouteProvider.php @@ -51,8 +51,8 @@ public function getRoutes(EntityTypeInterface $entity_type) { $apidoc_collection_route->setDefault('_title', $this->t('@entity_type catalog', ['@entity_type' => $entity_type->getLabel()])->render()); } - if ($update_spec_route = $this->getUpdateSpecFormRoute($entity_type)) { - $collection->add("entity.{$entity_type_id}.update_spec_form", $update_spec_route); + if ($reimport_spec_route = $this->getUpdateSpecFormRoute($entity_type)) { + $collection->add("entity.{$entity_type_id}.reimport_spec_form", $reimport_spec_route); } return $collection; @@ -83,7 +83,7 @@ protected function getSettingsFormRoute(EntityTypeInterface $entity_type) { } /** - * Gets the update-spec-form route. + * Gets the reimport-spec-form route. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * The entity type. @@ -92,17 +92,17 @@ protected function getSettingsFormRoute(EntityTypeInterface $entity_type) { * The generated route, if available. */ protected function getUpdateSpecFormRoute(EntityTypeInterface $entity_type) { - if ($entity_type->hasLinkTemplate('update-spec-form')) { + if ($entity_type->hasLinkTemplate('reimport-spec-form')) { $entity_type_id = $entity_type->id(); - $route = new Route($entity_type->getLinkTemplate('update-spec-form')); - // Use the update_spec form handler. - if ($entity_type->getFormClass('update_spec')) { - $operation = 'update_spec'; + $route = new Route($entity_type->getLinkTemplate('reimport-spec-form')); + // Use the reimport_spec form handler. + if ($entity_type->getFormClass('reimport_spec')) { + $operation = 'reimport_spec'; } $route ->setDefaults([ '_entity_form' => "{$entity_type_id}.{$operation}", - '_title' => 'Update API Doc OpenAPI specification', + '_title' => 'Re-import API Doc OpenAPI specification', ]) ->setRequirement('_entity_access', "{$entity_type_id}.update") ->setOption('parameters', [ diff --git a/src/Form/ApiDocUpdateSpecForm.php b/src/Form/ApiDocUpdateSpecForm.php index decf2fa..f24c0ea 100644 --- a/src/Form/ApiDocUpdateSpecForm.php +++ b/src/Form/ApiDocUpdateSpecForm.php @@ -17,13 +17,13 @@ class ApiDocUpdateSpecForm extends ContentEntityConfirmFormBase { /** * {@inheritdoc} */ - protected $operation = 'update_spec'; + protected $operation = 'reimport_spec'; /** * {@inheritdoc} */ public function getQuestion() { - return $this->t('Are you sure you want to update the OpenAPI specification + return $this->t('Are you sure you want to re-import the OpenAPI specification file from URL on API Doc %name?', [ '%name' => $this->entity->label(), ]); @@ -57,16 +57,16 @@ public function validateForm(array &$form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { /* @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ $entity = $this->getEntity(); - $status = $entity->updateOpenApiSpecFile(TRUE, TRUE); + $status = $entity->reimportOpenApiSpecFile(TRUE, TRUE); if ($status) { - $this->messenger()->addStatus($this->t('API Doc %label: updated the OpenAPI + $this->messenger()->addStatus($this->t('API Doc %label: imported the OpenAPI specification file from URL.', [ '%label' => $this->entity->label(), ])); } else { - $this->messenger()->addError($this->t('API Doc %label: could not update + $this->messenger()->addError($this->t('API Doc %label: could not import the OpenAPI specification file from URL.', [ '%label' => $this->entity->label(), ])); From 28e943db57919d6962e4555fb7091de8dae0e940 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 12:21:14 -0700 Subject: [PATCH 23/48] DRUP-610 Add url as source for apidocs spec file - updating README. --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6080da1..2cd4fd0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,18 @@ to be able to view published API documentation. The OpenAPI spec by default is rendered using Apigee SmartDocs. +The OpenAPI spec can be directly uploaded as a file, or associated to a source location +such as Apigee Edge or a URL. A "Re-import OpenAPI spec" operation is available per +API Doc to re-import the spec file when source location changes. + +The OpenAPI spec by default is shown on the API Doc detail page by default. +To render the OpenAPI spec using Swagger UI: + +1. Install an enable the [Swagger UI Field Formatter](https://www.drupal.org/project/swagger_ui_formatter) module. +2. Install the Swagger UI JS library as documented [on the module page](https://www.drupal.org/project/swagger_ui_formatter). +3. Go to __Configuration > API catalog > Manage display__ in the admin menu. +4. Change "OpenAPI specification" field format to use the Swagger UI field formatter. + The API Doc is an entity, you can configure it at __Configuration > API catalog__ in the admin menu. @@ -23,9 +35,7 @@ under Structure > Views in the admin menu. ## Planned Features - Integration with Apigee API Products -- Allow OpenAPI specs to be associated to a source location such as Apigee Edge or a URL - Add visual notifications when source URL specs have changed on the API Doc admin screen -- Ability to update API Docs when source location changes ### Known issues From eceb09577013925a973902c7439c892771080ac6 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 12:38:05 -0700 Subject: [PATCH 24/48] DRUP-610 Add url as source for apidocs spec file - refactored logic around checking if spec is provided as file. --- src/Entity/ApiDoc.php | 20 +++++++------------- src/Entity/ApiDocInterface.php | 8 -------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index a98edd7..582b5f1 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -139,18 +139,6 @@ public function setPublished(bool $published) : ApiDocInterface { return $this; } - /** - * {@inheritdoc} - */ - public function getSpecAsFile() : bool { - // Default is to use spec as file. - if ($this->get('spec_as_file')->isEmpty()) { - return TRUE; - } - - return (bool) $this->get('spec_as_file')->getValue()[0]['value']; - } - /** * {@inheritdoc} */ @@ -317,7 +305,13 @@ public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { $spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0]; - if (!$this->getSpecAsFile()) { + if (!$this->get('spec_as_file')->value) { + + // If the file_link field is empty, return without showing errors. + if ($this->get('file_link')->isEmpty()) { + return TRUE; + } + $file_uri = $this->get('file_link')->getValue()[0]['uri']; $data = file_get_contents($file_uri); if (empty($data)) { diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 5b11562..4572856 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -106,14 +106,6 @@ public function isPublished() : bool; */ public function setPublished(bool $published) : self; - /** - * Indicates if OpenAPI specs will be provided as a file (otherwise a Url). - * - * @return bool - * TRUE if specs will be provided as a file (otherwise a Url). - */ - public function getSpecAsFile() : bool; - /** * Check if entity is revisionable. * From 49cc614a9e17f8b46af10c96f48627e9661bf4fd Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 12:41:27 -0700 Subject: [PATCH 25/48] DRUP-610 Add url as source for apidocs spec file - removed isRevisionable() method. --- src/Entity/ApiDoc.php | 10 +--------- src/Entity/ApiDocInterface.php | 8 -------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 582b5f1..25e38f9 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -139,14 +139,6 @@ public function setPublished(bool $published) : ApiDocInterface { return $this; } - /** - * {@inheritdoc} - */ - public function isRevisionable() : bool { - // Entity types are revisionable if a revision key has been specified. - return (bool) $this->getEntityKey('revision'); - } - /** * {@inheritdoc} */ @@ -360,7 +352,7 @@ public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { // Only save if changes were made. if ($save && $needs_save) { - if ($new_revision && $this->isRevisionable()) { + if ($new_revision && $this->getEntityType()->isRevisionable()) { $this->setNewRevision(); } $this->save(); diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 4572856..99d6eb0 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -106,14 +106,6 @@ public function isPublished() : bool; */ public function setPublished(bool $published) : self; - /** - * Check if entity is revisionable. - * - * @return bool - * TRUE if entity is revisionable. - */ - public function isRevisionable() : bool; - /** * Re-import OpenAPI specifications file from URL. * From 84db514bb7aee4b88515c55e35c2fb9d454df681 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 15:53:48 -0700 Subject: [PATCH 26/48] DRUP-610 Add url as source for apidocs spec file - fixing code sniffer validations. --- src/Entity/ApiDoc.php | 4 ++-- src/Entity/Form/ApiDocForm.php | 1 - src/Form/ApiDocUpdateSpecForm.php | 7 +++---- .../Validation/Constraint/ApiDocFileLinkConstraint.php | 4 +++- .../Constraint/ApiDocFileLinkConstraintValidator.php | 3 ++- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 25e38f9..5ff25bb 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -324,8 +324,8 @@ public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { $uri_scheme = $specs_definition->getSetting('uri_scheme'); $file = file_save_data($data, "$uri_scheme://$target_dir/$filename", FILE_EXISTS_RENAME); $spec_value = [ - 'target_id' => $file->id(), - ] + $spec_value; + 'target_id' => $file->id(), + ] + $spec_value; $this->set('spec', $spec_value); $this->set('spec_md5', $data_md5); diff --git a/src/Entity/Form/ApiDocForm.php b/src/Entity/Form/ApiDocForm.php index a3c73e1..a00bc62 100755 --- a/src/Entity/Form/ApiDocForm.php +++ b/src/Entity/Form/ApiDocForm.php @@ -28,7 +28,6 @@ */ class ApiDocForm extends ContentEntityForm { - /** * {@inheritdoc} */ diff --git a/src/Form/ApiDocUpdateSpecForm.php b/src/Form/ApiDocUpdateSpecForm.php index f24c0ea..5ac4233 100644 --- a/src/Form/ApiDocUpdateSpecForm.php +++ b/src/Form/ApiDocUpdateSpecForm.php @@ -23,10 +23,9 @@ class ApiDocUpdateSpecForm extends ContentEntityConfirmFormBase { * {@inheritdoc} */ public function getQuestion() { - return $this->t('Are you sure you want to re-import the OpenAPI specification - file from URL on API Doc %name?', [ - '%name' => $this->entity->label(), - ]); + return $this->t('Are you sure you want to update the OpenAPI specification file from URL on API Doc %name?', [ + '%name' => $this->entity->label(), + ]); } /** diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php index 3d0e310..ecc9e3c 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php @@ -34,7 +34,9 @@ class ApiDocFileLinkConstraint extends Constraint { /** - * @var string Message to be shown when it is not a valid link. + * Message to be shown when it is not a valid link. + * + * @var string */ public $notValid = '%value is not a valid link'; diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php index 86066e8..efc5f62 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php @@ -27,7 +27,7 @@ use Symfony\Component\Validator\ConstraintValidator; /** - * Class ApiDocFileLinkConstraintValidator + * Class ApiDocFileLinkConstraintValidator. */ class ApiDocFileLinkConstraintValidator extends ConstraintValidator { @@ -75,4 +75,5 @@ public function validate($items, Constraint $constraint) { } } } + } From 63c671ed64252ba5f85b94cefdb462d99ce9a605 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 16:38:58 -0700 Subject: [PATCH 27/48] DRUP-610 Add url as source for apidocs spec file - rename spec_as_file to spec_file_source and make it list_string type. --- src/Entity/ApiDoc.php | 33 ++++++++++++++++----------------- src/Entity/ApiDocInterface.php | 14 ++++++++++++++ src/Entity/Form/ApiDocForm.php | 15 ++++++++------- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 5ff25bb..6609d0b 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -181,17 +181,19 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ]) ->setDisplayConfigurable('form', TRUE); - $fields['spec_as_file'] = BaseFieldDefinition::create('boolean') - ->setLabel(t('Provide OpenAPI specification as a file')) + $fields['spec_file_source'] = BaseFieldDefinition::create('list_string') + ->setLabel(t('OpenAPI specification file source')) ->setDescription(t('Indicate if the OpenAPI spec will be provided as a - file (or a URL otherwise).')) - ->setDefaultValue(TRUE) + file for upload or a URL.')) + ->setDefaultValue(ApiDocInterface::SPEC_AS_FILE) + ->setRequired(TRUE) + ->setSetting('allowed_values', [ + ApiDocInterface::SPEC_AS_FILE => t('File'), + ApiDocInterface::SPEC_AS_URL => t('URL'), + ]) ->setDisplayOptions('form', [ - 'type' => 'boolean_checkbox', + 'type' => 'options_buttons', 'weight' => 0, - 'settings' => [ - 'display_label' => TRUE, - ], ]) ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE); @@ -290,14 +292,12 @@ public function preSave(EntityStorageInterface $storage) { */ public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { $needs_save = FALSE; - - // If "spec_as_file", grab file from "file_link" and save it into the - // "spec" file field. The file_link field should already have validated that - // a valid file exists at that URL. - $spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0]; - if (!$this->get('spec_as_file')->value) { + // If "spec_file_source" uses URL, grab file from "file_link" and save it + // into the "spec" file field. The file_link field should already have + // validated that a valid file exists at that URL. + if ($this->get('spec_file_source')->value == ApiDocInterface::SPEC_AS_URL) { // If the file_link field is empty, return without showing errors. if ($this->get('file_link')->isEmpty()) { @@ -316,7 +316,7 @@ public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { // Only save file if it hasn't been fetched previously. $data_md5 = md5($data); - $prev_md5 = $this->get('spec_md5')->isEmpty() ? NULL : $this->get('spec_md5')->getValue()[0]['value']; + $prev_md5 = $this->get('spec_md5')->isEmpty() ? NULL : $this->get('spec_md5')->value; if ($prev_md5 != $data_md5) { $filename = \Drupal::service('file_system')->basename($file_uri); $specs_definition = $this->getFieldDefinition('spec')->getItemDefinition(); @@ -340,11 +340,10 @@ public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { ->load($spec_value['target_id']); if ($file) { - $prev_md5 = $this->get('spec_md5')->isEmpty() ? NULL : $this->get('spec_md5')->getValue()[0]['value']; + $prev_md5 = $this->get('spec_md5')->isEmpty() ? NULL : $this->get('spec_md5')->value; $file_md5 = md5_file($file->getFileUri()); if ($prev_md5 != $file_md5) { $this->set('spec_md5', $file_md5); - $needs_save = TRUE; } } diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 99d6eb0..4bb7e84 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -28,6 +28,20 @@ */ interface ApiDocInterface extends ContentEntityInterface, EntityChangedInterface { + /** + * The value of "spec_file_source" when it uses a file as source. + * + * @var string + */ + const SPEC_AS_FILE = 'file'; + + /** + * The value of "spec_file_source" when it uses a URL as source. + * + * @var string + */ + const SPEC_AS_URL = 'url'; + /** * Gets the API Doc name. * diff --git a/src/Entity/Form/ApiDocForm.php b/src/Entity/Form/ApiDocForm.php index a00bc62..f6acdcd 100755 --- a/src/Entity/Form/ApiDocForm.php +++ b/src/Entity/Form/ApiDocForm.php @@ -20,6 +20,7 @@ namespace Drupal\apigee_api_catalog\Entity\Form; +use Drupal\apigee_edge_apidocs\Entity\ApiDocInterface; use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Form\FormStateInterface; @@ -37,30 +38,30 @@ public function form(array $form, FormStateInterface $form_state) { $form['specifications'] = [ '#type' => 'fieldset', '#title' => $this->t('OpenAPI Specifications File'), - '#weight' => $form['spec_as_file']['#weight'], + '#weight' => $form['spec_file_source']['#weight'], ]; $form['spec']['#states'] = [ 'visible' => [ - ':input[name="spec_as_file[value]"]' => ['checked' => TRUE], + ':input[name="spec_file_source"]' => ['value' => ApiDocInterface::SPEC_AS_FILE], ], 'required' => [ - ':input[name="spec_as_file[value]"]' => ['checked' => TRUE], + ':input[name="spec_file_source"]' => ['value' => ApiDocInterface::SPEC_AS_FILE], ], ]; $form['file_link']['#states'] = [ 'visible' => [ - ':input[name="spec_as_file[value]"]' => ['checked' => FALSE], + ':input[name="spec_file_source"]' => ['value' => ApiDocInterface::SPEC_AS_URL], ], 'required' => [ - ':input[name="spec_as_file[value]"]' => ['checked' => FALSE], + ':input[name="spec_file_source"]' => ['value' => ApiDocInterface::SPEC_AS_URL], ], ]; - $form['specifications']['spec_as_file'] = $form['spec_as_file']; + $form['specifications']['spec_file_source'] = $form['spec_file_source']; $form['specifications']['spec'] = $form['spec']; $form['specifications']['file_link'] = $form['file_link']; - unset($form['spec_as_file'], $form['spec'], $form['file_link']); + unset($form['spec_file_source'], $form['spec'], $form['file_link']); return $form; } From 2b78af791a302fbea1116f63df2ec46bd2247214 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 16:45:36 -0700 Subject: [PATCH 28/48] DRUP-610 Add url as source for apidocs spec file - addressing PR feedback. --- src/Entity/ApiDoc.php | 2 +- src/Entity/ApiDocInterface.php | 2 +- .../Validation/Constraint/ApiDocFileLinkConstraintValidator.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 6609d0b..3bcc5c2 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -290,7 +290,7 @@ public function preSave(EntityStorageInterface $storage) { /** * {@inheritdoc} */ - public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE) { + public function reimportOpenApiSpecFile(bool $save = TRUE, bool $new_revision = TRUE) { $needs_save = FALSE; $spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0]; diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 4bb7e84..126fdb0 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -132,6 +132,6 @@ public function setPublished(bool $published) : self; * @return bool * Returned TRUE if the operation completed without errors. */ - public function reimportOpenApiSpecFile($save = TRUE, $new_revision = TRUE); + public function reimportOpenApiSpecFile(bool $save = TRUE, bool $new_revision = TRUE); } diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php index efc5f62..74b3999 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php @@ -65,7 +65,7 @@ public function validate($items, Constraint $constraint) { ]; // Perform only a HEAD method to save bandwidth. - /* @var $response ResponseInterface */ + /* @var $response \Psr\Http\Message\ResponseInterface */ $response = \Drupal::httpClient()->head($url, $options); } catch (RequestException $request_exception) { From 4ae0281dd1ce778c37865fef2cee922deb381da2 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 23:13:33 -0700 Subject: [PATCH 29/48] DRUP-459 Refactor API Doc open API spec fetcher as service. --- apigee_api_catalog.services.yml | 5 + src/ApiDocSpecFetcher.php | 185 +++++++++++++++++++++++++++++ src/ApiDocSpecFetcherInterface.php | 51 ++++++++ src/Entity/ApiDoc.php | 75 +----------- src/Entity/ApiDocInterface.php | 14 --- src/Entity/Form/ApiDocForm.php | 2 +- src/Form/ApiDocUpdateSpecForm.php | 57 ++++++++- 7 files changed, 296 insertions(+), 93 deletions(-) create mode 100644 src/ApiDocSpecFetcher.php create mode 100644 src/ApiDocSpecFetcherInterface.php diff --git a/apigee_api_catalog.services.yml b/apigee_api_catalog.services.yml index 021edf9..af208ca 100644 --- a/apigee_api_catalog.services.yml +++ b/apigee_api_catalog.services.yml @@ -1,4 +1,9 @@ services: + logger.channel.apigee_api_catalog: parent: logger.channel_base arguments: ['apigee_api_catalog'] + + apigee_api_catalog.spec_fetcher: + class: Drupal\apigee_api_catalog\ApiDocSpecFetcher + arguments: ['@file_system', '@http_client', '@entity_type.manager', '@messenger', '@logger.channel.apigee_api_catalog'] diff --git a/src/ApiDocSpecFetcher.php b/src/ApiDocSpecFetcher.php new file mode 100644 index 0000000..88aa5c2 --- /dev/null +++ b/src/ApiDocSpecFetcher.php @@ -0,0 +1,185 @@ +fileSystem = $file_system; + $this->httpClient = $http_client; + $this->entityTypeManager = $entityTypeManager; + $this->messenger = $messenger; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_revision = TRUE, bool $show_messages = TRUE) : bool { + $needs_save = FALSE; + $spec_value = $apidoc->get('spec')->isEmpty() ? [] : $apidoc->get('spec')->getValue()[0]; + + // If "spec_file_source" uses URL, grab file from "file_link" and save it + // into the "spec" file field. The file_link field should already have + // validated that a valid file exists at that URL. + if ($apidoc->get('spec_file_source')->value == ApiDocInterface::SPEC_AS_URL) { + + // If the file_link field is empty, return without error. + if ($apidoc->get('file_link')->isEmpty()) { + return TRUE; + } + + $file_uri = $apidoc->get('file_link')->getValue()[0]['uri']; + $data = file_get_contents($file_uri); + if (empty($data)) { + $message = 'Could not retrieve OpenAPI specification file located at %url'; + $params = [ + '%url' => $file_uri, + ]; + $this->logger->error($message, $params); + if ($show_messages) { + $this->messenger->addMessage($this->t($message, $params), 'error'); + } + return FALSE; + } + + // Only save file if it hasn't been fetched previously. + $data_md5 = md5($data); + $prev_md5 = $apidoc->get('spec_md5')->isEmpty() ? NULL : $apidoc->get('spec_md5')->value; + if ($prev_md5 != $data_md5) { + $filename = $this->fileSystem->basename($file_uri); + $specs_definition = $apidoc->getFieldDefinition('spec')->getItemDefinition(); + $target_dir = $specs_definition->getSetting('file_directory'); + $uri_scheme = $specs_definition->getSetting('uri_scheme'); + $file = file_save_data($data, "$uri_scheme://$target_dir/$filename", FILE_EXISTS_RENAME); + $spec_value = [ + 'target_id' => $file->id(), + ] + $spec_value; + $apidoc->set('spec', $spec_value); + $apidoc->set('spec_md5', $data_md5); + + $needs_save = TRUE; + } + } + + elseif (!empty($spec_value['target_id'])) { + /* @var \Drupal\file\Entity\File $file */ + $file = $this->entityTypeManager + ->getStorage('file') + ->load($spec_value['target_id']); + + if ($file) { + $prev_md5 = $apidoc->get('spec_md5')->isEmpty() ? NULL : $apidoc->get('spec_md5')->value; + $file_md5 = md5_file($file->getFileUri()); + if ($prev_md5 != $file_md5) { + $apidoc->set('spec_md5', $file_md5); + $needs_save = TRUE; + } + } + } + + // Only save if changes were made. + if ($save && $needs_save) { + if ($new_revision && $apidoc->getEntityType()->isRevisionable()) { + $apidoc->setNewRevision(); + } + + try { + $apidoc->save(); + } + catch (EntityStorageException $e) { + $message = 'Error while saving API Doc while fetching OpenAPI specification file located at %url'; + $params = [ + '%url' => $file_uri, + ]; + $this->logger->error($message, $params); + if ($show_messages) { + $this->messenger->addMessage($this->t($message, $params), 'error'); + } + return FALSE; + } + } + + return TRUE; + } + +} diff --git a/src/ApiDocSpecFetcherInterface.php b/src/ApiDocSpecFetcherInterface.php new file mode 100644 index 0000000..838c3cd --- /dev/null +++ b/src/ApiDocSpecFetcherInterface.php @@ -0,0 +1,51 @@ +reimportOpenApiSpecFile(FALSE, FALSE); - } - - /** - * {@inheritdoc} - */ - public function reimportOpenApiSpecFile(bool $save = TRUE, bool $new_revision = TRUE) { - $needs_save = FALSE; - $spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0]; - - // If "spec_file_source" uses URL, grab file from "file_link" and save it - // into the "spec" file field. The file_link field should already have - // validated that a valid file exists at that URL. - if ($this->get('spec_file_source')->value == ApiDocInterface::SPEC_AS_URL) { - - // If the file_link field is empty, return without showing errors. - if ($this->get('file_link')->isEmpty()) { - return TRUE; - } - - $file_uri = $this->get('file_link')->getValue()[0]['uri']; - $data = file_get_contents($file_uri); - if (empty($data)) { - \Drupal::messenger() - ->addMessage($this->t('Could not retrieve OpenAPI specifications file located at %url', [ - '%url' => $file_uri, - ]), 'error'); - return FALSE; - } - - // Only save file if it hasn't been fetched previously. - $data_md5 = md5($data); - $prev_md5 = $this->get('spec_md5')->isEmpty() ? NULL : $this->get('spec_md5')->value; - if ($prev_md5 != $data_md5) { - $filename = \Drupal::service('file_system')->basename($file_uri); - $specs_definition = $this->getFieldDefinition('spec')->getItemDefinition(); - $target_dir = $specs_definition->getSetting('file_directory'); - $uri_scheme = $specs_definition->getSetting('uri_scheme'); - $file = file_save_data($data, "$uri_scheme://$target_dir/$filename", FILE_EXISTS_RENAME); - $spec_value = [ - 'target_id' => $file->id(), - ] + $spec_value; - $this->set('spec', $spec_value); - $this->set('spec_md5', $data_md5); - - $needs_save = TRUE; - } - } - - elseif (!empty($spec_value['target_id'])) { - /* @var \Drupal\file\Entity\File $file */ - $file = \Drupal::entityTypeManager() - ->getStorage('file') - ->load($spec_value['target_id']); - - if ($file) { - $prev_md5 = $this->get('spec_md5')->isEmpty() ? NULL : $this->get('spec_md5')->value; - $file_md5 = md5_file($file->getFileUri()); - if ($prev_md5 != $file_md5) { - $this->set('spec_md5', $file_md5); - $needs_save = TRUE; - } - } - } - - // Only save if changes were made. - if ($save && $needs_save) { - if ($new_revision && $this->getEntityType()->isRevisionable()) { - $this->setNewRevision(); - } - $this->save(); - } - - return TRUE; + \Drupal::service('apigee_api_catalog.spec_fetcher')->fetchSpec($this, FALSE, FALSE); } } diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 126fdb0..3cac91e 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -120,18 +120,4 @@ public function isPublished() : bool; */ public function setPublished(bool $published) : self; - /** - * Re-import OpenAPI specifications file from URL. - * - * @param bool $save - * Boolean indicating if method should save the entity. - * @param bool $new_revision - * Boolean indicating if method should create a new revision when saving - * the entity. - * - * @return bool - * Returned TRUE if the operation completed without errors. - */ - public function reimportOpenApiSpecFile(bool $save = TRUE, bool $new_revision = TRUE); - } diff --git a/src/Entity/Form/ApiDocForm.php b/src/Entity/Form/ApiDocForm.php index f6acdcd..d4b901c 100755 --- a/src/Entity/Form/ApiDocForm.php +++ b/src/Entity/Form/ApiDocForm.php @@ -20,7 +20,7 @@ namespace Drupal\apigee_api_catalog\Entity\Form; -use Drupal\apigee_edge_apidocs\Entity\ApiDocInterface; +use Drupal\apigee_api_catalog\Entity\ApiDocInterface; use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Form\FormStateInterface; diff --git a/src/Form/ApiDocUpdateSpecForm.php b/src/Form/ApiDocUpdateSpecForm.php index 5ac4233..f28e5c9 100644 --- a/src/Form/ApiDocUpdateSpecForm.php +++ b/src/Form/ApiDocUpdateSpecForm.php @@ -2,23 +2,72 @@ namespace Drupal\apigee_api_catalog\Form; +use Drupal\apigee_api_catalog\ApiDocSpecFetcherInterface; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\ContentEntityConfirmFormBase; +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Messenger\MessengerTrait; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Class ApiDocUpdateSpecForm. */ class ApiDocUpdateSpecForm extends ContentEntityConfirmFormBase { - use MessengerTrait; + /** + * The messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * The ApiDoc spec fetcher service. + * + * @var \Drupal\apigee_api_catalog\ApiDocSpecFetcherInterface + */ + protected $specFetcher; /** * {@inheritdoc} */ protected $operation = 'reimport_spec'; + /** + * Constructs a ApiDocUpdateSpecForm object. + * + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository service. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle service. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. + * @param \Drupal\apigee_api_catalog\ApiDocSpecFetcherInterface $spec_fetcher + * The ApiDoc spec fetcher service. + */ + public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, MessengerInterface $messenger, ApiDocSpecFetcherInterface $spec_fetcher) { + parent::__construct($entity_repository, $entity_type_bundle_info, $time); + $this->messenger = $messenger; + $this->specFetcher = $spec_fetcher; + } + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.repository'), + $container->get('entity_type.bundle.info'), + $container->get('datetime.time'), + $container->get('messenger'), + $container->get('apigee_api_catalog.spec_fetcher') + ); + } + /** * {@inheritdoc} */ @@ -56,10 +105,10 @@ public function validateForm(array &$form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { /* @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ $entity = $this->getEntity(); - $status = $entity->reimportOpenApiSpecFile(TRUE, TRUE); + $status = $this->specFetcher->fetchSpec($entity, FALSE, FALSE); if ($status) { - $this->messenger()->addStatus($this->t('API Doc %label: imported the OpenAPI + $this->messenger->addStatus($this->t('API Doc %label: imported the OpenAPI specification file from URL.', [ '%label' => $this->entity->label(), ])); From 55fd0bdc7344cf21cdfe74fed71a1586aea38583 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Tue, 7 May 2019 23:25:07 -0700 Subject: [PATCH 30/48] DRUP-610 Add url as source for apidocs spec file - use DI in field constraint. --- .../ApiDocFileLinkConstraintValidator.php | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php index 74b3999..4ebebe7 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php @@ -20,16 +20,43 @@ namespace Drupal\apigee_api_catalog\Plugin\Validation\Constraint; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Url; +use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\RequestException; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; /** * Class ApiDocFileLinkConstraintValidator. */ -class ApiDocFileLinkConstraintValidator extends ConstraintValidator { +class ApiDocFileLinkConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { + + /** + * The HTTP client to fetch the files with. + * + * @var \GuzzleHttp\ClientInterface + */ + protected $httpClient; + + /** + * ApiDocFileLinkConstraintValidator constructor. + * + * @param \GuzzleHttp\ClientInterface $http_client + * A Guzzle client object. + */ + public function __construct(ClientInterface $http_client) { + $this->httpClient = $http_client; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('http_client')); + } /** * {@inheritdoc} @@ -66,7 +93,7 @@ public function validate($items, Constraint $constraint) { // Perform only a HEAD method to save bandwidth. /* @var $response \Psr\Http\Message\ResponseInterface */ - $response = \Drupal::httpClient()->head($url, $options); + $response = $this->httpClient->head($url, $options); } catch (RequestException $request_exception) { $this->context->addViolation($constraint->notValid, [ From 09ebbd485bf955dbae706e0e97a51fd1ea5d9451 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Wed, 8 May 2019 11:24:11 -0700 Subject: [PATCH 31/48] DRUP-459 Refactor API Doc open API spec fetcher as service - bugfix. --- src/Form/ApiDocUpdateSpecForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Form/ApiDocUpdateSpecForm.php b/src/Form/ApiDocUpdateSpecForm.php index f28e5c9..1462726 100644 --- a/src/Form/ApiDocUpdateSpecForm.php +++ b/src/Form/ApiDocUpdateSpecForm.php @@ -105,7 +105,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { /* @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ $entity = $this->getEntity(); - $status = $this->specFetcher->fetchSpec($entity, FALSE, FALSE); + $status = $this->specFetcher->fetchSpec($entity, TRUE, TRUE); if ($status) { $this->messenger->addStatus($this->t('API Doc %label: imported the OpenAPI From 61400ead4f7e64917cb9fa46d4ea40cf22473532 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Wed, 8 May 2019 12:43:58 -0700 Subject: [PATCH 32/48] DRUP-459 API Doc open API spec fetcher - filesystem validations. --- src/ApiDocSpecFetcher.php | 55 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/ApiDocSpecFetcher.php b/src/ApiDocSpecFetcher.php index 88aa5c2..1bc7d23 100644 --- a/src/ApiDocSpecFetcher.php +++ b/src/ApiDocSpecFetcher.php @@ -130,7 +130,29 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_ $specs_definition = $apidoc->getFieldDefinition('spec')->getItemDefinition(); $target_dir = $specs_definition->getSetting('file_directory'); $uri_scheme = $specs_definition->getSetting('uri_scheme'); - $file = file_save_data($data, "$uri_scheme://$target_dir/$filename", FILE_EXISTS_RENAME); + $destination = "$uri_scheme://$target_dir/"; + + try { + $this->checkRequirements($destination); + $file = file_save_data($data, $destination . $filename, FILE_EXISTS_RENAME); + + if (empty($file)) { + throw new \Exception('Could not save API Doc specification file.'); + } + } + catch (\Exception $e) { + $message = 'Error while saving API Doc spec file from URL on API Doc ID: %id. Error: %error'; + $params = [ + '%id' => $apidoc->id(), + '%error' => $e->getMessage(), + ]; + $this->logger->error($message, $params); + if ($show_messages) { + $this->messenger->addMessage($this->t($message, $params), 'error'); + } + return FALSE; + } + $spec_value = [ 'target_id' => $file->id(), ] + $spec_value; @@ -182,4 +204,35 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_ return TRUE; } + /** + * Checks requirements for saving of a file spec. + * + * If a requirement is not fulfilled it throws an exception. + * + * @param string $destination + * The specification file destination directory, including scheme. + * + * @throws \Exception + */ + private function checkRequirements(string $destination): void { + // If using private filesystem, check that it's been configured. + if (strpos($destination, 'private://') === 0 && !$this->isPrivateFileSystemConfigured()) { + throw new \Exception('Private filesystem has not been configured.'); + } + + if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { + throw new \Exception('Could not prepare API Doc specification file destination directory.'); + } + } + + /** + * Checks whether the private filesystem is configured. + * + * @return bool + * True if configured, FALSE otherwise. + */ + private function isPrivateFileSystemConfigured(): bool { + return (bool) $this->fileSystem->realpath('private://'); + } + } From af8ed9940a0a04ba125da42eb6c98850f3f28368 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Wed, 8 May 2019 16:49:32 -0700 Subject: [PATCH 33/48] DRUP-610 Show message when refetching but using file a source. --- src/ApiDocSpecFetcher.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ApiDocSpecFetcher.php b/src/ApiDocSpecFetcher.php index 1bc7d23..374ec40 100644 --- a/src/ApiDocSpecFetcher.php +++ b/src/ApiDocSpecFetcher.php @@ -164,6 +164,12 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_ } elseif (!empty($spec_value['target_id'])) { + if ($show_messages) { + $this->messenger->addStatus($this->t('API Doc %label is using a file upload as source. Nothing to update.', [ + '%label' => $apidoc->label(), + ])); + } + /* @var \Drupal\file\Entity\File $file */ $file = $this->entityTypeManager ->getStorage('file') From 52cfd07225220bab148ea54bef627f5676769401 Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Fri, 10 May 2019 11:35:25 -0700 Subject: [PATCH 34/48] DRUP-610 Replace file_get_contents() with http_client service. --- src/ApiDocSpecFetcher.php | 136 ++++++++++++++++------------- src/ApiDocSpecFetcherInterface.php | 20 +++-- src/Entity/ApiDoc.php | 19 ++++ src/Form/ApiDocUpdateSpecForm.php | 18 ++-- 4 files changed, 117 insertions(+), 76 deletions(-) diff --git a/src/ApiDocSpecFetcher.php b/src/ApiDocSpecFetcher.php index 374ec40..e10b3ab 100644 --- a/src/ApiDocSpecFetcher.php +++ b/src/ApiDocSpecFetcher.php @@ -21,12 +21,15 @@ namespace Drupal\apigee_api_catalog; use Drupal\apigee_api_catalog\Entity\ApiDocInterface; -use Drupal\Core\Entity\EntityStorageException; +use Drupal\Component\Datetime\DateTimePlus; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Url; use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Request; use Psr\Log\LoggerInterface; /** @@ -94,7 +97,7 @@ public function __construct(FileSystemInterface $file_system, ClientInterface $h /** * {@inheritdoc} */ - public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_revision = TRUE, bool $show_messages = TRUE) : bool { + public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE) : bool { $needs_save = FALSE; $spec_value = $apidoc->get('spec')->isEmpty() ? [] : $apidoc->get('spec')->getValue()[0]; @@ -103,22 +106,50 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_ // validated that a valid file exists at that URL. if ($apidoc->get('spec_file_source')->value == ApiDocInterface::SPEC_AS_URL) { - // If the file_link field is empty, return without error. + // If the file_link field is empty, return without changes. if ($apidoc->get('file_link')->isEmpty()) { - return TRUE; + return FALSE; } $file_uri = $apidoc->get('file_link')->getValue()[0]['uri']; - $data = file_get_contents($file_uri); + $file_uri = Url::fromUri($file_uri, ['absolute' => TRUE])->toString(); + $request = new Request('GET', $file_uri); + $options = [ + 'exceptions' => TRUE, + 'allow_redirects' => [ + 'strict' => TRUE, + ], + ]; + + // Generate conditional GET header. + if (!$apidoc->get('fetched_timestamp')->isEmpty()) { + $request = $request->withAddedHeader('If-Modified-Since', gmdate(DateTimePlus::RFC7231, $apidoc->get('fetched_timestamp')->value)); + } + + try { + $response = $this->httpClient->send($request, $options); + + // In case of a 304 Not Modified there are no changes, but update + // last fetched timestamp. + if ($response->getStatusCode() == 304) { + $apidoc->set('fetched_timestamp', time()); + return TRUE; + } + } + catch (RequestException $e) { + $this->log($apidoc, static::TYPE_ERROR, 'API Doc %label: Could not retrieve OpenAPI specification file located at %url.', [ + '%url' => $file_uri, + '%label' => $apidoc->label(), + ], $show_messages); + return FALSE; + } + + $data = (string) $response->getBody(); if (empty($data)) { - $message = 'Could not retrieve OpenAPI specification file located at %url'; - $params = [ + $this->log($apidoc, static::TYPE_ERROR, 'API Doc %label: OpenAPI specification file located at %url is empty.', [ '%url' => $file_uri, - ]; - $this->logger->error($message, $params); - if ($show_messages) { - $this->messenger->addMessage($this->t($message, $params), 'error'); - } + '%label' => $apidoc->label(), + ], $show_messages); return FALSE; } @@ -141,15 +172,10 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_ } } catch (\Exception $e) { - $message = 'Error while saving API Doc spec file from URL on API Doc ID: %id. Error: %error'; - $params = [ + $this->log($apidoc, static::TYPE_ERROR, 'Error while saving API Doc spec file from URL on API Doc ID: %id. Error: %error', [ '%id' => $apidoc->id(), '%error' => $e->getMessage(), - ]; - $this->logger->error($message, $params); - if ($show_messages) { - $this->messenger->addMessage($this->t($message, $params), 'error'); - } + ], $show_messages); return FALSE; } @@ -158,56 +184,48 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_ ] + $spec_value; $apidoc->set('spec', $spec_value); $apidoc->set('spec_md5', $data_md5); + $apidoc->set('fetched_timestamp', time()); $needs_save = TRUE; } } - elseif (!empty($spec_value['target_id'])) { - if ($show_messages) { - $this->messenger->addStatus($this->t('API Doc %label is using a file upload as source. Nothing to update.', [ - '%label' => $apidoc->label(), - ])); - } - - /* @var \Drupal\file\Entity\File $file */ - $file = $this->entityTypeManager - ->getStorage('file') - ->load($spec_value['target_id']); - - if ($file) { - $prev_md5 = $apidoc->get('spec_md5')->isEmpty() ? NULL : $apidoc->get('spec_md5')->value; - $file_md5 = md5_file($file->getFileUri()); - if ($prev_md5 != $file_md5) { - $apidoc->set('spec_md5', $file_md5); - $needs_save = TRUE; - } - } + elseif ($apidoc->get('spec_file_source')->value == ApiDocInterface::SPEC_AS_FILE) { + $this->log($apidoc, static::TYPE_STATUS, 'API Doc %label is using a file upload as source. Nothing to update.', [ + '%label' => $apidoc->label(), + ], $show_messages); + $needs_save = FALSE; } - // Only save if changes were made. - if ($save && $needs_save) { - if ($new_revision && $apidoc->getEntityType()->isRevisionable()) { - $apidoc->setNewRevision(); - } + return $needs_save; + } - try { - $apidoc->save(); - } - catch (EntityStorageException $e) { - $message = 'Error while saving API Doc while fetching OpenAPI specification file located at %url'; - $params = [ - '%url' => $file_uri, - ]; + /** + * Log a message, and optionally display it on the UI. + * + * @param \Drupal\apigee_api_catalog\Entity\ApiDocInterface $apidoc + * The API Doc entity. + * @param string $type + * Type of message. + * @param string $message + * The message. + * @param array $params + * Optional parameters array. + * @param bool $show_messages + * TRUE if message should be displayed to the UI as well. + */ + private function log(ApiDocInterface $apidoc, string $type, string $message, array $params = [], bool $show_messages = TRUE) { + switch ($type) { + case static::TYPE_ERROR: $this->logger->error($message, $params); - if ($show_messages) { - $this->messenger->addMessage($this->t($message, $params), 'error'); - } - return FALSE; - } + break; + case static::TYPE_STATUS: + default: + $this->logger->info($message, $params); + } + if ($show_messages) { + $this->messenger->addMessage($this->t($message, $params), $type); } - - return TRUE; } /** diff --git a/src/ApiDocSpecFetcherInterface.php b/src/ApiDocSpecFetcherInterface.php index 838c3cd..fa6bbac 100644 --- a/src/ApiDocSpecFetcherInterface.php +++ b/src/ApiDocSpecFetcherInterface.php @@ -27,6 +27,16 @@ */ interface ApiDocSpecFetcherInterface { + /** + * A status message. + */ + const TYPE_STATUS = 'status'; + + /** + * An error. + */ + const TYPE_ERROR = 'error'; + /** * Fetch OpenAPI specification file from URL. * @@ -36,16 +46,12 @@ interface ApiDocSpecFetcherInterface { * * @param \Drupal\apigee_api_catalog\Entity\ApiDocInterface $apidoc * The ApiDoc entity. - * @param bool $save - * Boolean indicating if method should save the entity. - * @param bool $new_revision - * Boolean indicating if method should create a new revision when saving - * the entity. * @param bool $show_messages * Boolean indicating if method should display status messages. * * @return bool - * Returns TRUE if the operation completed without errors. + * Returns TRUE if the entity was changed and needs to be saved, FALSE + * otherwise. */ - public function fetchSpec(ApiDocInterface $apidoc, bool $save = TRUE, bool $new_revision = TRUE, bool $show_messages = TRUE) : bool; + public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE) : bool; } diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index d45d0d4..bd19a23 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -275,6 +275,10 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Changed')) ->setDescription(t('The time that the entity was last edited.')); + $fields['fetched_timestamp'] = BaseFieldDefinition::create('timestamp') + ->setLabel(t('Spec fetched from URL timestamp')) + ->setDescription(t('When the OpenAPI spec file was last fetched from URL as a Unix timestamp.')); + return $fields; } @@ -285,6 +289,21 @@ public function preSave(EntityStorageInterface $storage) { parent::preSave($storage); \Drupal::service('apigee_api_catalog.spec_fetcher')->fetchSpec($this, FALSE, FALSE); + + // Update spec_md5 value if using "file" as source. + if ($this->get('spec_file_source')->value == static::SPEC_AS_FILE) { + $spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0]; + if (!empty($spec_value['target_id'])) { + /* @var \Drupal\file\Entity\File $file */ + $file = $this->entityTypeManager() + ->getStorage('file') + ->load($spec_value['target_id']); + + if ($file) { + $this->set('spec_md5', md5_file($file->getFileUri())); + } + } + } } } diff --git a/src/Form/ApiDocUpdateSpecForm.php b/src/Form/ApiDocUpdateSpecForm.php index 1462726..b18d3bc 100644 --- a/src/Form/ApiDocUpdateSpecForm.php +++ b/src/Form/ApiDocUpdateSpecForm.php @@ -105,17 +105,15 @@ public function validateForm(array &$form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { /* @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ $entity = $this->getEntity(); - $status = $this->specFetcher->fetchSpec($entity, TRUE, TRUE); - if ($status) { - $this->messenger->addStatus($this->t('API Doc %label: imported the OpenAPI - specification file from URL.', [ - '%label' => $this->entity->label(), - ])); - } - else { - $this->messenger()->addError($this->t('API Doc %label: could not import - the OpenAPI specification file from URL.', [ + $needs_save = $this->specFetcher->fetchSpec($entity, TRUE); + if ($needs_save) { + if ($entity->getEntityType()->isRevisionable()) { + $entity->setNewRevision(); + } + $entity->save(); + $this->messenger->addStatus($this->t('API Doc %label: imported the + OpenAPI specification file from URL.', [ '%label' => $this->entity->label(), ])); } From c4758b79d3e44b96794c087829a542f3eacb9f9f Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Fri, 10 May 2019 11:49:58 -0700 Subject: [PATCH 35/48] DRUP-459 API Doc open API spec fetcher - rename stuff. --- apigee_api_catalog.links.task.yml | 4 ++-- src/Entity/ApiDoc.php | 1 + src/Entity/Routing/ApiDocHtmlRouteProvider.php | 4 ++-- ...{ApiDocUpdateSpecForm.php => ApiDocReimportSpecForm.php} | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) rename src/Form/{ApiDocUpdateSpecForm.php => ApiDocReimportSpecForm.php} (95%) diff --git a/apigee_api_catalog.links.task.yml b/apigee_api_catalog.links.task.yml index 6f1921e..ff548fe 100755 --- a/apigee_api_catalog.links.task.yml +++ b/apigee_api_catalog.links.task.yml @@ -27,7 +27,7 @@ apidoc.admin: weight: 10 entity.apidoc.reimport_spec_form: - route_name: entity.apidoc.reimport_spec_form - base_route: entity.apidoc.canonical + route_name: entity.apidoc.reimport_spec_form + base_route: entity.apidoc.canonical title: 'Re-import OpenAPI spec' weight: 15 diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index bd19a23..81ce489 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -46,6 +46,7 @@ * "add" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocForm", * "edit" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocForm", * "delete" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocDeleteForm", + * "reimport_spec" = "Drupal\apigee_api_catalog\Form\ApiDocReimportSpecForm", * }, * "access" = "Drupal\apigee_api_catalog\Entity\Access\ApiDocAccessControlHandler", * "route_provider" = { diff --git a/src/Entity/Routing/ApiDocHtmlRouteProvider.php b/src/Entity/Routing/ApiDocHtmlRouteProvider.php index 9556b91..712e3c7 100755 --- a/src/Entity/Routing/ApiDocHtmlRouteProvider.php +++ b/src/Entity/Routing/ApiDocHtmlRouteProvider.php @@ -51,7 +51,7 @@ public function getRoutes(EntityTypeInterface $entity_type) { $apidoc_collection_route->setDefault('_title', $this->t('@entity_type catalog', ['@entity_type' => $entity_type->getLabel()])->render()); } - if ($reimport_spec_route = $this->getUpdateSpecFormRoute($entity_type)) { + if ($reimport_spec_route = $this->getReimportSpecFormRoute($entity_type)) { $collection->add("entity.{$entity_type_id}.reimport_spec_form", $reimport_spec_route); } @@ -91,7 +91,7 @@ protected function getSettingsFormRoute(EntityTypeInterface $entity_type) { * @return \Symfony\Component\Routing\Route|null * The generated route, if available. */ - protected function getUpdateSpecFormRoute(EntityTypeInterface $entity_type) { + protected function getReimportSpecFormRoute(EntityTypeInterface $entity_type) { if ($entity_type->hasLinkTemplate('reimport-spec-form')) { $entity_type_id = $entity_type->id(); $route = new Route($entity_type->getLinkTemplate('reimport-spec-form')); diff --git a/src/Form/ApiDocUpdateSpecForm.php b/src/Form/ApiDocReimportSpecForm.php similarity index 95% rename from src/Form/ApiDocUpdateSpecForm.php rename to src/Form/ApiDocReimportSpecForm.php index b18d3bc..8eab5f7 100644 --- a/src/Form/ApiDocUpdateSpecForm.php +++ b/src/Form/ApiDocReimportSpecForm.php @@ -13,9 +13,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Class ApiDocUpdateSpecForm. + * Class ApiDocReimportSpecForm. */ -class ApiDocUpdateSpecForm extends ContentEntityConfirmFormBase { +class ApiDocReimportSpecForm extends ContentEntityConfirmFormBase { /** * The messenger service. @@ -37,7 +37,7 @@ class ApiDocUpdateSpecForm extends ContentEntityConfirmFormBase { protected $operation = 'reimport_spec'; /** - * Constructs a ApiDocUpdateSpecForm object. + * Constructs a ApiDocReimportSpecForm object. * * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity repository service. From d5bdecb62b5104eea26bccdb4f946fe469080c3b Mon Sep 17 00:00:00 2001 From: Arlina Espinoza Date: Fri, 10 May 2019 11:51:49 -0700 Subject: [PATCH 36/48] DRUP-610 Code sniffer fixes. --- apigee_api_catalog.links.task.yml | 4 ++-- src/ApiDocSpecFetcher.php | 10 ++++++---- src/ApiDocSpecFetcherInterface.php | 3 ++- src/Entity/ApiDoc.php | 16 ++++++++-------- src/Entity/ApiDocInterface.php | 16 ++++++++-------- src/Form/ApiDocReimportSpecForm.php | 4 ++-- 6 files changed, 28 insertions(+), 25 deletions(-) diff --git a/apigee_api_catalog.links.task.yml b/apigee_api_catalog.links.task.yml index ff548fe..1b171b0 100755 --- a/apigee_api_catalog.links.task.yml +++ b/apigee_api_catalog.links.task.yml @@ -15,8 +15,8 @@ entity.apidoc.edit_form: title: 'Edit' entity.apidoc.delete_form: - route_name: entity.apidoc.delete_form - base_route: entity.apidoc.canonical + route_name: entity.apidoc.delete_form + base_route: entity.apidoc.canonical title: 'Delete' weight: 10 diff --git a/src/ApiDocSpecFetcher.php b/src/ApiDocSpecFetcher.php index e10b3ab..64d6513 100644 --- a/src/ApiDocSpecFetcher.php +++ b/src/ApiDocSpecFetcher.php @@ -81,6 +81,8 @@ class ApiDocSpecFetcher implements ApiDocSpecFetcherInterface { * The file_system service. * @param \GuzzleHttp\ClientInterface $http_client * The http_client service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager. * @param \Drupal\Core\Messenger\MessengerInterface $messenger * The messenger service. * @param \Psr\Log\LoggerInterface $logger @@ -97,7 +99,7 @@ public function __construct(FileSystemInterface $file_system, ClientInterface $h /** * {@inheritdoc} */ - public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE) : bool { + public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): bool { $needs_save = FALSE; $spec_value = $apidoc->get('spec')->isEmpty() ? [] : $apidoc->get('spec')->getValue()[0]; @@ -179,9 +181,7 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE) : return FALSE; } - $spec_value = [ - 'target_id' => $file->id(), - ] + $spec_value; + $spec_value = ['target_id' => $file->id()] + $spec_value; $apidoc->set('spec', $spec_value); $apidoc->set('spec_md5', $data_md5); $apidoc->set('fetched_timestamp', time()); @@ -219,7 +219,9 @@ private function log(ApiDocInterface $apidoc, string $type, string $message, arr case static::TYPE_ERROR: $this->logger->error($message, $params); break; + case static::TYPE_STATUS: + default: $this->logger->info($message, $params); } diff --git a/src/ApiDocSpecFetcherInterface.php b/src/ApiDocSpecFetcherInterface.php index fa6bbac..eee60d1 100644 --- a/src/ApiDocSpecFetcherInterface.php +++ b/src/ApiDocSpecFetcherInterface.php @@ -53,5 +53,6 @@ interface ApiDocSpecFetcherInterface { * Returns TRUE if the entity was changed and needs to be saved, FALSE * otherwise. */ - public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE) : bool; + public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): bool; + } diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 81ce489..7a14725 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -83,14 +83,14 @@ class ApiDoc extends ContentEntityBase implements ApiDocInterface { /** * {@inheritdoc} */ - public function getName() : string { + public function getName(): string { return $this->get('name')->value; } /** * {@inheritdoc} */ - public function setName(string $name) : ApiDocInterface { + public function setName(string $name): ApiDocInterface { $this->set('name', $name); return $this; } @@ -98,14 +98,14 @@ public function setName(string $name) : ApiDocInterface { /** * {@inheritdoc} */ - public function getDescription() : string { + public function getDescription(): string { return $this->get('description')->value; } /** * {@inheritdoc} */ - public function setDescription(string $description) : ApiDocInterface { + public function setDescription(string $description): ApiDocInterface { $this->set('description', $description); return $this; } @@ -113,14 +113,14 @@ public function setDescription(string $description) : ApiDocInterface { /** * {@inheritdoc} */ - public function getCreatedTime() : int { + public function getCreatedTime(): int { return $this->get('created')->value; } /** * {@inheritdoc} */ - public function setCreatedTime(int $timestamp) : ApiDocInterface { + public function setCreatedTime(int $timestamp): ApiDocInterface { $this->set('created', $timestamp); return $this; } @@ -128,14 +128,14 @@ public function setCreatedTime(int $timestamp) : ApiDocInterface { /** * {@inheritdoc} */ - public function isPublished() : bool { + public function isPublished(): bool { return (bool) $this->getEntityKey('status'); } /** * {@inheritdoc} */ - public function setPublished(bool $published) : ApiDocInterface { + public function setPublished(bool $published): ApiDocInterface { $this->set('status', $published); return $this; } diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index 3cac91e..e50a7c5 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -48,7 +48,7 @@ interface ApiDocInterface extends ContentEntityInterface, EntityChangedInterface * @return string * Name of the API Doc. */ - public function getName() : string; + public function getName(): string; /** * Sets the API Doc name. @@ -59,7 +59,7 @@ public function getName() : string; * @return \Drupal\apigee_api_catalog\Entity\ApiDocInterface * The called API Doc entity. */ - public function setName(string $name) : self; + public function setName(string $name): self; /** * Gets the description. @@ -67,7 +67,7 @@ public function setName(string $name) : self; * @return null|string * The API Doc description. */ - public function getDescription() : string; + public function getDescription(): string; /** * Sets the description. @@ -78,7 +78,7 @@ public function getDescription() : string; * @return \Drupal\apigee_api_catalog\Entity\ApiDocInterface * The API Doc entity. */ - public function setDescription(string $description) : self; + public function setDescription(string $description): self; /** * Gets the API Doc creation timestamp. @@ -86,7 +86,7 @@ public function setDescription(string $description) : self; * @return int * Creation timestamp of the API Doc. */ - public function getCreatedTime() : int; + public function getCreatedTime(): int; /** * Sets the API Doc creation timestamp. @@ -97,7 +97,7 @@ public function getCreatedTime() : int; * @return \Drupal\apigee_api_catalog\Entity\ApiDocInterface * The called API Doc entity. */ - public function setCreatedTime(int $timestamp) : self; + public function setCreatedTime(int $timestamp): self; /** * Returns the API Doc published status indicator. @@ -107,7 +107,7 @@ public function setCreatedTime(int $timestamp) : self; * @return bool * TRUE if the API Doc is published. */ - public function isPublished() : bool; + public function isPublished(): bool; /** * Sets the published status of a API Doc. @@ -118,6 +118,6 @@ public function isPublished() : bool; * @return \Drupal\apigee_api_catalog\Entity\ApiDocInterface * The called API Doc entity. */ - public function setPublished(bool $published) : self; + public function setPublished(bool $published): self; } diff --git a/src/Form/ApiDocReimportSpecForm.php b/src/Form/ApiDocReimportSpecForm.php index 8eab5f7..c4931a6 100644 --- a/src/Form/ApiDocReimportSpecForm.php +++ b/src/Form/ApiDocReimportSpecForm.php @@ -55,6 +55,7 @@ public function __construct(EntityRepositoryInterface $entity_repository, Entity $this->messenger = $messenger; $this->specFetcher = $spec_fetcher; } + /** * {@inheritdoc} */ @@ -112,8 +113,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $entity->setNewRevision(); } $entity->save(); - $this->messenger->addStatus($this->t('API Doc %label: imported the - OpenAPI specification file from URL.', [ + $this->messenger->addStatus($this->t('API Doc %label: imported the OpenAPI specification file from URL.', [ '%label' => $this->entity->label(), ])); } From b170d1ba2ffee685e944b5cc745b3dc5b49626fd Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Tue, 14 May 2019 10:26:27 -0700 Subject: [PATCH 37/48] [DRUP-610] Fixes the `apidoc` entity test. --- tests/src/Kernel/ApidocEntityTest.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/src/Kernel/ApidocEntityTest.php b/tests/src/Kernel/ApidocEntityTest.php index ffb1fb3..8d52a74 100755 --- a/tests/src/Kernel/ApidocEntityTest.php +++ b/tests/src/Kernel/ApidocEntityTest.php @@ -32,11 +32,14 @@ class ApidocEntityTest extends KernelTestBase { protected static $modules = [ 'user', - 'text', - 'file', + 'system', 'apigee_edge', 'key', 'apigee_api_catalog', + 'options', + 'text', + 'file', + 'file_link', ]; /** From 245de2aed60efa49b640e4cb6b9620b42ebc2461 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Tue, 14 May 2019 10:29:03 -0700 Subject: [PATCH 38/48] [DRUP-610] Adds access control to the reimport `apidoc` entity operation. --- .../Access/ApiDocAccessControlHandler.php | 33 ++++++++++--------- .../Routing/ApiDocHtmlRouteProvider.php | 9 +++-- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/Entity/Access/ApiDocAccessControlHandler.php b/src/Entity/Access/ApiDocAccessControlHandler.php index 35f83e0..bd5348b 100755 --- a/src/Entity/Access/ApiDocAccessControlHandler.php +++ b/src/Entity/Access/ApiDocAccessControlHandler.php @@ -20,6 +20,7 @@ namespace Drupal\apigee_api_catalog\Entity\Access; +use Drupal\apigee_api_catalog\Entity\ApiDocInterface; use Drupal\Core\Entity\EntityAccessControlHandler; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; @@ -36,27 +37,29 @@ class ApiDocAccessControlHandler extends EntityAccessControlHandler { * {@inheritdoc} */ protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { - $parent_access = parent::checkAccess($entity, $operation, $account); + /** @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ + $access = parent::checkAccess($entity, $operation, $account); - if (!$parent_access->isAllowed()) { - /** @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ - switch ($operation) { - case 'view': - if (!$entity->isPublished()) { - return $parent_access->orIf(AccessResult::allowedIfHasPermission($account, 'view unpublished apidoc entities')); - } - return $parent_access->orIf(AccessResult::allowedIfHasPermission($account, 'view published apidoc entities')); + switch ($operation) { + case 'view': + return $access->orIf($entity->isPublished() + ? AccessResult::allowedIfHasPermission($account, 'view published apidoc entities') + : AccessResult::allowedIfHasPermission($account, 'view unpublished apidoc entities') + ); - case 'update': - return $parent_access->orIf(AccessResult::allowedIfHasPermission($account, 'edit apidoc entities')); + case 'reimport': + return AccessResult::allowedIf($entity->spec_file_source->value === ApiDocInterface::SPEC_AS_URL) + ->andIf($entity->access('update', $account, TRUE)); - case 'delete': - return $parent_access->orIf(AccessResult::allowedIfHasPermission($account, 'delete apidoc entities')); - } + case 'update': + return $access->orIf(AccessResult::allowedIfHasPermission($account, 'edit apidoc entities')); + + case 'delete': + return $access->orIf(AccessResult::allowedIfHasPermission($account, 'delete apidoc entities')); } // Unknown operation, no opinion. - return $parent_access; + return $access; } /** diff --git a/src/Entity/Routing/ApiDocHtmlRouteProvider.php b/src/Entity/Routing/ApiDocHtmlRouteProvider.php index 712e3c7..8e0c7bb 100755 --- a/src/Entity/Routing/ApiDocHtmlRouteProvider.php +++ b/src/Entity/Routing/ApiDocHtmlRouteProvider.php @@ -93,7 +93,6 @@ protected function getSettingsFormRoute(EntityTypeInterface $entity_type) { */ protected function getReimportSpecFormRoute(EntityTypeInterface $entity_type) { if ($entity_type->hasLinkTemplate('reimport-spec-form')) { - $entity_type_id = $entity_type->id(); $route = new Route($entity_type->getLinkTemplate('reimport-spec-form')); // Use the reimport_spec form handler. if ($entity_type->getFormClass('reimport_spec')) { @@ -101,18 +100,18 @@ protected function getReimportSpecFormRoute(EntityTypeInterface $entity_type) { } $route ->setDefaults([ - '_entity_form' => "{$entity_type_id}.{$operation}", + '_entity_form' => "apidoc.{$operation}", '_title' => 'Re-import API Doc OpenAPI specification', ]) - ->setRequirement('_entity_access', "{$entity_type_id}.update") + ->setRequirement('_entity_access', "apidoc.reimport") ->setOption('parameters', [ - $entity_type_id => ['type' => 'entity:' . $entity_type_id], + 'apidoc' => ['type' => 'entity:apidoc'], ]); // Entity types with serial IDs can specify this in their route // requirements, improving the matching process. if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') { - $route->setRequirement($entity_type_id, '\d+'); + $route->setRequirement('apidoc', '\d+'); } return $route; } From 078a98b0ae5a94d2dcb4c61bd2b5fbd0012b5d6a Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Wed, 15 May 2019 18:49:14 -0700 Subject: [PATCH 39/48] [DRUP-610] Rename the spec fetch er service. --- apigee_api_catalog.services.yml | 2 +- src/Form/ApiDocReimportSpecForm.php | 8 ++++---- src/{ApiDocSpecFetcher.php => SpecFetcher.php} | 8 ++++---- ...cSpecFetcherInterface.php => SpecFetcherInterface.php} | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) rename src/{ApiDocSpecFetcher.php => SpecFetcher.php} (98%) rename src/{ApiDocSpecFetcherInterface.php => SpecFetcherInterface.php} (95%) diff --git a/apigee_api_catalog.services.yml b/apigee_api_catalog.services.yml index af208ca..a10c8f4 100644 --- a/apigee_api_catalog.services.yml +++ b/apigee_api_catalog.services.yml @@ -5,5 +5,5 @@ services: arguments: ['apigee_api_catalog'] apigee_api_catalog.spec_fetcher: - class: Drupal\apigee_api_catalog\ApiDocSpecFetcher + class: Drupal\apigee_api_catalog\SpecFetcher arguments: ['@file_system', '@http_client', '@entity_type.manager', '@messenger', '@logger.channel.apigee_api_catalog'] diff --git a/src/Form/ApiDocReimportSpecForm.php b/src/Form/ApiDocReimportSpecForm.php index c4931a6..88dae31 100644 --- a/src/Form/ApiDocReimportSpecForm.php +++ b/src/Form/ApiDocReimportSpecForm.php @@ -2,7 +2,7 @@ namespace Drupal\apigee_api_catalog\Form; -use Drupal\apigee_api_catalog\ApiDocSpecFetcherInterface; +use Drupal\apigee_api_catalog\SpecFetcherInterface; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\ContentEntityConfirmFormBase; use Drupal\Core\Entity\EntityRepositoryInterface; @@ -27,7 +27,7 @@ class ApiDocReimportSpecForm extends ContentEntityConfirmFormBase { /** * The ApiDoc spec fetcher service. * - * @var \Drupal\apigee_api_catalog\ApiDocSpecFetcherInterface + * @var \Drupal\apigee_api_catalog\SpecFetcherInterface */ protected $specFetcher; @@ -47,10 +47,10 @@ class ApiDocReimportSpecForm extends ContentEntityConfirmFormBase { * The time service. * @param \Drupal\Core\Messenger\MessengerInterface $messenger * The messenger service. - * @param \Drupal\apigee_api_catalog\ApiDocSpecFetcherInterface $spec_fetcher + * @param \Drupal\apigee_api_catalog\SpecFetcherInterface $spec_fetcher * The ApiDoc spec fetcher service. */ - public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, MessengerInterface $messenger, ApiDocSpecFetcherInterface $spec_fetcher) { + public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL, MessengerInterface $messenger, SpecFetcherInterface $spec_fetcher) { parent::__construct($entity_repository, $entity_type_bundle_info, $time); $this->messenger = $messenger; $this->specFetcher = $spec_fetcher; diff --git a/src/ApiDocSpecFetcher.php b/src/SpecFetcher.php similarity index 98% rename from src/ApiDocSpecFetcher.php rename to src/SpecFetcher.php index 64d6513..0e4e75d 100644 --- a/src/ApiDocSpecFetcher.php +++ b/src/SpecFetcher.php @@ -1,6 +1,6 @@ Date: Wed, 15 May 2019 19:00:14 -0700 Subject: [PATCH 40/48] [Cleanup] Move entity classes under `Entity`. --- src/Entity/ApiDoc.php | 2 +- src/{ => Entity}/Form/ApiDocReimportSpecForm.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/{ => Entity}/Form/ApiDocReimportSpecForm.php (98%) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 7a14725..e3c8274 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -46,7 +46,7 @@ * "add" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocForm", * "edit" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocForm", * "delete" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocDeleteForm", - * "reimport_spec" = "Drupal\apigee_api_catalog\Form\ApiDocReimportSpecForm", + * "reimport_spec" = "Drupal\apigee_api_catalog\Entity\Form\ApiDocReimportSpecForm", * }, * "access" = "Drupal\apigee_api_catalog\Entity\Access\ApiDocAccessControlHandler", * "route_provider" = { diff --git a/src/Form/ApiDocReimportSpecForm.php b/src/Entity/Form/ApiDocReimportSpecForm.php similarity index 98% rename from src/Form/ApiDocReimportSpecForm.php rename to src/Entity/Form/ApiDocReimportSpecForm.php index 88dae31..9e41903 100644 --- a/src/Form/ApiDocReimportSpecForm.php +++ b/src/Entity/Form/ApiDocReimportSpecForm.php @@ -1,6 +1,6 @@ Date: Wed, 15 May 2019 23:46:39 -0700 Subject: [PATCH 41/48] [DRUP-610] Optimizations/suggestions from `https://github.com/apigee/apigee-edge-drupal/pull/191`. --- README.md | 6 +-- src/Entity/ApiDoc.php | 2 +- src/Entity/ApiDocInterface.php | 4 +- src/Entity/Form/ApiDocReimportSpecForm.php | 9 +--- src/Entity/ListBuilder/ApiDocListBuilder.php | 2 +- .../Routing/ApiDocHtmlRouteProvider.php | 43 ++++++++----------- .../Constraint/ApiDocFileLinkConstraint.php | 7 +++ .../ApiDocFileLinkConstraintValidator.php | 8 ++-- src/SpecFetcher.php | 6 +-- src/SpecFetcherInterface.php | 4 +- 10 files changed, 42 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 2cd4fd0..6745a81 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ To render the OpenAPI spec using Swagger UI: The API Doc is an entity, you can configure it at __Configuration > API catalog__ in the admin menu. -The "APIs" menu link is a view, you can modify it by editing the "API Catalog" view +The "APIs" menu link is a view, you can modify it by editing the "API Catalog" view under Structure > Views in the admin menu. ## Planned Features @@ -45,10 +45,10 @@ under Structure > Views in the admin menu. ## Installing -This module must be installed on a Drupal site that is managed by Composer. Drupal.org has documentation on how to +This module must be installed on a Drupal site that is managed by Composer. Drupal.org has documentation on how to [use Composer to manage Drupal site dependencies](https://www.drupal.org/docs/develop/using-composer/using-composer-to-manage-drupal-site-dependencies) to get you started quickly. - + 1. Install the module using [Composer](https://getcomposer.org/). Composer will download the this module and all its dependencies. **Note**: Composer must be executed at the root of your Drupal installation. diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index e3c8274..4d0f2c6 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -292,7 +292,7 @@ public function preSave(EntityStorageInterface $storage) { \Drupal::service('apigee_api_catalog.spec_fetcher')->fetchSpec($this, FALSE, FALSE); // Update spec_md5 value if using "file" as source. - if ($this->get('spec_file_source')->value == static::SPEC_AS_FILE) { + if ($this->get('spec_file_source')->value === static::SPEC_AS_FILE) { $spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0]; if (!empty($spec_value['target_id'])) { /* @var \Drupal\file\Entity\File $file */ diff --git a/src/Entity/ApiDocInterface.php b/src/Entity/ApiDocInterface.php index e50a7c5..506b1e8 100755 --- a/src/Entity/ApiDocInterface.php +++ b/src/Entity/ApiDocInterface.php @@ -33,14 +33,14 @@ interface ApiDocInterface extends ContentEntityInterface, EntityChangedInterface * * @var string */ - const SPEC_AS_FILE = 'file'; + public const SPEC_AS_FILE = 'file'; /** * The value of "spec_file_source" when it uses a URL as source. * * @var string */ - const SPEC_AS_URL = 'url'; + public const SPEC_AS_URL = 'url'; /** * Gets the API Doc name. diff --git a/src/Entity/Form/ApiDocReimportSpecForm.php b/src/Entity/Form/ApiDocReimportSpecForm.php index 9e41903..2e3853b 100644 --- a/src/Entity/Form/ApiDocReimportSpecForm.php +++ b/src/Entity/Form/ApiDocReimportSpecForm.php @@ -93,13 +93,6 @@ public function getDescription() { This action cannot be undone.'); } - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - parent::validateForm($form, $form_state); - } - /** * {@inheritdoc} */ @@ -113,7 +106,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $entity->setNewRevision(); } $entity->save(); - $this->messenger->addStatus($this->t('API Doc %label: imported the OpenAPI specification file from URL.', [ + $this->messenger()->addStatus($this->t('API Doc %label: imported the OpenAPI specification file from URL.', [ '%label' => $this->entity->label(), ])); } diff --git a/src/Entity/ListBuilder/ApiDocListBuilder.php b/src/Entity/ListBuilder/ApiDocListBuilder.php index 537dab7..2c6ce64 100755 --- a/src/Entity/ListBuilder/ApiDocListBuilder.php +++ b/src/Entity/ListBuilder/ApiDocListBuilder.php @@ -89,7 +89,7 @@ public function getOperations(EntityInterface $entity) { $operations = parent::getOperations($entity); // Add "Re-import OpenAPI spec" link. - if ($entity->access('update') && $entity->hasLinkTemplate('reimport-spec-form')) { + if ($entity->access('reimport')) { $operations['reimport_spec'] = [ 'title' => $this->t('Re-import OpenAPI spec'), 'weight' => 15, diff --git a/src/Entity/Routing/ApiDocHtmlRouteProvider.php b/src/Entity/Routing/ApiDocHtmlRouteProvider.php index 8e0c7bb..fa9e861 100755 --- a/src/Entity/Routing/ApiDocHtmlRouteProvider.php +++ b/src/Entity/Routing/ApiDocHtmlRouteProvider.php @@ -41,7 +41,6 @@ class ApiDocHtmlRouteProvider extends AdminHtmlRouteProvider { */ public function getRoutes(EntityTypeInterface $entity_type) { $collection = parent::getRoutes($entity_type); - $entity_type_id = $entity_type->id(); if ($settings_form_route = $this->getSettingsFormRoute($entity_type)) { $collection->add('entity.apidoc.settings', $settings_form_route); @@ -52,7 +51,7 @@ public function getRoutes(EntityTypeInterface $entity_type) { } if ($reimport_spec_route = $this->getReimportSpecFormRoute($entity_type)) { - $collection->add("entity.{$entity_type_id}.reimport_spec_form", $reimport_spec_route); + $collection->add("entity.apidoc.reimport_spec_form", $reimport_spec_route); } return $collection; @@ -92,29 +91,25 @@ protected function getSettingsFormRoute(EntityTypeInterface $entity_type) { * The generated route, if available. */ protected function getReimportSpecFormRoute(EntityTypeInterface $entity_type) { - if ($entity_type->hasLinkTemplate('reimport-spec-form')) { - $route = new Route($entity_type->getLinkTemplate('reimport-spec-form')); - // Use the reimport_spec form handler. - if ($entity_type->getFormClass('reimport_spec')) { - $operation = 'reimport_spec'; - } - $route - ->setDefaults([ - '_entity_form' => "apidoc.{$operation}", - '_title' => 'Re-import API Doc OpenAPI specification', - ]) - ->setRequirement('_entity_access', "apidoc.reimport") - ->setOption('parameters', [ - 'apidoc' => ['type' => 'entity:apidoc'], - ]); - - // Entity types with serial IDs can specify this in their route - // requirements, improving the matching process. - if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') { - $route->setRequirement('apidoc', '\d+'); - } - return $route; + $route = new Route($entity_type->getLinkTemplate('reimport-spec-form')); + + $route + ->setDefaults([ + '_entity_form' => "apidoc.reimport_spec", + '_title' => 'Re-import API Doc OpenAPI specification', + ]) + ->setRequirement('_entity_access', "apidoc.reimport") + ->setOption('parameters', [ + 'apidoc' => ['type' => 'entity:apidoc'], + ]); + + // Entity types with serial IDs can specify this in their route + // requirements, improving the matching process. + if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') { + $route->setRequirement('apidoc', '\d+'); } + + return $route; } } diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php index ecc9e3c..1810512 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraint.php @@ -40,4 +40,11 @@ class ApiDocFileLinkConstraint extends Constraint { */ public $notValid = '%value is not a valid link'; + /** + * Message to be shown when it is not a valid link. + * + * @var string + */ + public $urlParseError = 'The following error occurred while getting the link URL: @error'; + } diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php index 4ebebe7..7ad2882 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php @@ -72,14 +72,12 @@ public function validate($items, Constraint $constraint) { continue; } - $uri = $item->getValue()['uri']; - - // Try to resolve the given URI to a URL. It may fail if it's schemeless. + // Try to resolve the given URI to a URL. It may fail if it's scheme-less. try { - $url = Url::fromUri($uri, ['absolute' => TRUE])->toString(); + $url = Url::fromUri($item->getValue()['uri'], ['absolute' => TRUE])->toString(); } catch (\InvalidArgumentException $e) { - $this->context->addViolation("The following error occurred while getting the link URL: @error", ['@error' => $e->getMessage()]); + $this->context->addViolation($constraint->urlParseError, ['@error' => $e->getMessage()]); return; } diff --git a/src/SpecFetcher.php b/src/SpecFetcher.php index 0e4e75d..0aaf366 100644 --- a/src/SpecFetcher.php +++ b/src/SpecFetcher.php @@ -106,7 +106,7 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): // If "spec_file_source" uses URL, grab file from "file_link" and save it // into the "spec" file field. The file_link field should already have // validated that a valid file exists at that URL. - if ($apidoc->get('spec_file_source')->value == ApiDocInterface::SPEC_AS_URL) { + if ($apidoc->get('spec_file_source')->value === ApiDocInterface::SPEC_AS_URL) { // If the file_link field is empty, return without changes. if ($apidoc->get('file_link')->isEmpty()) { @@ -133,7 +133,7 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): // In case of a 304 Not Modified there are no changes, but update // last fetched timestamp. - if ($response->getStatusCode() == 304) { + if ($response->getStatusCode() === 304) { $apidoc->set('fetched_timestamp', time()); return TRUE; } @@ -190,7 +190,7 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): } } - elseif ($apidoc->get('spec_file_source')->value == ApiDocInterface::SPEC_AS_FILE) { + elseif ($apidoc->get('spec_file_source')->value === ApiDocInterface::SPEC_AS_FILE) { $this->log($apidoc, static::TYPE_STATUS, 'API Doc %label is using a file upload as source. Nothing to update.', [ '%label' => $apidoc->label(), ], $show_messages); diff --git a/src/SpecFetcherInterface.php b/src/SpecFetcherInterface.php index 9975d2e..7007e1e 100644 --- a/src/SpecFetcherInterface.php +++ b/src/SpecFetcherInterface.php @@ -30,12 +30,12 @@ interface SpecFetcherInterface { /** * A status message. */ - const TYPE_STATUS = 'status'; + public const TYPE_STATUS = 'status'; /** * An error. */ - const TYPE_ERROR = 'error'; + public const TYPE_ERROR = 'error'; /** * Fetch OpenAPI specification file from URL. From ffd3900d8d2e615d3a638af5fb5203db3bd90441 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Thu, 16 May 2019 00:55:47 -0700 Subject: [PATCH 42/48] [DRUP-610] Simplify logging sand remove unneeded parameters. --- src/Entity/Form/ApiDocReimportSpecForm.php | 2 +- src/SpecFetcher.php | 55 ++++++++-------------- src/SpecFetcherInterface.php | 22 ++++----- 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/Entity/Form/ApiDocReimportSpecForm.php b/src/Entity/Form/ApiDocReimportSpecForm.php index 2e3853b..ed193c8 100644 --- a/src/Entity/Form/ApiDocReimportSpecForm.php +++ b/src/Entity/Form/ApiDocReimportSpecForm.php @@ -100,7 +100,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { /* @var \Drupal\apigee_api_catalog\Entity\ApiDocInterface $entity */ $entity = $this->getEntity(); - $needs_save = $this->specFetcher->fetchSpec($entity, TRUE); + $needs_save = $this->specFetcher->fetchSpec($entity); if ($needs_save) { if ($entity->getEntityType()->isRevisionable()) { $entity->setNewRevision(); diff --git a/src/SpecFetcher.php b/src/SpecFetcher.php index 0aaf366..411f722 100644 --- a/src/SpecFetcher.php +++ b/src/SpecFetcher.php @@ -22,15 +22,18 @@ use Drupal\apigee_api_catalog\Entity\ApiDocInterface; use Drupal\Component\Datetime\DateTimePlus; +use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Messenger\MessengerTrait; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; /** * Class SpecFetcher. @@ -38,6 +41,7 @@ class SpecFetcher implements SpecFetcherInterface { use StringTranslationTrait; + use MessengerTrait; /** * Drupal\Core\File\FileSystemInterface definition. @@ -60,13 +64,6 @@ class SpecFetcher implements SpecFetcherInterface { */ protected $entityTypeManager; - /** - * Drupal\Core\Messenger\MessengerInterface definition. - * - * @var \Drupal\Core\Messenger\MessengerInterface - */ - protected $messenger; - /** * The logger. * @@ -99,7 +96,7 @@ public function __construct(FileSystemInterface $file_system, ClientInterface $h /** * {@inheritdoc} */ - public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): bool { + public function fetchSpec(ApiDocInterface $apidoc): bool { $needs_save = FALSE; $spec_value = $apidoc->get('spec')->isEmpty() ? [] : $apidoc->get('spec')->getValue()[0]; @@ -139,19 +136,19 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): } } catch (RequestException $e) { - $this->log($apidoc, static::TYPE_ERROR, 'API Doc %label: Could not retrieve OpenAPI specification file located at %url.', [ + $this->log(LogLevel::ERROR, 'API Doc %label: Could not retrieve OpenAPI specification file located at %url.', [ '%url' => $file_uri, '%label' => $apidoc->label(), - ], $show_messages); + ]); return FALSE; } $data = (string) $response->getBody(); if (empty($data)) { - $this->log($apidoc, static::TYPE_ERROR, 'API Doc %label: OpenAPI specification file located at %url is empty.', [ + $this->log(LogLevel::ERROR, 'API Doc %label: OpenAPI specification file located at %url is empty.', [ '%url' => $file_uri, '%label' => $apidoc->label(), - ], $show_messages); + ]); return FALSE; } @@ -174,10 +171,10 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): } } catch (\Exception $e) { - $this->log($apidoc, static::TYPE_ERROR, 'Error while saving API Doc spec file from URL on API Doc ID: %id. Error: %error', [ + $this->log(LogLevel::ERROR, 'Error while saving API Doc spec file from URL on API Doc ID: %id. Error: %error', [ '%id' => $apidoc->id(), '%error' => $e->getMessage(), - ], $show_messages); + ]); return FALSE; } @@ -191,9 +188,9 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): } elseif ($apidoc->get('spec_file_source')->value === ApiDocInterface::SPEC_AS_FILE) { - $this->log($apidoc, static::TYPE_STATUS, 'API Doc %label is using a file upload as source. Nothing to update.', [ + $this->log(LogLevel::INFO, 'API Doc %label is using a file upload as source. Nothing to update.', [ '%label' => $apidoc->label(), - ], $show_messages); + ]); $needs_save = FALSE; } @@ -203,31 +200,17 @@ public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): /** * Log a message, and optionally display it on the UI. * - * @param \Drupal\apigee_api_catalog\Entity\ApiDocInterface $apidoc - * The API Doc entity. - * @param string $type - * Type of message. + * @param string $level + * The Error level. * @param string $message * The message. * @param array $params * Optional parameters array. - * @param bool $show_messages - * TRUE if message should be displayed to the UI as well. */ - private function log(ApiDocInterface $apidoc, string $type, string $message, array $params = [], bool $show_messages = TRUE) { - switch ($type) { - case static::TYPE_ERROR: - $this->logger->error($message, $params); - break; - - case static::TYPE_STATUS: - - default: - $this->logger->info($message, $params); - } - if ($show_messages) { - $this->messenger->addMessage($this->t($message, $params), $type); - } + private function log(string $level, string $message, array $params = []) { + $this->logger->log($level, $message, $params); + // Show the message. + $this->messenger()->addMessage(new FormattableMarkup($message, $params), static::LOG_LEVEL_MAP[$level] ?? MessengerInterface::TYPE_ERROR); } /** diff --git a/src/SpecFetcherInterface.php b/src/SpecFetcherInterface.php index 7007e1e..d5f5c45 100644 --- a/src/SpecFetcherInterface.php +++ b/src/SpecFetcherInterface.php @@ -21,21 +21,21 @@ namespace Drupal\apigee_api_catalog; use Drupal\apigee_api_catalog\Entity\ApiDocInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Psr\Log\LogLevel; /** * Interface SpecFetcherInterface. */ interface SpecFetcherInterface { - /** - * A status message. - */ - public const TYPE_STATUS = 'status'; - - /** - * An error. - */ - public const TYPE_ERROR = 'error'; + public const LOG_LEVEL_MAP = [ + LogLevel::ALERT => MessengerInterface::TYPE_WARNING, + LogLevel::WARNING => MessengerInterface::TYPE_WARNING, + LogLevel::NOTICE => MessengerInterface::TYPE_STATUS, + LogLevel::INFO => MessengerInterface::TYPE_STATUS, + LogLevel::DEBUG => MessengerInterface::TYPE_STATUS, + ]; /** * Fetch OpenAPI specification file from URL. @@ -46,13 +46,11 @@ interface SpecFetcherInterface { * * @param \Drupal\apigee_api_catalog\Entity\ApiDocInterface $apidoc * The ApiDoc entity. - * @param bool $show_messages - * Boolean indicating if method should display status messages. * * @return bool * Returns TRUE if the entity was changed and needs to be saved, FALSE * otherwise. */ - public function fetchSpec(ApiDocInterface $apidoc, bool $show_messages = TRUE): bool; + public function fetchSpec(ApiDocInterface $apidoc): bool; } From d373b6fa142b868b5ea2f777b1914b07cb77929a Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Thu, 16 May 2019 01:20:22 -0700 Subject: [PATCH 43/48] =?UTF-8?q?[DRUP-610]=20No=20need=20to=20log=20?= =?UTF-8?q?=E2=80=9Cnothing=20to=20do=E2=80=9D=20during=20`fetchSpec`=20fo?= =?UTF-8?q?r=20file=20`apidoc`=20entities.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Entity/ApiDoc.php | 2 +- src/SpecFetcher.php | 28 ++++++++++------------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index 4d0f2c6..f858f8e 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -291,7 +291,7 @@ public function preSave(EntityStorageInterface $storage) { \Drupal::service('apigee_api_catalog.spec_fetcher')->fetchSpec($this, FALSE, FALSE); - // Update spec_md5 value if using "file" as source. + // API docs that use the "file" source will still need their md5 updated. if ($this->get('spec_file_source')->value === static::SPEC_AS_FILE) { $spec_value = $this->get('spec')->isEmpty() ? [] : $this->get('spec')->getValue()[0]; if (!empty($spec_value['target_id'])) { diff --git a/src/SpecFetcher.php b/src/SpecFetcher.php index 411f722..2752161 100644 --- a/src/SpecFetcher.php +++ b/src/SpecFetcher.php @@ -97,7 +97,6 @@ public function __construct(FileSystemInterface $file_system, ClientInterface $h * {@inheritdoc} */ public function fetchSpec(ApiDocInterface $apidoc): bool { - $needs_save = FALSE; $spec_value = $apidoc->get('spec')->isEmpty() ? [] : $apidoc->get('spec')->getValue()[0]; // If "spec_file_source" uses URL, grab file from "file_link" and save it @@ -110,9 +109,9 @@ public function fetchSpec(ApiDocInterface $apidoc): bool { return FALSE; } - $file_uri = $apidoc->get('file_link')->getValue()[0]['uri']; - $file_uri = Url::fromUri($file_uri, ['absolute' => TRUE])->toString(); - $request = new Request('GET', $file_uri); + $source_uri = $apidoc->get('file_link')->getValue()[0]['uri']; + $source_uri = Url::fromUri($source_uri, ['absolute' => TRUE])->toString(); + $request = new Request('GET', $source_uri); $options = [ 'exceptions' => TRUE, 'allow_redirects' => [ @@ -137,16 +136,16 @@ public function fetchSpec(ApiDocInterface $apidoc): bool { } catch (RequestException $e) { $this->log(LogLevel::ERROR, 'API Doc %label: Could not retrieve OpenAPI specification file located at %url.', [ - '%url' => $file_uri, + '%url' => $source_uri, '%label' => $apidoc->label(), ]); return FALSE; } $data = (string) $response->getBody(); - if (empty($data)) { + if (($file_size = $response->getBody()->getSize()) && $file_size < 1) { $this->log(LogLevel::ERROR, 'API Doc %label: OpenAPI specification file located at %url is empty.', [ - '%url' => $file_uri, + '%url' => $source_uri, '%label' => $apidoc->label(), ]); return FALSE; @@ -156,7 +155,7 @@ public function fetchSpec(ApiDocInterface $apidoc): bool { $data_md5 = md5($data); $prev_md5 = $apidoc->get('spec_md5')->isEmpty() ? NULL : $apidoc->get('spec_md5')->value; if ($prev_md5 != $data_md5) { - $filename = $this->fileSystem->basename($file_uri); + $filename = $this->fileSystem->basename($source_uri); $specs_definition = $apidoc->getFieldDefinition('spec')->getItemDefinition(); $target_dir = $specs_definition->getSetting('file_directory'); $uri_scheme = $specs_definition->getSetting('uri_scheme'); @@ -164,7 +163,7 @@ public function fetchSpec(ApiDocInterface $apidoc): bool { try { $this->checkRequirements($destination); - $file = file_save_data($data, $destination . $filename, FILE_EXISTS_RENAME); + $file = file_save_data($data, $destination . $filename, FileSystemInterface::EXISTS_RENAME); if (empty($file)) { throw new \Exception('Could not save API Doc specification file.'); @@ -183,18 +182,11 @@ public function fetchSpec(ApiDocInterface $apidoc): bool { $apidoc->set('spec_md5', $data_md5); $apidoc->set('fetched_timestamp', time()); - $needs_save = TRUE; + return TRUE; } } - elseif ($apidoc->get('spec_file_source')->value === ApiDocInterface::SPEC_AS_FILE) { - $this->log(LogLevel::INFO, 'API Doc %label is using a file upload as source. Nothing to update.', [ - '%label' => $apidoc->label(), - ]); - $needs_save = FALSE; - } - - return $needs_save; + return FALSE; } /** From d82351296ee8c8e40acb03fbec4174fa81b8f974 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Thu, 16 May 2019 12:19:53 -0700 Subject: [PATCH 44/48] [DRUP-610] Adds todo about validation. --- src/Entity/Form/ApiDocForm.php | 1 + src/Entity/Form/ApiDocReimportSpecForm.php | 3 +++ src/SpecFetcher.php | 1 + 3 files changed, 5 insertions(+) diff --git a/src/Entity/Form/ApiDocForm.php b/src/Entity/Form/ApiDocForm.php index d4b901c..722cc4b 100755 --- a/src/Entity/Form/ApiDocForm.php +++ b/src/Entity/Form/ApiDocForm.php @@ -41,6 +41,7 @@ public function form(array $form, FormStateInterface $form_state) { '#weight' => $form['spec_file_source']['#weight'], ]; + // TODO: Required states are not working and no server side validation. $form['spec']['#states'] = [ 'visible' => [ ':input[name="spec_file_source"]' => ['value' => ApiDocInterface::SPEC_AS_FILE], diff --git a/src/Entity/Form/ApiDocReimportSpecForm.php b/src/Entity/Form/ApiDocReimportSpecForm.php index ed193c8..8c65fa8 100644 --- a/src/Entity/Form/ApiDocReimportSpecForm.php +++ b/src/Entity/Form/ApiDocReimportSpecForm.php @@ -110,6 +110,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { '%label' => $this->entity->label(), ])); } + // TODO: $needs_save doesn't tell us if something went wrong. + // If the file wasn't changed we should notify, if something went wrong, we + // should notify what went wrong. } } diff --git a/src/SpecFetcher.php b/src/SpecFetcher.php index 2752161..64d0466 100644 --- a/src/SpecFetcher.php +++ b/src/SpecFetcher.php @@ -105,6 +105,7 @@ public function fetchSpec(ApiDocInterface $apidoc): bool { if ($apidoc->get('spec_file_source')->value === ApiDocInterface::SPEC_AS_URL) { // If the file_link field is empty, return without changes. + // TODO: The file link shouldn't be empty. Consider throwing an error. if ($apidoc->get('file_link')->isEmpty()) { return FALSE; } From ec769ac3fc50189fd5ba8d49ffccef75ce9316af Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Thu, 16 May 2019 17:34:07 -0700 Subject: [PATCH 45/48] [DRUP-610] Require the `file_link` module in `composer.json`. --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9380b2e..3b1b930 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "description": "Apigee API Catalog for Drupal", "require": { "php": ">=7.1", - "drupal/apigee_edge": "~1.0-rc4" + "drupal/apigee_edge": "~1.0-rc4", + "drupal/file_link": "~1.0" }, "require-dev": { "drupal/coder": "^8.3", From f0db112c87dcc01e5d2a4110f9d1426d59170447 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Thu, 23 May 2019 14:36:30 -0700 Subject: [PATCH 46/48] [DRUP-610] Remove unnecessary trait from the APIDOc entity. --- src/Entity/ApiDoc.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Entity/ApiDoc.php b/src/Entity/ApiDoc.php index f858f8e..6817c0e 100755 --- a/src/Entity/ApiDoc.php +++ b/src/Entity/ApiDoc.php @@ -25,7 +25,6 @@ use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\link\LinkItemInterface; /** @@ -78,7 +77,6 @@ class ApiDoc extends ContentEntityBase implements ApiDocInterface { use EntityChangedTrait; - use StringTranslationTrait; /** * {@inheritdoc} From 8a0d7d292fd1551241f818414ad2c908bea56fbe Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Fri, 24 May 2019 17:31:15 -0700 Subject: [PATCH 47/48] [DRUP-610] Use string translation in spec fetcher log messages. --- apigee_api_catalog.services.yml | 2 +- src/SpecFetcher.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apigee_api_catalog.services.yml b/apigee_api_catalog.services.yml index a10c8f4..fdbae5a 100644 --- a/apigee_api_catalog.services.yml +++ b/apigee_api_catalog.services.yml @@ -6,4 +6,4 @@ services: apigee_api_catalog.spec_fetcher: class: Drupal\apigee_api_catalog\SpecFetcher - arguments: ['@file_system', '@http_client', '@entity_type.manager', '@messenger', '@logger.channel.apigee_api_catalog'] + arguments: ['@file_system', '@http_client', '@entity_type.manager', '@string_translation', '@messenger', '@logger.channel.apigee_api_catalog'] diff --git a/src/SpecFetcher.php b/src/SpecFetcher.php index 64d0466..adde85e 100644 --- a/src/SpecFetcher.php +++ b/src/SpecFetcher.php @@ -22,12 +22,12 @@ use Drupal\apigee_api_catalog\Entity\ApiDocInterface; use Drupal\Component\Datetime\DateTimePlus; -use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Messenger\MessengerTrait; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\Core\Url; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\RequestException; @@ -80,15 +80,18 @@ class SpecFetcher implements SpecFetcherInterface { * The http_client service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * The entity type manager. + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation + * The string translation. * @param \Drupal\Core\Messenger\MessengerInterface $messenger * The messenger service. * @param \Psr\Log\LoggerInterface $logger * The logger service. */ - public function __construct(FileSystemInterface $file_system, ClientInterface $http_client, EntityTypeManagerInterface $entityTypeManager, MessengerInterface $messenger, LoggerInterface $logger) { + public function __construct(FileSystemInterface $file_system, ClientInterface $http_client, EntityTypeManagerInterface $entityTypeManager, TranslationInterface $translation, MessengerInterface $messenger, LoggerInterface $logger) { $this->fileSystem = $file_system; $this->httpClient = $http_client; $this->entityTypeManager = $entityTypeManager; + $this->stringTranslation = $translation; $this->messenger = $messenger; $this->logger = $logger; } @@ -203,7 +206,7 @@ public function fetchSpec(ApiDocInterface $apidoc): bool { private function log(string $level, string $message, array $params = []) { $this->logger->log($level, $message, $params); // Show the message. - $this->messenger()->addMessage(new FormattableMarkup($message, $params), static::LOG_LEVEL_MAP[$level] ?? MessengerInterface::TYPE_ERROR); + $this->messenger()->addMessage($this->t($message, $params), static::LOG_LEVEL_MAP[$level] ?? MessengerInterface::TYPE_ERROR); } /** From 45caf360bf724e770631750d9500f350109396b5 Mon Sep 17 00:00:00 2001 From: Jaesin Mulenex Date: Fri, 24 May 2019 17:42:58 -0700 Subject: [PATCH 48/48] [DRUP-610] Remove the deprecated `exceptions` options. The default value for the `http_errors` option is `TRUE`. --- .../Validation/Constraint/ApiDocFileLinkConstraintValidator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php index 7ad2882..8c9e081 100644 --- a/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ApiDocFileLinkConstraintValidator.php @@ -83,7 +83,6 @@ public function validate($items, Constraint $constraint) { try { $options = [ - 'exceptions' => TRUE, 'allow_redirects' => [ 'strict' => TRUE, ],