contextly-8.x-2.1/src/Contextly/ContextlyDrupalKit.php

src/Contextly/ContextlyDrupalKit.php
<?php

namespace Drupal\contextly\Contextly;

use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use Drupal\node\NodeInterface;

/**
 * Drupal-specific kit implementation.
 *
 * @method ContextlyDrupalApiSessionShared newApiSession()
 * @method ContextlyDrupalApiTransport newApiTransport()
 * @method ContextlyDrupalAssetsList newAssetsList()
 *
 * @method ContextlyDrupalApiSessionShared newDrupalApiSessionShared()
 * @method ContextlyDrupalApiTransport newDrupalApiTransport()
 * @method ContextlyDrupalAssetsLibraryRenderer newDrupalAssetsLibraryRenderer()
 * @method ContextlyDrupalNodeEditor newDrupalNodeEditor()
 * @method ContextlyDrupalNodeData newDrupalNodeData()
 * @method ContextlyDrupalException newDrupalException()
 */
class ContextlyDrupalKit extends \ContextlyKit {

  // @todo remove if field is exist on admin form.
  const KIT_PATH = '/libraries/contextly-kit';

  /**
   * Return instance.
   *
   * @return \ContextlyDrupalKit
   *   The ContextlyDrupalKit object.
   */
  public static function getInstance() {
    static $instance;

    if (!isset($instance)) {
      $config = self::getDefaultSettings();
      $instance = new self($config);
    }

    return $instance;
  }

  /**
   * Return the Contextly Kit Settings.
   *
   * @return \ContextlyKitSettings
   *   The ContextlyKitSettings object.
   */
  public static function getDefaultSettings() {
    $config = new \ContextlyKitSettings();
    $drupal_config = \Drupal::service('config.factory')
      ->get('contextly.settings');
    $config->cdn = FALSE;
    $config->mode = $drupal_config->get('server_mode');

    $key = \Drupal::service('contextly.base')->getApiKey();
    if (!empty($key['appID']) && !empty($key['appSecret'])) {
      $config->appID = $key['appID'];
      $config->appSecret = $key['appSecret'];
    }

    return $config;
  }

  /**
   * Return the class map.
   *
   * @return array
   *   The classes map array.
   */
  protected function getClassesMap() {
    $map = parent::getClassesMap();

    // Overrides.
    $map['ApiSession'] = '\Drupal\contextly\Contextly\ContextlyDrupalApiSessionShared';
    $map['ApiTransport'] = '\Drupal\contextly\Contextly\ContextlyDrupalApiTransport';
    $map['AssetsList'] = '\Drupal\contextly\Contextly\ContextlyDrupalAssetsList';

    // Drupal-specific classes.
    $map['DrupalApiSessionShared'] = '\Drupal\contextly\Contextly\ContextlyDrupalApiSessionShared';
    $map['DrupalApiTransport'] = '\Drupal\contextly\Contextly\ContextlyDrupalApiTransport';
    $map['DrupalAssetsLibraryRenderer'] = '\Drupal\contextly\Contextly\ContextlyDrupalAssetsLibraryRenderer';
    $map['DrupalNodeEditor'] = '\Drupal\contextly\Contextly\ContextlyDrupalNodeEditor';
    $map['DrupalNodeData'] = '\Drupal\contextly\Contextly\ContextlyDrupalNodeData';
    $map['DrupalException'] = '\Drupal\contextly\Contextly\ContextlyDrupalException';

    return $map;
  }

  /**
   * Create file url.
   *
   * @param string $filepath
   *   The file path.
   *
   * @return string
   *   The file url.
   */
  public function buildFileUrl($filepath) {
    // @todo prepare field on admin form for library folder.
    return \Drupal::service('file_url_generator')
      ->generateString($this::KIT_PATH . '/' . $filepath);
  }

}

/**
 * Sends updated node to the Contextly service, builds JS settings.
 *
 * @property ContextlyDrupalKit $kit
 */
