diff --git a/modules/entity_share_async/entity_share_async.permissions.yml b/modules/entity_share_async/entity_share_async.permissions.yml new file mode 100644 index 0000000..b5f33e3 --- /dev/null +++ b/modules/entity_share_async/entity_share_async.permissions.yml @@ -0,0 +1,3 @@ +entity_share_async_access_endpoint: + title: 'Access async endpoint' + description: 'Allows to register entities to be pulled later.' diff --git a/modules/entity_share_async/entity_share_async.routing.yml b/modules/entity_share_async/entity_share_async.routing.yml new file mode 100644 index 0000000..96f8cd1 --- /dev/null +++ b/modules/entity_share_async/entity_share_async.routing.yml @@ -0,0 +1,2 @@ +route_callbacks: + - '\Drupal\entity_share_async\Routing\Routes::entryPoint' diff --git a/modules/entity_share_async/src/Controller/EntryPoint.php b/modules/entity_share_async/src/Controller/EntryPoint.php new file mode 100644 index 0000000..124c5b4 --- /dev/null +++ b/modules/entity_share_async/src/Controller/EntryPoint.php @@ -0,0 +1,94 @@ +currentRequest = $container->get('request_stack')->getCurrentRequest(); + $instance->queueHelper = $container->get('entity_share_async.queue_helper'); + return $instance; + } + + /** + * Controller to register entities to be pulled later. + */ + public function index() { + $request_body = $this->currentRequest->getContent(); + $request_body = Json::decode($request_body); + + // Validate the body. + if (!is_array($request_body)) { + throw new AccessDeniedHttpException($this->t('The request body is not correct. Expected body is like {"remote_id":"example","channel_id":"example","uuid":"example"}.')); + } + + $data_keys = [ + 'remote_config_id', + 'remote_id', + 'channel_id', + 'uuid', + ]; + foreach ($data_keys as $data_key) { + if (!isset($request_body[$data_key]) || empty($request_body[$data_key]) || !is_string($request_body[$data_key])) { + throw new AccessDeniedHttpException($this->t('The request body is not correct. Expected body is like {"remote_config_id":"example","remote_id":"example","channel_id":"example","uuid":"example"}.')); + } + } + + /** @var \Drupal\entity_share_client\Entity\RemoteInterface[] $remotes */ + $remotes = $this->entityTypeManager() + ->getStorage('remote') + ->loadMultiple(); + $remote_found = FALSE; + foreach ($remotes as $remote) { + if ($remote->id() == $request_body['remote_id']) { + $remote_found = TRUE; + } + } + + if (!$remote_found) { + throw new AccessDeniedHttpException($this->t('There is no remote with the ID @remote_id.', [ + '@remote_id' => Xss::filter($request_body['remote_id']), + ])); + } + + $this->queueHelper->enqueue( + $request_body['remote_id'], + $request_body['channel_id'], + $request_body['remote_config_id'], + [$request_body['uuid']] + ); + + return new JsonResponse($this->t('Entity enqueued for synchronization.')); + } + +} diff --git a/modules/entity_share_async/src/Routing/Routes.php b/modules/entity_share_async/src/Routing/Routes.php new file mode 100644 index 0000000..25c3214 --- /dev/null +++ b/modules/entity_share_async/src/Routing/Routes.php @@ -0,0 +1,89 @@ +authCollector = $auth_collector; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + /* @var \Drupal\Core\Authentication\AuthenticationCollectorInterface $auth_collector */ + $auth_collector = $container->get('authentication_collector'); + + return new static($auth_collector); + } + + /** + * Provides the entry point route. + */ + public function entryPoint() { + $collection = new RouteCollection(); + + $route_collection = (new Route('/entity_share/async', [ + RouteObjectInterface::CONTROLLER_NAME => '\Drupal\entity_share_async\Controller\EntryPoint::index', + ])) + ->setRequirement('_permission', 'entity_share_async_access_endpoint') + ->setMethods(['POST']); + $route_collection->addOptions([ + '_auth' => $this->authProviderList(), + ]); + $collection->add('entity_share_async.notification_endpoint', $route_collection); + + return $collection; + } + + /** + * Build a list of authentication provider ids. + * + * @return string[] + * The list of IDs. + */ + protected function authProviderList() { + if (isset($this->providerIds)) { + return $this->providerIds; + } + $this->providerIds = array_keys($this->authCollector->getSortedProviders()); + + return $this->providerIds; + } + +} diff --git a/modules/entity_share_notifier/config/schema/entity_share_subscriber.schema.yml b/modules/entity_share_notifier/config/schema/entity_share_subscriber.schema.yml new file mode 100644 index 0000000..23f43fd --- /dev/null +++ b/modules/entity_share_notifier/config/schema/entity_share_subscriber.schema.yml @@ -0,0 +1,32 @@ +entity_share_notifier.entity_share_subscriber.*: + type: config_entity + label: 'Entity share subscriber config' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + subscriber_url: + type: string + label: 'URL' + basic_auth_username: + type: string + label: 'Basic auth username' + basic_auth_password: + type: string + label: 'Basic auth password' + remote_id: + type: string + label: 'Remote ID' + remote_config_id: + type: string + label: 'Remote Config import ID' + channel_ids: + type: sequence + label: 'Channel IDs' + nullable: true + sequence: + type: string + label: 'Channel ID' diff --git a/modules/entity_share_notifier/entity_share_notifier.info.yml b/modules/entity_share_notifier/entity_share_notifier.info.yml new file mode 100644 index 0000000..d73743c --- /dev/null +++ b/modules/entity_share_notifier/entity_share_notifier.info.yml @@ -0,0 +1,7 @@ +name: Entity share notifier +type: module +description: Allows an entity share server to notify remote websites. +core: 8.x +package: Web services +dependencies: + - entity_share:entity_share_server diff --git a/modules/entity_share_notifier/entity_share_notifier.links.action.yml b/modules/entity_share_notifier/entity_share_notifier.links.action.yml new file mode 100644 index 0000000..42136f8 --- /dev/null +++ b/modules/entity_share_notifier/entity_share_notifier.links.action.yml @@ -0,0 +1,5 @@ +entity.entity_share_subscriber.add_form: + route_name: entity.entity_share_subscriber.add_form + title: 'Add Entity share subscriber' + appears_on: + - entity.entity_share_subscriber.collection diff --git a/modules/entity_share_notifier/entity_share_notifier.links.menu.yml b/modules/entity_share_notifier/entity_share_notifier.links.menu.yml new file mode 100644 index 0000000..0ff7508 --- /dev/null +++ b/modules/entity_share_notifier/entity_share_notifier.links.menu.yml @@ -0,0 +1,6 @@ +# Entity share subscriber menu items definition. +entity.entity_share_subscriber.collection: + title: 'Subscribers' + route_name: entity.entity_share_subscriber.collection + description: 'List Entity share subscriber' + parent: entity_share.admin_config_page diff --git a/modules/entity_share_notifier/entity_share_notifier.module b/modules/entity_share_notifier/entity_share_notifier.module new file mode 100644 index 0000000..7afdb4f --- /dev/null +++ b/modules/entity_share_notifier/entity_share_notifier.module @@ -0,0 +1,29 @@ +getInstanceFromDefinition(EntityHookHandler::class) + ->process($entity); +} + +/** + * Implements hook_entity_update(). + */ +function entity_share_notifier_entity_update(EntityInterface $entity) { + \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityHookHandler::class) + ->process($entity); +} diff --git a/modules/entity_share_notifier/entity_share_notifier.permissions.yml b/modules/entity_share_notifier/entity_share_notifier.permissions.yml new file mode 100644 index 0000000..af33399 --- /dev/null +++ b/modules/entity_share_notifier/entity_share_notifier.permissions.yml @@ -0,0 +1,4 @@ +administer_entity_share_subscriber_entity: + title: 'Administer entity share subscriber entity' + description: 'Allows to administer the subscribers.' + restrict access: true diff --git a/modules/entity_share_notifier/entity_share_notifier.services.yml b/modules/entity_share_notifier/entity_share_notifier.services.yml new file mode 100644 index 0000000..751655a --- /dev/null +++ b/modules/entity_share_notifier/entity_share_notifier.services.yml @@ -0,0 +1,4 @@ +services: + logger.channel.entity_share_notifier: + parent: logger.channel_base + arguments: ['entity_share_notifier'] diff --git a/modules/entity_share_notifier/src/Entity/EntityShareSubscriber.php b/modules/entity_share_notifier/src/Entity/EntityShareSubscriber.php new file mode 100644 index 0000000..91a58aa --- /dev/null +++ b/modules/entity_share_notifier/src/Entity/EntityShareSubscriber.php @@ -0,0 +1,125 @@ +get('subscriber_url'); + if (!empty($subscriber_url) && preg_match('/(.*)\/$/', $subscriber_url, $matches)) { + $this->set('subscriber_url', $matches[1]); + } + } + +} diff --git a/modules/entity_share_notifier/src/Entity/EntityShareSubscriberInterface.php b/modules/entity_share_notifier/src/Entity/EntityShareSubscriberInterface.php new file mode 100644 index 0000000..15dcfea --- /dev/null +++ b/modules/entity_share_notifier/src/Entity/EntityShareSubscriberInterface.php @@ -0,0 +1,13 @@ +t('Entity share subscriber'); + $header['subscriber_url'] = $this->t('Subscriber URL'); + $header['remote_id'] = $this->t('Remote ID'); + $header['channel_ids'] = $this->t('Channel IDs'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['subscriber_url'] = $entity->get('subscriber_url'); + $row['remote_id'] = $entity->get('remote_id'); + $row['channel_ids'] = [ + 'data' => [ + '#theme' => 'item_list', + '#items' => $entity->get('channel_ids'), + ], + ]; + return $row + parent::buildRow($entity); + } + +} diff --git a/modules/entity_share_notifier/src/Form/EntityShareSubscriberDeleteForm.php b/modules/entity_share_notifier/src/Form/EntityShareSubscriberDeleteForm.php new file mode 100644 index 0000000..6dd57b9 --- /dev/null +++ b/modules/entity_share_notifier/src/Form/EntityShareSubscriberDeleteForm.php @@ -0,0 +1,53 @@ +t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.entity_share_subscriber.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + + $this->messenger()->addMessage( + $this->t('content @type: deleted @label.', [ + '@type' => $this->entity->bundle(), + '@label' => $this->entity->label(), + ]) + ); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/modules/entity_share_notifier/src/Form/EntityShareSubscriberForm.php b/modules/entity_share_notifier/src/Form/EntityShareSubscriberForm.php new file mode 100644 index 0000000..baa7499 --- /dev/null +++ b/modules/entity_share_notifier/src/Form/EntityShareSubscriberForm.php @@ -0,0 +1,153 @@ +entity; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $entity_share_subscriber->label(), + '#description' => $this->t('Label for the Entity share subscriber.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $entity_share_subscriber->id(), + '#machine_name' => [ + 'exists' => '\Drupal\entity_share_notifier\Entity\EntityShareSubscriber::load', + ], + '#disabled' => !$entity_share_subscriber->isNew(), + ]; + + $form['subscriber_url'] = [ + '#type' => 'url', + '#title' => $this->t('URL'), + '#maxlength' => 255, + '#description' => $this->t('The subscriber URL. Example: http://example.com'), + '#default_value' => $entity_share_subscriber->get('subscriber_url'), + '#required' => TRUE, + ]; + + $form['basic_auth'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Basic Auth'), + ]; + + $form['basic_auth']['basic_auth_username'] = [ + '#type' => 'textfield', + '#title' => $this->t('Username'), + '#default_value' => $entity_share_subscriber->get('basic_auth_username'), + '#required' => TRUE, + ]; + + $form['basic_auth']['basic_auth_password'] = [ + '#type' => 'password', + '#title' => $this->t('Password'), + '#required' => TRUE, + ]; + + $form['remote_config_id'] = [ + '#type' => 'textfield', + '#title' => $this->t('Remote Config ID'), + '#default_value' => $entity_share_subscriber->get('remote_config_id'), + '#description' => $this->t('The remote config ID used on the subscriber website to pull this server.'), + '#required' => TRUE, + ]; + + $form['remote_id'] = [ + '#type' => 'textfield', + '#title' => $this->t('Remote ID'), + '#default_value' => $entity_share_subscriber->get('remote_id'), + '#description' => $this->t('The remote ID used on the subscriber website to pull this server.'), + '#required' => TRUE, + ]; + + $channel_ids = $entity_share_subscriber->get('channel_ids'); + $form['channel_ids'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Channel IDs'), + '#description' => $this->t('The list of channel this subscriber will be notified on.'), + '#options' => $this->getChannelsOptions(), + '#default_value' => !is_null($channel_ids) ? $channel_ids : [], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // Validate URL. + if (!UrlHelper::isValid($form_state->getValue('subscriber_url'), TRUE)) { + $form_state->setError($form['subscriber_url'], $this->t('Invalid URL.')); + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /** @var \Drupal\entity_share_notifier\Entity\EntityShareSubscriberInterface $entity_share_subscriber */ + $entity_share_subscriber = $this->entity; + + $channel_ids = array_filter($form_state->getValue('channel_ids')); + $entity_share_subscriber->set('channel_ids', $channel_ids); + + $status = $entity_share_subscriber->save(); + + switch ($status) { + case SAVED_NEW: + $this->messenger()->addMessage($this->t('Created the %label Entity share subscriber.', [ + '%label' => $entity_share_subscriber->label(), + ])); + break; + + default: + $this->messenger()->addMessage($this->t('Saved the %label Entity share subscriber.', [ + '%label' => $entity_share_subscriber->label(), + ])); + } + $form_state->setRedirectUrl($entity_share_subscriber->toUrl('collection')); + } + + /** + * Get channels. + * + * @return array + * An array of options. + */ + protected function getChannelsOptions() { + $channel_options = []; + + /** @var \Drupal\entity_share_server\Entity\ChannelInterface[] $channels */ + $channels = $this->entityTypeManager + ->getStorage('channel') + ->loadMultiple(); + foreach ($channels as $channel) { + $channel_options[$channel->id()] = $channel->label(); + } + + return $channel_options; + } + +} diff --git a/modules/entity_share_notifier/src/HookHandler/EntityHookHandler.php b/modules/entity_share_notifier/src/HookHandler/EntityHookHandler.php new file mode 100644 index 0000000..93a69c0 --- /dev/null +++ b/modules/entity_share_notifier/src/HookHandler/EntityHookHandler.php @@ -0,0 +1,228 @@ +entityTypeManager = $entity_type_manager; + $this->languageManager = $language_manager; + $this->channelManipulator = $channel_manipulator; + $this->clientFactory = $client_factory; + $this->logger = $logger; + $this->bundleInfos = $entity_type_bundle_info->getAllBundleInfo(); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('language_manager'), + $container->get('entity_share_server.channel_manipulator'), + $container->get('http_client_factory'), + $container->get('logger.channel.entity_share_notifier'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * Notify, if available, clients. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + */ + public function process(EntityInterface $entity) { + $channels_to_notify = []; + /** @var \Drupal\entity_share_server\Entity\ChannelInterface[] $channels */ + $channels = $this->entityTypeManager + ->getStorage('channel') + ->loadMultiple(); + $languages = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL); + + $entity_type_id = $entity->getEntityTypeId(); + $entity_bundle = $entity->bundle(); + $entity_langcode = $entity->language()->getId(); + $entity_uuid = $entity->uuid(); + + // Loop on channels to find the ones (for translations) in which the + // entity is present. + foreach ($channels as $channel) { + // After this check, we know that we are manipulating a content entity. + $channel_entity_type = $channel->get('channel_entity_type'); + if ($channel_entity_type != $entity_type_id) { + continue; + } + + // Check bundle. + $channel_bundle = $channel->get('channel_bundle'); + if ($channel_bundle != $entity_bundle) { + continue; + } + + // Check langcode, if applicable. + if (isset($this->bundleInfos[$channel_entity_type][$channel_bundle]['translatable']) && $this->bundleInfos[$channel_entity_type][$channel_bundle]['translatable']) { + $channel_langcode = $channel->get('channel_langcode'); + if ($channel_langcode != $entity_langcode) { + continue; + } + } + + // TODO. Will require to add a new field on channel to store the user to + // test with. + // Test that the entity appears on the channel. + // $route_name = sprintf('jsonapi.%s--%s.collection', $channel_entity_type, $channel_bundle); + // $query = $this->channelManipulator->getQuery($channel); + // $query['filter']['uuid-filter'] = [ + // 'condition' => [ + // 'path' => 'id', + // 'operator' => 'IN', + // 'value' => [$entity_uuid], + // ], + // ]; + // $query = UrlHelper::buildQuery($query); + // + // $url = Url::fromRoute($route_name) + // ->setOption('language', $languages[$channel_langcode]) + // ->setOption('absolute', TRUE) + // ->setOption('query', $query);. + $channels_to_notify[] = $channel->id(); + } + + /** @var \Drupal\entity_share_notifier\Entity\EntityShareSubscriberInterface[] $subscribers */ + $subscribers = $this->entityTypeManager + ->getStorage('entity_share_subscriber') + ->loadMultiple(); + + // Loop on subscribers to notify its. + foreach ($subscribers as $subscriber) { + $channels_to_notify_for_subscriber = array_intersect($channels_to_notify, $subscriber->get('channel_ids')); + + if (empty($channels_to_notify_for_subscriber)) { + continue; + } + + $remote_id = $subscriber->get('remote_id'); + $remote_config_id = $subscriber->get('remote_config_id'); + $http_client = $this->clientFactory->fromOptions([ + 'base_uri' => $subscriber->get('subscriber_url') . '/', + 'auth' => [ + $subscriber->get('basic_auth_username'), + $subscriber->get('basic_auth_password'), + ], + 'headers' => [ + 'Content-type' => 'application/json', + ], + ]); + + foreach ($channels_to_notify_for_subscriber as $channel_id) { + try { + $http_client->request('POST', 'entity_share/async', [ + 'json' => [ + 'remote_config_id' => $remote_config_id, + 'remote_id' => $remote_id, + 'channel_id' => $channel_id, + 'uuid' => $entity_uuid, + ], + ]); + } + catch (ClientException $e) { + $this->logger->error('Error when requesting client: @exception_message', ['@exception_message' => $e->getMessage()]); + } + catch (ServerException $e) { + $this->logger->error('Error when requesting client: @exception_message', ['@exception_message' => $e->getMessage()]); + } + catch (\Exception $e) { + $this->logger->error('Error when requesting client: @exception_message', ['@exception_message' => $e->getMessage()]); + } + } + } + } + +}