activitypub-1.0.x-dev/src/Services/Type/TypePluginBase.php
src/Services/Type/TypePluginBase.php
<?php namespace Drupal\activitypub\Services\Type; use Drupal\activitypub\Entity\ActivityPubActivity; use Drupal\activitypub\Entity\ActivityPubActivityInterface; use Drupal\activitypub\Entity\ActivityPubTypeInterface; use Drupal\activitypub\Event\ActivityAudienceEvent; use Drupal\activitypub\Services\ActivityPubProcessClientInterface; use Drupal\activitypub\Services\ActivityPubUtilityInterface; use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Url; use Drupal\file\FileInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * A base class for ActivityPub types that implements most of the boilerplate. */ abstract class TypePluginBase extends PluginBase implements TypePluginInterface, ContainerFactoryPluginInterface { /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * The renderer. * * @var \Drupal\Core\Render\RendererInterface */ protected $renderer; /** * The date formatter. * * @var \Drupal\Core\DateTime\DateFormatInterface */ protected $dateFormatter; /** * The ActivityPub process client. * * @var \Drupal\activitypub\Services\ActivityPubProcessClient */ protected $activityPubProcessClient; /** * The ActivityPub utility service. * * @var \Drupal\activitypub\Services\ActivityPubUtilityInterface */ protected $activityPubUtility; /** * The entity field manager. * * @var \Drupal\Core\Entity\EntityFieldManagerInterface */ protected $entityFieldManager; /** * The config factory. * * @var \Drupal\Core\Config\ConfigFactoryInterface */ protected $configFactory; /** * A logger instance. * * @var \Psr\Log\LoggerInterface */ protected $logger; /** * The file url generator. * * @var \Drupal\Core\File\FileUrlGeneratorInterface */ protected $fileUrlGenerator; /** * The event dispatcher. * * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface */ protected $eventDispatcher; /** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, DateFormatterInterface $date_formatter, ActivityPubProcessClientInterface $activitypub_process_client, ActivityPubUtilityInterface $activitypub_utility, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory, LoggerInterface $logger, FileUrlGeneratorInterface $file_url_generator, EventDispatcherInterface $event_dispatcher) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->setConfiguration($configuration); $this->entityTypeManager = $entity_type_manager; $this->renderer = $renderer; $this->dateFormatter = $date_formatter; $this->activityPubProcessClient = $activitypub_process_client; $this->activityPubUtility = $activitypub_utility; $this->entityFieldManager = $entity_field_manager; $this->configFactory = $config_factory; $this->logger = $logger; $this->fileUrlGenerator = $file_url_generator; $this->eventDispatcher = $event_dispatcher; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $configuration, $plugin_id, $plugin_definition, $container->get('entity_type.manager'), $container->get('renderer'), $container->get('date.formatter'), $container->get('activitypub.process.client'), $container->get('activitypub.utility'), $container->get('entity_field.manager'), $container->get('config.factory'), $container->get('logger.channel.activitypub'), $container->get('file_url_generator'), $container->get('event_dispatcher') ); } /** * {@inheritdoc} */ public function isExposed() { return TRUE; } /** * {@inheritdoc} */ public function build(ActivityPubActivityInterface $activity, EntityInterface $entity = NULL) { return []; } /** * {@inheritdoc} */ public function getActivities() { return []; } /** * {@inheritdoc} */ public function getObjects() { return []; } /** * {@inheritdoc} */ public function getProperties($object) { return []; } /** * {@inheritdoc} */ public function onActivityInboxPreSave(ActivityPubActivityInterface $activity, &$doSave) {} /** * {@inheritdoc} */ public function onActivityOutboxPreSave(ActivityPubActivityInterface $activity, EntityInterface $entity, ActivityPubTypeInterface $activityPubType, &$doSave) {} /** * {@inheritdoc} */ public function onActivityPostSave(ActivityPubActivityInterface $activity, $update = TRUE) {} /** * {@inheritdoc} */ public function doInboxProcess(ActivityPubActivityInterface $activity, EntityInterface $entity = NULL) {} /** * {@inheritdoc} */ public function onEntityDelete(ActivityPubActivityInterface $activity, EntityInterface $entity) {} /** * {@inheritdoc} */ public function onActivityDelete(ActivityPubActivityInterface $activity) {} /** * Get value for a property. * * @param $property * @param $value * @param $field_type * * @return array|mixed|null * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function getValue($property, $value, $field_type = NULL) { $return = NULL; switch ($property) { case 'published': $date_timestamp = $value[0]['value']; if (!is_numeric($date_timestamp)) { $date_timestamp = strtotime($value[0]['value']); } $return = $this->dateFormatter->format($date_timestamp, 'custom', 'Y-m-d\TH:i:s\Z'); break; case 'name': $return = trim($value[0]['value']); break; case 'content': case 'summary': $return = trim(check_markup($value[0]['value'], $this->configFactory->get('activitypub.settings')->get('filter_format'))); break; case 'object': case 'inReplyTo': // This can either be a link field or just a text field. if (!empty($value[0]['uri'])) { $return = $value[0]['uri']; } if (!empty($value[0]['value'])) { $return = $value[0]['value']; } break; case 'attachment': $return = []; foreach ($value as $v) { // Check if field_type is coming from media entity reference field // type. if ($field_type == 'entity_reference') { /** @var \Drupal\media\MediaInterface $media */ $media = $this->entityTypeManager->getStorage('media')->load($v['target_id']); $fid = $media->getSource()->getSourceFieldValue($media); /** @var \Drupal\file\FileInterface $file */ $file = $this->entityTypeManager->getStorage('file')->load($fid); } else { /** @var \Drupal\file\FileInterface $file */ $file = $this->entityTypeManager->getStorage('file')->load($v['target_id']); } $type = $this->getMediaType($file); $return[] = (object) [ 'type' => $type, 'mediaType' => $file->getMimeType(), 'url' => $this->getFileUrl($file, $type), ]; } break; } return $return; } /** * Get the media type. * * @param \Drupal\file\FileInterface $file * * @return string */ protected function getMediaType(FileInterface $file) { $type = 'Document'; switch ($file->getMimeType()) { case 'image/jpg': case 'image/jpeg': case 'image/webp': case 'image/png': case 'image/gif': $type = 'Image'; break; case 'video/mpeg': case 'video/mp4': case 'video/quicktime': case 'video/ogg': $type = 'Video'; break; case 'audio/ogg': case 'audio/mp3': $type = 'Audio'; break; } return $type; } /** * Gets the file URL. * * @param \Drupal\file\FileInterface $file * @param string $type * * @return string * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function getFileUrl(FileInterface $file, string $type) { $config = $this->configFactory->get('activitypub.settings'); $attachment_content_style = $config->get('attachment_content_style'); if (!empty($attachment_content_style) && $type == 'Image') { /** @var \Drupal\image\ImageStyleInterface $image_style */ $storage = $this->entityTypeManager->getStorage('image_style'); $image_style = $storage->load($attachment_content_style); return $image_style->buildUrl($file->getFileUri()); } else { return $this->fileUrlGenerator->generateAbsoluteString($file->getFileUri()); } } /** * Render a URL from an entity. * * @param \Drupal\Core\Entity\EntityInterface $entity * * @return string */ protected function renderEntityUrl(EntityInterface $entity) { return $this->renderer->executeInRenderContext(new RenderContext(), function () use ($entity) { return $entity->toUrl('canonical', ['absolute' => TRUE])->toString(); }); } /** * Render a Url. * * @param \Drupal\Core\Url $url * * @return string */ protected function renderUrl(Url $url) { return $this->renderer->executeInRenderContext(new RenderContext(), function () use ($url) { return $url->toString(); }); } /** * {@inheritdoc} */ public function defaultConfiguration() { return [ 'activity' => '', ]; } /** * {@inheritdoc} */ public function setConfiguration(array $configuration) { $this->configuration = NestedArray::mergeDeep( $this->defaultConfiguration(), $configuration ); } /** * {@inheritdoc} */ public function getConfiguration() { return $this->configuration; } /** * {@inheritdoc} */ public function calculateDependencies() { return []; } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { $entity_type_id = $this->getConfiguration()['target_entity_type_id']; $entity_types = ['node' => $this->t('Content')]; if (\Drupal::moduleHandler()->moduleExists('comment')) { $entity_types['comment'] = $this->t('Comment'); } $form['target_entity_type_id'] = [ '#type' => 'select', '#title' => $this->t('Entity type'), '#options' => $entity_types, '#default_value' => $entity_type_id, '#ajax' => [ 'trigger_as' => ['name' => 'type_configure'], 'callback' => '::buildAjaxTypeForm', 'wrapper' => 'type-config-form', 'method' => 'replace', 'effect' => 'fade', ], ]; if ($entity_type_id == 'comment') { $bundle_options = activitypub_comment_get_names(); } else { $bundle_options = node_type_get_names(); } $form['target_bundle'] = [ '#type' => 'select', '#title' => $this->t('Type'), '#options' => ['' => $this->t('- Select -')] + $bundle_options, '#default_value' => $this->getConfiguration()['target_bundle'], '#ajax' => [ 'trigger_as' => ['name' => 'type_configure'], 'callback' => '::buildAjaxTypeForm', 'wrapper' => 'type-config-form', 'method' => 'replace', 'effect' => 'fade', ], ]; $activitypub_activity_options = ['' => $this->t('- Select -')]; $activitypub_object_options = ['' => $this->t('- Select -')]; foreach ($this->getActivities() as $a) { $activitypub_activity_options[$a] = $a; } foreach ($this->getObjects() as $o) { $option_id = $o; $activitypub_object_options[$option_id] = $o; } $form['activity'] = [ '#type' => 'select', '#title' => $this->t('ActivityPub activity'), '#default_value' => $this->getConfiguration()['activity'], '#options' => $activitypub_activity_options, ]; $form['object'] = [ '#type' => 'select', '#title' => $this->t('ActivityPub object'), '#default_value' =>$this->getConfiguration()['object'], '#options' => $activitypub_object_options, ]; return $form; } /** * Gets an entity from an url. * * @param $url string * The string url to obtain an entity. * * @return \Drupal\Core\Entity\EntityInterface $entity|NULL * Returns an entity or NULL. */ public static function getEntityFromUrl(string $url) { $host = \Drupal::request()->getSchemeAndHttpHost(); try { if (UrlHelper::externalIsLocal($url, $host)) { $path = str_replace($host . base_path(), '', $url); /** @var \Drupal\Core\Path\PathValidatorInterface $validator */ $validator = \Drupal::service('path.validator'); $url_object = $validator->getUrlIfValidWithoutAccessCheck($path); if ($url_object && in_array($url_object->getRouteName(), ["entity.node.canonical", "activitypub.user.self", "entity.comment.canonical"])) { $entity_type_id = $entity_id = NULL; switch ($url_object->getRouteName()) { case 'entity.node.canonical': $entity_type_id = 'node'; $entity_id = $url_object->getRouteParameters()['node']; break; case 'entity.comment.canonical': $entity_type_id = 'comment'; $entity_id = $url_object->getRouteParameters()['comment']; break; case 'activitypub.user.self': $entity_type_id = 'user'; $entity_id = $url_object->getRouteParameters()['user']; break; } if ($entity_type_id && $entity_id) { $entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id); if ($entity) { return $entity; } } } } } catch (\Exception $ignored) {} return NULL; } /** * Build audience for an activity. * * @param $to * @param $cc * @param $mention * @param \Drupal\activitypub\Entity\ActivityPubActivityInterface $activity * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function buildAudience(&$to, &$cc, &$mention, ActivityPubActivityInterface $activity) { if ($activity->isUnlisted()) { return; } $to = $activity->getTo(); if ($activity->isPublic()) { array_unshift($to, ActivityPubActivityInterface::PUBLIC_URL); } if ($activity->isPublic() || $activity->isFollowers()) { /** @var \Drupal\activitypub\Entity\ActivityPubActorInterface $actor */ $actor = $this->entityTypeManager->getStorage('activitypub_actor')->loadActorByEntityIdAndType($activity->getOwnerId(), 'person'); $followers_url = $this->renderUrl(Url::fromRoute('activitypub.followers', ['user' => $actor->getOwnerId(), 'activitypub_actor' => $actor->getName()], ['absolute' => TRUE])); if ($activity->isPublic()) { $cc = [$followers_url]; } else { $to = [$followers_url]; } } if ($activity->isPrivate()) { if (!empty($to[0])) { $href = $to[0]; $parsed = parse_url($href); if (!empty($parsed['host']) && !empty($parsed['path'])) { // A bit naive, but let's fix this later. $ex = explode('/', $parsed['path']); $username = array_pop($ex); $name = '@' . $username . '@' . $parsed['host']; } else { $name = $href; } $mention = [ 'type' => 'Mention', 'href' => $href, 'name' => $name, ]; } } $audience = ['to' => $to, 'cc' => $cc, 'mention' => $mention]; $event = new ActivityAudienceEvent($activity, $audience); $this->eventDispatcher->dispatch($event, ActivityAudienceEvent::ALTER_AUDIENCE); $audience = $event->getAudience(); $to = $audience['to']; $cc = $audience['cc']; $mention = $audience['mention']; } }