class ContextlyDrupalNodeEditor extends \ContextlyKitBase {

  /**
   * The ContextlyKitApi definition.
   *
   * @var \ContextlyKitApi
   */
  protected $api;

  /**
   * The constructor.
   *
   * @param \ContextlyKit $kit
   *   The ContextlyKit object.
   */
  public function __construct(\ContextlyKit $kit) {
    parent::__construct($kit);

    $this->api = $this->kit->newApi();
  }

  /**
   * Sends the node to Contextly.
   *
   * @param \Drupal\node\NodeInterface $node
   *   The node entity.
   */
  public function putNode(NodeInterface $node) {
    $this->putNodeContent($node);
    if (\Drupal::service('module_handler')->moduleExists('taxonomy')) {
      $this->putNodeTags($node);
    }
  }

  /**
   * Sends the node text content and meta-information to Contextly.
   *
   * @param \Drupal\node\NodeInterface $node
   *   The node entity.
   */
  protected function putNodeContent(NodeInterface $node) {
    // Build absolute node URL.
    /** @var \Drupal\Core\Url $uri */
    $url = $node->toUrl();
    if (empty($url)) {
      throw $this->kit->newDrupalException($this->t('Unable to generate URL for the node #@nid.', [
        '@nid' => $node->id(),
      ]));
    }
    $url->setAbsolute();
    $node_url = $url->toString();

    // Check if post has been saved to Contextly earlier.
    $contextly_post = $this->api
      ->method('posts', 'get')
      ->param('page_id', $node->id())
      ->get();

    /** @var \Drupal\user\UserInterface $user */
    $user = $node->uid->entity;

    // @todo Take care about langcode.
    $entity_type_manager = \Drupal::service('entity_type.manager');
    $view_builder = $entity_type_manager->getViewBuilder('node');
    $content = $view_builder->view($node, 'contextly');
    $content = \Drupal::service('renderer')->render($content);

    $base_service = \Drupal::service('contextly.base');
    $post_data = [
      'post_id' => $node->id(),
      'post_title' => $node->title->value,
      'post_date' => $base_service->formatDate($node->created->value),
      'post_modified' => $base_service->formatDate($node->changed->value),
      'post_status' => $node->status->value ? 'publish' : 'draft',
      'post_type' => $node->bundle(),
      'post_content' => $content,
      'url' => $node_url,
      'author_id' => !empty($user) ? $user->id() : '',
      'post_author' => !empty($user) ? $user->getAccountName() : '',
    ];
    $this->api
      ->method('posts', 'put')
      ->extraParams($post_data);
    if (isset($contextly_post->entry)) {
      $this->api->param('id', $contextly_post->entry->id);
    }

    $this->api
      ->requireSuccess()
      ->get();
  }

  /**
   * Sends node tags to the Contextly.
   *
   * @param \Drupal\node\NodeInterface $node
   *   The node entity.
   */
  protected function putNodeTags(NodeInterface $node) {
    // Remove existing tags first, if any.
    // @todo Handle pagination of the request.
    $post_tags = $this->api
      ->method('poststags', 'list')
      ->searchParam('post_id', \ContextlyKitApiRequest::SEARCH_TYPE_EQUAL, $node->id())
      ->get();
    if (!empty($post_tags->list)) {
      foreach ($post_tags->list as $tag) {
        $this->api
          ->method('poststags', 'delete')
          ->param('id', $tag->id)
          ->requireSuccess();
      }
    }

    // Save new tags.
    // @todo WP Plugin sends only 3 first tags. Why?
    $tags = $this->kit
      ->newDrupalNodeData($node)
      ->getTags();
    foreach ($tags as $tag) {
      $this->api
        ->method('poststags', 'put')
        ->extraParams([
          'post_id' => $node->id(),
          'name' => $tag,
        ])
        ->requireSuccess();
    }

    // Make all requests at once.
    $this->api->get();
  }

}

