diff --git a/dcx_integration.info.yml b/dcx_integration.info.yml index 38165b2..e639e05 100644 --- a/dcx_integration.info.yml +++ b/dcx_integration.info.yml @@ -4,4 +4,5 @@ description: 'Integration of DC-X digital asset management.' version: '0' core: '8.x' +package: dcx diff --git a/dcx_integration.module b/dcx_integration.module deleted file mode 100644 index 5f4bf81..0000000 --- a/dcx_integration.module +++ /dev/null @@ -1,56 +0,0 @@ -getEntityTypeId() && 'image' === $entity->bundle()) { - return; - } - - $usage = []; - // Iterate over the field definition of the given entitiy - foreach ($entity->getFieldDefinitions() as $definition) { - - // Find entity reference fields - if ('entity_reference' === $definition->getType()) { - $settings = $definition->getSettings(); - $target_type = $settings['target_type']; - - // Only care about field referencing media - if ('media' == $target_type) { - $target_bundles = $settings['handler_settings']['target_bundles']; - if (in_array('image', $target_bundles)) { - $field = $definition->getName(); - if (! empty($entity->$field->target_id)) { - $usage[] = $entity->$field->referencedEntities()[0]->field_source->value; - } - } - } - } - } - - $url = $entity->toUrl()->setAbsolute()->toString(); - \Drupal::service('dcx_integration.client')->trackUsage($usage, $url); -} - diff --git a/modules/dcx_article_archive/dcx_article_archive.info.yml b/modules/dcx_article_archive/dcx_article_archive.info.yml index a46fd92..fa5f706 100644 --- a/modules/dcx_article_archive/dcx_article_archive.info.yml +++ b/modules/dcx_article_archive/dcx_article_archive.info.yml @@ -2,6 +2,7 @@ name: DC-X Article Archive type: module description: Archive Articles to DC-X core: 8.x -package: Custom +package: dcx + dependencies: - dcx_integration diff --git a/modules/dcx_integration_debug/dcx_integration_debug.info.yml b/modules/dcx_integration_debug/dcx_integration_debug.info.yml index 4d69686..6ce15b4 100644 --- a/modules/dcx_integration_debug/dcx_integration_debug.info.yml +++ b/modules/dcx_integration_debug/dcx_integration_debug.info.yml @@ -4,6 +4,7 @@ description: 'Debugging helper for DC-X integration. Mocking DC-X the service.' version: '0' core: '8.x' +package: dcx dependencies: - dcx_integration diff --git a/modules/dcx_integration_debug/src/MockClient.php b/modules/dcx_integration_debug/src/MockClient.php index 1c0182c..bf869ab 100644 --- a/modules/dcx_integration_debug/src/MockClient.php +++ b/modules/dcx_integration_debug/src/MockClient.php @@ -68,8 +68,8 @@ protected function buildStoryAsset($url) { return new Article($data); } - public function trackUsage($id, $url) { - print_r("Media $id used on URL {" . $url . "}"); + public function trackUsage($dcx_ids, $url, $published) { + dpm("Media " . print_r($dcx_ids, 1) . " used on URL {" . $url . "}"); } /** @@ -79,7 +79,7 @@ public function archiveArticle($url, $title, $text, $dcx_id) { if (!$dcx_id) { $dcx_id = "dcxapi:document/doc__mocked__" . rand(10000000000, 99999999999); } - + return $dcx_id; } } diff --git a/modules/dcx_migration/config/install/field.field.media.image.field_dcx_id.yml b/modules/dcx_migration/config/install/field.field.media.image.field_dcx_id.yml index e86cb56..58c9549 100644 --- a/modules/dcx_migration/config/install/field.field.media.image.field_dcx_id.yml +++ b/modules/dcx_migration/config/install/field.field.media.image.field_dcx_id.yml @@ -1,4 +1,3 @@ -uuid: 51fbed76-12e5-4949-bd09-9d4ebd12234e langcode: en status: true dependencies: diff --git a/modules/dcx_migration/config/install/field.field.media.image.field_published.yml b/modules/dcx_migration/config/install/field.field.media.image.field_published.yml index 947e982..f8492c6 100644 --- a/modules/dcx_migration/config/install/field.field.media.image.field_published.yml +++ b/modules/dcx_migration/config/install/field.field.media.image.field_published.yml @@ -1,4 +1,3 @@ -uuid: efe612b1-fe02-45af-8000-e9b7a348e84c langcode: en status: true dependencies: diff --git a/modules/dcx_migration/config/install/field.storage.media.field_dcx_id.yml b/modules/dcx_migration/config/install/field.storage.media.field_dcx_id.yml index fe6cb02..be5d297 100644 --- a/modules/dcx_migration/config/install/field.storage.media.field_dcx_id.yml +++ b/modules/dcx_migration/config/install/field.storage.media.field_dcx_id.yml @@ -1,4 +1,3 @@ -uuid: 89835a1f-04a9-42d0-a003-3d4439a5c09f langcode: en status: true dependencies: diff --git a/modules/dcx_migration/config/install/field.storage.media.field_published.yml b/modules/dcx_migration/config/install/field.storage.media.field_published.yml index 5b46b94..96f5715 100644 --- a/modules/dcx_migration/config/install/field.storage.media.field_published.yml +++ b/modules/dcx_migration/config/install/field.storage.media.field_published.yml @@ -1,4 +1,3 @@ -uuid: ad36bb56-9d9e-414d-bc31-49c1a4c733e2 langcode: en status: true dependencies: diff --git a/modules/dcx_migration/dcx_migration.info.yml b/modules/dcx_migration/dcx_migration.info.yml index 6857d99..91f65bb 100644 --- a/modules/dcx_migration/dcx_migration.info.yml +++ b/modules/dcx_migration/dcx_migration.info.yml @@ -4,8 +4,10 @@ description: 'Migration of data from DC-X digital asset management.' version: '0' core: '8.x' +package: dcx dependencies: - dcx_integration - migrate - migrate_plus #MigratePlusEvents::PREPARE_ROW + - media_entity diff --git a/modules/dcx_track_media_usage/dcx_track_media_usage.info.yml b/modules/dcx_track_media_usage/dcx_track_media_usage.info.yml new file mode 100644 index 0000000..0eb1d02 --- /dev/null +++ b/modules/dcx_track_media_usage/dcx_track_media_usage.info.yml @@ -0,0 +1,7 @@ +name: DC-X Track Media Usage +type: module +description: Provides usage tracking of DC-X media an nodes +core: 8.x +package: dcx +dependencies: + - dcx_integration diff --git a/modules/dcx_track_media_usage/dcx_track_media_usage.module b/modules/dcx_track_media_usage/dcx_track_media_usage.module new file mode 100644 index 0000000..f48c3c2 --- /dev/null +++ b/modules/dcx_track_media_usage/dcx_track_media_usage.module @@ -0,0 +1,137 @@ +getEntityTypeId() && 'image' === $entity->bundle()) { + return; + } + + $usage = _dcx_track_media_collect_usage_on_entity_reference_fields($entity); + $usage += _dcx_track_media_collect_usage_on_paragraphs($entity); + + $url = $entity->toUrl()->toString(); + $status = $entity->status->value; + try { + Drupal::service('dcx_integration.client')->trackUsage($usage, $url, $status); + } catch (\Exception $e) { + drupal_set_message($e->getMessage(), 'error'); + } +} + +/** + * Collect media:image entities referenced by entity reference fields on the + * given entity. + * + * @param type $entity + * @return array $usage list of DC-X IDs keyed by DC-X IDs. + */ +function _dcx_track_media_collect_usage_on_entity_reference_fields($entity) { + + $usage = []; + // Iterate over the field definition of the given entitiy + foreach ($entity->getFieldDefinitions() as $definition) { + // Fields have FieldConfig. Let's assume our media is referenced within a + // field + if (! $definition instanceof FieldConfig) { + continue; + } + // Only care about entity reference fields + if ('entity_reference' !== $definition->getType()) { continue; } + $settings = $definition->getSettings(); + + // We can't be sure that a target type is defined. Deal with it. + $target_type = isset($settings['target_type'])?$settings['target_type']:NULL; + + // Only care about field referencing media + if ('media' !== $target_type) { continue; } + + $target_bundles = $settings['handler_settings']['target_bundles']; + + // Only care about refs allowing images + if (! in_array('image', $target_bundles)) { continue; } + + $field = $definition->getName(); + + // Don't care about empty reference fields; + if (empty($entity->$field->target_id)) { continue; } + + $referenced_entities = $entity->$field->referencedEntities(); + foreach ($referenced_entities as $referenced_entity) { + // Only care about image media + if ('image' !== $referenced_entity->bundle()) { continue; } + + $dcx_id = $referenced_entity->field_dcx_id->value; + + if (empty($dcx_id)) { + // @TODO This must not happen by contract. How do we deal if it happens? + throw \Exception(t('Media image %id has no DC-X ID', ['%id' => $referenced_entity->id()])); + } + $usage[$dcx_id] = $dcx_id; + } + + } + return $usage; +} + +/** + * Collect media:image entities referenced by paragraphs fields on the + * given entity. + * + * @param type $entity + * @return array $usage list of DC-X IDs keyed by DC-X IDs. + */ +function _dcx_track_media_collect_usage_on_paragraphs($entity) { + $usage = []; + + dpm($entity); + + foreach ($entity->getFieldDefinitions() as $definition) { + + // Paragraphes are stored in a proper field with FieldConfig + if (! $definition instanceof FieldConfig) { + continue; + } + + // Only care about entity_reference_revisions, which is the field type of + // paragraphs + if ('entity_reference_revisions' !== $definition->getType()) { continue; } + + $settings = $definition->getSettings(); + + $target_type = isset($settings['target_type'])?$settings['target_type']:NULL; + + // Only care about field referencing paragraphs + if ('paragraph' !== $target_type) { continue; } + + $field = $definition->getName(); + + dpm($field); + } + + return $usage; +} diff --git a/src/ClientInterface.php b/src/ClientInterface.php index c81f0f0..5743ad2 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -16,7 +16,18 @@ interface ClientInterface { public function getObject($id); - public function trackUsage($id, $url); + /** + * Track usage of DC-X Documents on the given URL. + * + * The given URL should be expanded to the appropriate public absolute URL. + * + * @param array $dcx_ids List of DC-X document IDs. + * @param string $url relative canonical URL where the documents are used. + * @param bool $published status of the given URL + * + * @throws \Exception if something is going wrong. + */ + public function trackUsage($dcx_ids, $url, $published); /** * Archive an article. @@ -29,6 +40,8 @@ public function trackUsage($id, $url); * * @return int * The DC-X document ID of the article + * + * @throws \Exception if something is going wrong. */ public function archiveArticle($url, $title, $text, $dcx_id); } diff --git a/src/Controller/DcxDebugController.php b/src/Controller/DcxDebugController.php index 1e780ba..e7a7500 100644 --- a/src/Controller/DcxDebugController.php +++ b/src/Controller/DcxDebugController.php @@ -9,7 +9,7 @@ use Drupal\Core\Controller\ControllerBase; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\dcx_integration\JsonClient; +use Drupal\dcx_integration\ClientInterface; /** * Class DcxDebugController. @@ -28,7 +28,7 @@ class DcxDebugController extends ControllerBase { /** * {@inheritdoc} */ - public function __construct(JsonClient $dcx_integration_client) { + public function __construct(ClientInterface $dcx_integration_client) { $this->dcx_integration_client = $dcx_integration_client; } @@ -84,7 +84,7 @@ public function archive() { catch (\Exception $e) { dpm($e); } - + return [ '#type' => 'markup', '#markup' => __METHOD__ . " " . $dcx_id, diff --git a/src/Form/JsonClientSettings.php b/src/Form/JsonClientSettings.php index 96742ac..f8c50af 100644 --- a/src/Form/JsonClientSettings.php +++ b/src/Form/JsonClientSettings.php @@ -43,6 +43,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => $this->t('URL'), '#maxlength' => 64, '#size' => 64, + '#required' => TRUE, '#default_value' => $config->get('url'), ]; $form['username'] = [ @@ -53,12 +54,30 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#default_value' => $config->get('username'), ]; $form['password'] = [ - '#type' => 'textfield', + '#type' => 'password', '#title' => $this->t('Password'), '#maxlength' => 64, '#size' => 64, '#default_value' => $config->get('password'), ]; + $form['publication'] = [ + '#type' => 'textfield', + '#title' => $this->t('Publication'), + '#maxlength' => 64, + '#size' => 64, + '#required' => TRUE, + '#default_value' => $config->get('publication'), + '#description' => $this->t('Machine name of the publication (this website) in DC-X, e.g "publication-freundin".') + ]; + $form['frontendurl'] = [ + '#type' => 'textfield', + '#title' => $this->t('Frontend-URL'), + '#maxlength' => 64, + '#size' => 64, + '#default_value' => $config->get('frontendurl'), + '#description' => $this->t('The public facing frontpage URL of this website, e.g "http://www.bunte.de". If there are no alternative backend urls (like e.g. http://redaktion.bunte.de) you may leave this blank.'), + ]; + return parent::buildForm($form, $form_state); } @@ -75,10 +94,17 @@ public function validateForm(array &$form, FormStateInterface $form_state) { public function submitForm(array &$form, FormStateInterface $form_state) { parent::submitForm($form, $form_state); + $password = $form_state->getValue('password'); + if (empty($password)) { + $password = $config = $this->config('dcx_integration.jsonclientsettings')->get('password'); + } + $this->config('dcx_integration.jsonclientsettings') ->set('url', $form_state->getValue('url')) ->set('username', $form_state->getValue('username')) - ->set('password', $form_state->getValue('password')) + ->set('password', $password) + ->set('frontendurl', trim($form_state->getValue('frontendurl'))) + ->set('publication', trim($form_state->getValue('publication'))) ->save(); } diff --git a/src/JsonClient.php b/src/JsonClient.php index ae28c86..b897f73 100644 --- a/src/JsonClient.php +++ b/src/JsonClient.php @@ -31,17 +31,23 @@ class JsonClient implements ClientInterface { */ protected $api_client; + /** + * JSON client settings. + * + * @var \Drupal\Core\Config\ImmutableConfig + */ + protected $config; /** * Constructor. */ public function __construct(ConfigFactory $config_factory, TranslationInterface $string_translation) { $this->stringTranslation = $string_translation; - $config = $config_factory->get('dcx_integration.jsonclientsettings'); + $this->config = $config_factory->get('dcx_integration.jsonclientsettings'); - $url = $config->get('url'); - $username = $config->get('username'); - $password = $config->get('password'); + $url = $this->config->get('url'); + $username = $this->config->get('username'); + $password = $this->config->get('password'); $this->api_client = new \DCX_Api_Client($url, $username, $password); @@ -53,19 +59,24 @@ public function __construct(ConfigFactory $config_factory, TranslationInterface * It's not part of the interface, it should be protected. * It really shouldn't be called directly. */ - public function getJson($id) { + public function getJson($id, $params = NULL) { $json = NULL; - $params = [ - // All fields - 's[fields]' => '*', - // All properties - 's[properties]' => '*', - // All files - 's[files]'=> '*', - // attribute _file_absolute_url of all referenced files in the document - 's[_referenced][dcx:file][s][properties]' => '_file_url_absolute', - ]; + if ($params == NULL) { + $params = [ + 's[pubinfos]' => '*', + // All fields + 's[fields]' => '*', + // All properties + 's[properties]' => '*', + // All files + 's[files]'=> '*', + // attribute _file_absolute_url of all referenced files in the document + 's[_referenced][dcx:file][s][properties]' => '_file_url_absolute', + + 's[_referenced][dcx:pubinfo][s]' => '*', + ]; + } $url = preg_replace('/^dcxapi:/', '', $id); $http_status = $this->api_client->getObject($url, $params, $json); @@ -188,33 +199,113 @@ protected function extractUrl($keys, $json) { return $file_url; } - public function trackUsage($id, $url) { - $data = [ - "_type" => "dcx:pubinfo", - "properties" => [ - "doc_id" => [ - "_id" => $id, - "_type" => "dcx:document" - ], - "uri" => $url, - "status_id" => [ - "_id" => "dcxapi:tm_topic/pubstatus-published", - "_type" => "dcx:tm_topic", - "value" => "Published" - ], - "publication_id" => [ - "_id" => "dcxapi:tm_topic/publication-default", - "_type" => "dcx:tm_topic", - "value" => "Bunte" - ], - "type_id" => [ - "_id" => "dcxapi:tm_topic/pubtype-article", - "_type" => "dcx:tm_topic", - "value" => "Article" + /** + * {@inheritdoc} + */ + public function trackUsage($dcx_ids, $url, $published) { + $dcx_status = $published?'pubstatus-published':'pubstatus-planned'; + + $dateTime = new \DateTime(); + $date = $dateTime->format(\DateTime::W3C); + // 1. Find all documents with a usage of on url. + // non yet + + $dcx_publication = $this->config->get('publication'); + + // Expand given relative URL to absolute URL. + $frontendurl = $this->config->get('frontendurl'); + if (empty($frontendurl)) { + global $base_url; + $frontendurl = $base_url; + } + $url = $frontendurl . $url; + + foreach($dcx_ids as $id) { + $data = [ + "_type" => "dcx:pubinfo", + "properties" => [ + "doc_id" => [ + "_id" => $id, + "_type" => "dcx:document" + ], + "uri" => $url, + "date" => $date, + "status_id" => [ + "_id" => "dcxapi:tm_topic/$dcx_status", + "_type" => "dcx:tm_topic", + "value" => "Published" + ], + "publication_id" => [ + "_id" => "dcxapi:tm_topic/$dcx_publication", + "_type" => "dcx:tm_topic", + "value" => "Bunte" + ], + "type_id" => [ + "_id" => "dcxapi:tm_topic/pubtype-article", + "_type" => "dcx:tm_topic", + "value" => "Article" + ] ] - ] - ]; - dpm($data); + ]; + + $pubinfo = $this->getRelevantPubinfo($id, $url); + if (count($pubinfo) > 1) { + throw new \Exception($this->t('For document !id exists more that one ' + . 'pubinfo refering to %url. This should not be the case and cannot ' + . 'be resolved manually. Please fix this in DC-X.', + ['%id' => $id, '%url' => $url])); + } + if (0 == count($pubinfo)) { + $http_status = $this->api_client->createObject('pubinfo', [], $data, $response_body); + if (201 !== $http_status) { + $message = $this->t('Error creating object %url. Status code was %code.', ['%url' => pubinfo, '%code' => $http_status]); + throw new \Exception($message); + } + } + else { // 1 == count($pubinfo) + $pubinfo = current($pubinfo); + $dcx_api_url = preg_replace('/dcxapi:/', '', $pubinfo['_id']); + + $modcount = $pubinfo['properties']['_modcount']; + $data['properties']['_modcount'] = $modcount; + $data['_id'] = $pubinfo['_id']; + + $http_status = $this->api_client->setObject($dcx_api_url, [], $data, $response_body); + if (200 !== $http_status) { + $message = $this->t('Error setting object %url. Status code was %code.', ['%url' => $dcx_api_url, '%code' => $http_status]); + throw new \Exception($message); + } + } + } + + } + + /** + * Retrieve pubinfo of the given DC-X id, which is relevant + * for the given article url. + * + * As no one can prevent users from adding a pubinfo manually for our URL + * this will always return a list of relevant pubinfo entries, even if there's + * suppose to be only one. + * + * @param string $dcx_id DC-X document ID + * @param string $url absolute canonical URL of the article + * + * @return array list of relevant pubinfo as it comes from DC-X + */ + protected function getRelevantPubinfo($dcx_id, $url) { + $json = $this->getJson($dcx_id, ['s[pubinfos]' => '*', 's[_referenced][dcx:pubinfo][s]' => '*'] ); + + $relevant_entries = []; + foreach($json['_referenced']['dcx:pubinfo'] as $pubinfo_id => $pubinfo) { + // We're not interested in pubinfo without uri. + if (! isset($pubinfo['properties']['uri'])) { continue; } + + // We're not interested in pubinfo on any other than our URI + if ($url !== $pubinfo['properties']['uri'] ) { continue; } + $relevant_entries[$pubinfo_id] = $pubinfo; + } + return $relevant_entries; } public function archiveArticle($url, $title, $text, $dcx_id) { @@ -253,11 +344,11 @@ public function archiveArticle($url, $title, $text, $dcx_id) { $data['properties']['_modcount'] = $modcount; $data['_id'] = '/dcx/api/' . $dcx_id; $dcx_api_url = $dcx_id; - $this->api_client->setObject($dcx_api_url, $params, $data, $response_body); + $this->api_client->setObject($dcx_api_url, [], $data, $response_body); } else { $dcx_api_url = 'document'; - $this->api_client->createObject($dcx_api_url, $params, $data, $response_body); + $this->api_client->createObject($dcx_api_url, [], $data, $response_body); } $error = FALSE;