/**
 * The ContextlyDrupalApiTransport class.
 */
class ContextlyDrupalApiTransport implements \ContextlyKitApiTransportInterface {

  /**
   * Performs the HTTP request.
   *
   * @param string $method
   *   "GET" or "POST".
   * @param string $url
   *   Request URL.
   * @param array $query
   *   GET query parameters.
   * @param array $data
   *   POST data.
   * @param array $headers
   *   List of headers.
   *
   * @return \ContextlyKitApiResponse
   *   The response.
   */
  public function request($method,
    $url,
    $query = [],
    $data = [],
    $headers = []) {
    // Add content type to the headers.
    $headers['Content-Type'] = 'application/x-www-form-urlencoded';

    // Add query to the URL.
    $options = [
      'external' => TRUE,
      'query' => $query,
    ];
    $url = Url::fromUri($url, [], $options)->toString();

    /** @var \GuzzleHttp\Client $client */
    $client = \Drupal::httpClient();
    $options = [
      'form_params' => $data,
      'headers' => $headers,
    ];
    /** @var \GuzzleHttp\Psr7\Response $result */
    $result = $client->request($method, $url, $options);

    try {
      // Build response for the Kit.
      $response = new \ContextlyKitApiResponse();
      $response->code = $result->getStatusCode();
      $response->body = $result->getBody()->getContents();
      if ($response->code != 200) {
        $response->error = $result->getBody();
      }
    }
    catch (Exception $exc) {
      // Do nothing.
    }

    return $response;
  }

}

/**
 * The ContextlyDrupalApiSessionShared class.
 */
class ContextlyDrupalApiSessionShared extends \ContextlyKitBase implements \ContextlyKitApiSessionInterface {

  const TOKEN_CACHE_ID = 'contextly:access-token';
  const TOKEN_CACHE_BIN = 'cache';

  /**
   * The api token object.
   *
   * @var \ContextlyKitApiTokenInterface
   */
  protected $token;

  /**
   * The Drupal\Core\Cache\CacheBackendInterface definition.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * The constructor.
   *
   * @param \ContextlyKit $kit
   *   The contextly kit object.
   */
  public function __construct(\ContextlyKit $kit) {
    parent::__construct($kit);

    $this->cache = \Drupal::service('cache.data');
    $this->token = $this->loadSharedToken();
  }

  /**
   * Load the shared token.
   *
   * @return \ContextlyKitApiTokenInterface
   *   The api token.
   */
  public function loadSharedToken() {
    $cache = $this->cache->get(self::TOKEN_CACHE_ID);

    if ($cache) {
      try {
        return $this->kit->newApiToken($cache->data);
      }
      catch (\ContextlyKitException $e) {
        // Just suppress the exception on a broken saved value.
        Error::logException('contextly', $e);
      }
    }

    // Fallback to an empty token.
    return $this->kit->newApiTokenEmpty();
  }

  /**
   * Save the shared token.
   *
   * @param \ContextlyKitApiTokenInterface $token
   *   The token object.
   */
  public function saveSharedToken(\ContextlyKitApiTokenInterface $token) {
    $expire = $token->getExpirationDate();
    $this->cache->set(self::TOKEN_CACHE_ID, (string) $token, $expire);
  }

  /**
   * Remove shared token.
   */
  public function removeSharedToken() {
    $this->cache->delete(self::TOKEN_CACHE_ID);
  }

  /**
   * Cleanup the token.
   */
  public function cleanupToken() {
    $this->token = $this->kit->newApiTokenEmpty();
    $this->removeSharedToken();
  }

  /**
   * Set the token.
   *
   * @param \ContextlyKitApiTokenInterface $token
   *   The token.
   */
  public function setToken($token) {
    $this->token = $token;
    $this->saveSharedToken($token);
  }

  /**
   * Return the token.
   *
   * @return \ContextlyKitApiTokenInterface
   *   The token.
   */
  public function getToken() {
    return $this->token;
  }

}

/**
 * The contextly drupal assets list class.
 */
class ContextlyDrupalAssetsList extends \ContextlyKitAssetsPackage {

  /**
   * Return the css paths.
   *
   * @return string
   *   The css paths.
   */
  public function buildCssPaths() {
    $css = $this->getCss();
    if (empty($css)) {
      return [];
    }

    $paths = [];
    $basePath = _contextly_kit_path() . '/' . $this->kit->getFolderPath('client') . '/';
    foreach ($css as $path) {
      $paths[$path] = $basePath . $path . '.css';
    }

    return $paths;
  }

  /**
   * Return the js paths.
   *
   * @return string
   *   The js paths.
   */
  public function buildJsPaths() {
    $js = $this->getJs();
    if (empty($js)) {
      return [];
    }

    $paths = [];
    $basePath = _contextly_kit_path() . '/' . $this->kit->getFolderPath('client') . '/';
    foreach ($js as $path) {
      $paths[$path] = $basePath . $path . '.js';
    }

    return $paths;
  }

}

/**
 * Renders Contextly Kit assets in format suitable for hook_library() entry.
 *
 * @property ContextlyDrupalAssetsList $assets
 */
class ContextlyDrupalAssetsLibraryRenderer extends \ContextlyKitAssetsRenderer {

  /**
   * Render assets.
   *
   * @param string $assetsMethod
   *   The assets method.
   * @param bool $external
   *   External flag.
   *
   * @return array
   *   The rendered assets.
   */
  protected function renderAssets($assetsMethod,
    $external = FALSE) {
    $uris = array_values($this->assets->{$assetsMethod}());
    if (empty($uris)) {
      return [];
    }

    $options = [];
    if ($external) {
      $options += [
        'type' => 'external',
      ];
    }
    return array_combine($uris, array_fill(0, count($uris), $options));
  }

  /**
   * Render css libraries.
   *
   * @return string
   *   The rendered css libraries.
   */
  public function renderCss() {
    if ($this->kit->isCdnEnabled()) {
      return $this->renderAssets('buildCssUrls', TRUE);
    }
    else {
      return $this->renderAssets('buildCssPaths');
    }
  }

  /**
   * Render js libraries.
   *
   * @return string
   *   The rendered js library.
   */
  public function renderJs() {
    if ($this->kit->isCdnEnabled()) {
      return $this->renderAssets('buildJsUrls', TRUE);
    }
    else {
      return $this->renderAssets('buildJsPaths');
    }
  }

  /**
   * Template rendering.
   */
  public function renderTpl() {
    // We don't support templates rendering to the Drupal library yet.
  }

  /**
   * Render css and js libraries.
   *
   * @return array
   *   The rendered css and js libraries.
   */
  public function renderAll() {
    return [
      'css' => $this->renderCss(),
      'js' => $this->renderJs(),
    ];
  }

}

/**
 * Helper to extract different data from the node.
 *
 * @property ContextlyDrupalKit $kit
 */
class ContextlyDrupalNodeData extends \ContextlyKitBase {

  /**
   * The Drupal\node\NodeInterface definition.
   *
   * @var \Drupal\node\NodeInterface
   */
  protected $node;

  /**
   * The construct function.
   *
   * @param string $kit
   *   The contextly kit.
   * @param \Drupal\node\NodeInterface $node
   *   The node entity.
   */
  public function __construct($kit,
    NodeInterface $node) {
    parent::__construct($kit);

    $this->node = $node;
  }

  /**
   * Return the metadata array.
   *
   * @param string $language
   *   The language code.
   *
   * @return array
   *   The metadata array.
   */
  public function getMetadata(string $language = NULL): array {
    $metadata = [];
    /** @var \Drupal\contextly\ContextlyBaseServiceInterface $base_service */
    $base_service = \Drupal::service('contextly.base');

    // Basic data.
    $metadata['title'] = $this->node->getTitle();
    $metadata['type'] = $this->node->bundle();
    $metadata['post_id'] = $this->node->id();

    // Timestamps.
    $metadata['pub_date'] = $base_service->formatDate($this->node->getCreatedTime());
    $metadata['mod_date'] = $base_service->formatDate($this->node->getChangedTime());

    // Node URL.
    $metadata['url'] = $this->getUrl();

    // Author info.
    /** @var \Drupal\user\UserInterface $author */
    $author = $this->getAuthor();
    if ($author) {
      $metadata['author_id'] = $author->id();
      $metadata['author_name'] = $author->getAccountName();
      $metadata['author_display_name'] = $author->getDisplayName();
    }
    else {
      $metadata['author_id'] = 0;
      $metadata['author_name'] = \Drupal::config('user.settings')->get('anonymous');
      $metadata['author_display_name'] = $metadata['author_name'];
    }

    // Tags and categories.
    // @todo Fill categories same way as tags, but from different fields.
    $metadata['tags'] = $this->getTags();
    $metadata['categories'] = [];

    // Featured image.
    $metadata['image'] = $this->getFeaturedImageUrl($language);

    return $metadata;
  }

  /**
   * Return the node author entity.
   *
   * @return \Drupal\user\Entity\User|null
   *   The author entity or null.
   */
  protected function getAuthor() {
    $author = NULL;
    if (!empty($this->node->uid)) {
      $author = $this->node->uid->entity;
    }
    return $author;
  }

  /**
   * Return the absolute url.
   *
   * @return string
   *   The absolute url.
   */
  protected function getUrl() {
    $uri = $this->node->toUrl();
    $uri->setAbsolute();
    return $uri->toString();
  }

  /**
   * Return the featured image url.
   *
   * @param string $langcode
   *   The language code.
   *
   * @return string|null
   *   The image url or null.
   */
  protected function getFeaturedImageUrl($langcode) {
    $image_url = NULL;
    if (!\Drupal::service('module_handler')->moduleExists('image')) {
      return $image_url;
    }

    /** @var \Drupal\contextly\ContextlyBaseServiceInterface $base_service */
    $base_service = \Drupal::service('contextly.base');
    $fields = $base_service
      ->getNodeTypeFields($this->node->bundle(), ['image']);
    if (empty($fields)) {
      return $image_url;
    }

    $field = key($fields);
    if (empty($langcode)) {
      $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
    }
    $translation = $this->node->getTranslation($langcode);
    /** @var \Drupal\file\FileInterface $image */
    $image = $translation->{$field}->entity;
    if (empty($image->getFileUri())) {
      return $image_url;
    }

    $uri = $image->getFileUri();
    $image_url = \Drupal::service('file_url_generator')
      ->generateAbsoluteString($uri);
    return $image_url;
  }

  /**
   * Extracts tags list attached to the node that should be sent to Contextly.
   *
   * @return array
   *   The tags array.
   */
  public function getTags() {
    $tags = [];
    if (!\Drupal::service('module_handler')->moduleExists('taxonomy')) {
      return $tags;
    }

    // Use either all available fields or selected fields only.
    /** @var \Drupal\contextly\ContextlyBaseServiceInterface $base_service */
    $base_service = \Drupal::service('contextly.base');
    $fields = $base_service
      ->getNodeTypeFields($this->node->bundle(), ['default:taxonomy_term']);

    // Collect all term IDs first.
    $tags = [];
    foreach ($fields as $field_name => $field_label) {
      if (!empty($this->node->{$field_name})) {
        // @todo Handle multi-language fields properly.
        // For now just post all languages.
        $terms = $this->node->{$field_name}->referencedEntities();
        $tags = array_merge($tags, $terms);
      }
    }

    return $tags;
  }

}

/**
 * The Drupal exception class.
 */
class ContextlyDrupalException extends \ContextlyKitException {

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc