media_library_extend_crowdriff-1.x-dev/src/CrowdriffMediaLibrarySourceBase.php

src/CrowdriffMediaLibrarySourceBase.php
<?php

namespace Drupal\media_library_extend_crowdriff;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Utility\Token;
use Drupal\crowdriff_api\CrowdriffService;
use Drupal\file\FileRepositoryInterface;
use Drupal\media_library_extend\Plugin\MediaLibrarySource\MediaLibrarySourceBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a base class for Crowdriff media library panes to inherit from.
 */
class CrowdriffMediaLibrarySourceBase extends MediaLibrarySourceBase {

  /**
   * The Crowdriff service.
   *
   * @var \Drupal\crowdriff_api\CrowdriffService
   */
  protected $crowdriffService;

  /**
   * The Crowdriff asset service.
   *
   * @var \Drupal\media_library_extend_crowdriff\CrowdriffAssetService
   */
  protected $crowdriffAssetService;

  /**
   * The current request.
   *
   * @var \Symfony\Component\HttpFoundation\Request|\Drupal\Core\Http\RequestStack
   */
  protected $currentRequest;

  /**
   * The file repository.
   *
   * @var \Drupal\file\FileRepositoryInterface
   */
  protected $fileRepository;

  /**
   * The constructor.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Utility\Token $token
   *   The token service.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system.
   * @param \Drupal\crowdriff_api\CrowdriffService $crowdriff_service
   *   The Crowdriff service.
   * @param \Drupal\media_library_extend_crowdriff\CrowdriffAssetService $crowdriff_asset_service
   *   The Crowdriff asset service.
   * @param \Symfony\Component\HttpFoundation\Request|\Drupal\Core\Http\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\file\FileRepositoryInterface $file_repository
   *   The file repository.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    EntityTypeManagerInterface $entity_type_manager,
    Token $token,
    FileSystemInterface $file_system,
    CrowdriffService $crowdriff_service,
    CrowdriffAssetService $crowdriff_asset_service,
    $request_stack,
    FileRepositoryInterface $file_repository
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $token, $file_system);
    $this->crowdriffService = $crowdriff_service;
    $this->crowdriffAssetService = $crowdriff_asset_service;
    $this->currentRequest = $request_stack->getMainRequest();
    $this->fileRepository = $file_repository;
  }

  /**
   * {@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('token'),
      $container->get('file_system'),
      $container->get('crowdriff_api.crowdriff_service'),
      $container->get('media_library_extend_crowdriff.crowdriff_asset_service'),
      $container->get('request_stack'),
      $container->get('file.repository')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration(): array {
    return [
      'items_per_page' => 20,
      'hide_existing_assets' => FALSE,
      'show_asset_size' => FALSE,
      'max_asset_size' => 25,
      'media_type' => '',
      'sort_direction' => 'dsc',
      'sort_field' => 'created_at',
      'orientation' => '',
      'apps' => FALSE,
      'curated' => FALSE,
      'copied' => FALSE,
      'debug' => FALSE,
    ] + parent::defaultConfiguration();
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
    $form['items_per_page'] = [
      '#title' => $this->t('Items per page'),
      '#description' => $this->t('Specifies the number of assets to be returned, per page.'),
      '#type' => 'number',
      '#min' => 1,
      '#max' => 100,
      '#default_value' => $this->configuration['items_per_page'],
      '#required' => TRUE,
    ];

    $form['media_type'] = [
      '#title' => $this->t('Media type'),
      '#description' => $this->t('Filter by assets of a specific media type.'),
      '#type' => 'select',
      '#empty_option' => $this->t('- Select -'),
      '#options' => $this->getMediaTypes(),
      '#default_value' => $this->configuration['media_type'],
    ];

    $form['sort_direction'] = [
      '#title' => $this->t('Sort direction'),
      '#description' => $this->t('Sort assets in ascending or descending order. This is the default sort direction.'),
      '#type' => 'select',
      '#empty_option' => $this->t('- Select -'),
      '#options' => $this->getSortDirections(),
      '#default_value' => $this->configuration['sort_direction'],
    ];

    $form['curated'] = [
      '#title' => $this->t('Curated'),
      '#description' => $this->t('Filter assets that have been used by other users.'),
      '#type' => 'checkbox',
      '#default_value' => $this->configuration['curated'],
    ];

    $form['copied'] = [
      '#title' => $this->t('Copied'),
      '#description' => $this->t('Filter assets that have been copied.'),
      '#type' => 'checkbox',
      '#default_value' => $this->configuration['copied'],
    ];

    $form['orientation'] = [
      '#title' => $this->t('Orientation'),
      '#description' => $this->t('Filter by assets that have a specific orientation (landscape, square, portrait).'),
      '#type' => 'select',
      '#empty_option' => $this->t('- Select -'),
      '#options' => $this->getOrientations(),
      '#default_value' => $this->configuration['orientation'],
    ];

    $form['hide_existing_assets'] = [
      '#title' => $this->t('Hide existing assets'),
      '#description' => $this->t('Hide assets which already exist as media entities. This will filter out existing assets.'),
      '#type' => 'checkbox',
      '#default_value' => $this->configuration['hide_existing_assets'],
    ];

    $form['show_asset_size'] = [
      '#title' => $this->t('Show asset file size'),
      '#description' => $this->t('Displays asset file size in media library view.'),
      '#type' => 'checkbox',
      '#default_value' => $this->configuration['show_asset_size'],
    ];

    $form['max_asset_size'] = [
      '#title' => $this->t('Max asset file size'),
      '#description' => $this->t('Maximum file size in (MB) for asset. This will filter out assets larger than set size.'),
      '#type' => 'number',
      '#min' => 1,
      '#max' => 100,
      '#field_suffix' => $this->t('MB'),
      '#default_value' => $this->configuration['max_asset_size'],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $values = array_intersect_key($form_state->getValues(), $this->configuration);
    foreach ($values as $config_key => $config_value) {
      $this->configuration[$config_key] = $config_value;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getSummary(): array {
    return [
      '#theme' => 'item_list',
      '#list_type' => 'ul',
      '#items' => [
        $this->formatPlural($this->configuration['items_per_page'], '@count item per page', '@count items per page'),
        $this->t('Media type: @type', ['@type' => ucfirst($this->configuration['media_type'])]),
        $this->t('Sort direction: @direction', ['@direction' => ucfirst($this->configuration['sort_direction'])]),
        $this->t('Orientation: @orientation', ['@orientation' => ($this->configuration['orientation']) ?: 'All']),
        $this->t('Curated: @curated', ['@curated' => $this->configuration['curated'] ? 'Yes' : 'No']),
        $this->t('Copied: @copied', ['@copied' => $this->configuration['copied'] ? 'Yes' : 'No']),
        $this->t('Hide existing assets: @hide', ['@hide' => $this->configuration['hide_existing_assets'] ? 'Yes' : 'No']),
        $this->t('Display asset size: @size', ['@size' => $this->configuration['show_asset_size'] ? 'Yes' : 'No']),
        $this->t('Max asset size: @size MB', ['@size' => $this->configuration['max_asset_size']]),
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getCount() {
    return NULL;
  }

  /**
   * Get total matched.
   *
   * @return int
   *   Returns total matched assets.
   */
  protected function getTotalMatched(): int {
    $total_matched = 0;
    $session = $this->currentRequest->getSession();
    if ($session) {
      if (!$session->get($this->getPluginId() . '_total_matched')) {
        $session->set($this->getPluginId() . '_total_matched', $total_matched);
        $session->save();
      }
      else {
        $total_matched = $session->get($this->getPluginId() . '_total_matched');
      }
    }
    return $total_matched;
  }

  /**
   * Set total matched.
   *
   * @param int $matched
   *   The total matched assets.
   */
  protected function setTotalMatched(int $matched) {
    $session = $this->currentRequest->getSession();
    if ($session) {
      $session->set($this->getPluginId() . '_total_matched', $matched);
      $session->save();
    }
  }

  /**
   * Get paging.
   *
   * @return array
   *   Returns the paging array.
   */
  protected function getPaging(): array {
    $paging = [];
    $session = $this->currentRequest->getSession();
    if ($session) {
      if (!$session->get($this->getPluginId() . '_paging')) {
        $session->set($this->getPluginId() . '_paging', $paging);
        $session->save();
      }
      else {
        $paging = $session->get($this->getPluginId() . '_paging');
      }
    }
    return $paging;
  }

  /**
   * Set paging.
   *
   * @param array $paging
   *   The paging array.
   */
  protected function setPaging(array $paging) {
    $session = $this->currentRequest->getSession();
    if ($session) {
      $session->set($this->getPluginId() . '_paging', $paging);
      $session->save();
    }
  }

  /**
   * Set page and it's paging key.
   *
   * @param int $page
   *   The page.
   * @param string $paging_key
   *   The paging key.
   */
  protected function setPage(int $page, string $paging_key) {
    $paging = $this->getPaging();
    $paging[$page] = $paging_key;
    $this->setPaging($paging);
  }

  /**
   * Get submitted values.
   *
   * @return array
   *   Returns array of submitted values.
   */
  protected function getSubmittedValues(): array {
    return [
      'search_term' => $this->getValue('search_term'),
      'album' => $this->getValue('album'),
      'app' => $this->getValue('app'),
      'sources' => $this->getValue('sources'),
      'sort' => $this->getValue('sort'),
      'sort_field' => $this->getValue('sort_field'),
      'orientation' => $this->getValue('orientation'),
      'page' => $this->getValue('page'),
    ];
  }

  /**
   * Get Crowdriff search params.
   *
   * @param array $values
   *   The submitted/filter values.
   *
   * @return array
   *   Returns the search params.
   */
  protected function getSearchParams(array $values): array {
    // Get search term.
    $search_term = $values['search_term'];

    // Get apps.
    $apps = [];
    if (!empty($values['app'])) {
      $apps = [$values['app']];
    }

    // Get albums.
    $albums = [];
    if (!empty($values['album'])) {
      $albums = [$values['album']];
    }

    // Get sources.
    $sources = [];
    if (!empty($values['sources'])) {
      $sources = [$values['sources']];
    }

    // Get sort direction.
    $sort = $this->configuration['sort_direction'];
    if (!empty($values['sort'])) {
      $sort = $values['sort'];
    }

    // Get sort field.
    $sort_field = $this->configuration['sort_field'];
    if (!empty($values['sort_field'])) {
      $sort_field = $values['sort_field'];
    }

    // Get orientation.
    $orientation = $this->configuration['orientation'];
    if (!empty($values['orientation'])) {
      $orientation = $values['orientation'];
    }

    // Build API search params.
    $params = [
      'search_filter' => (object) [
        'apps' => $apps,
        'albums' => $albums,
        'media_type' => $this->configuration['media_type'],
        'sources' => $sources,
        'orientation' => $orientation,
      ],
      'order' => (object) [
        'field' => $sort_field,
        'direction' => $sort,
      ],
      'search_term' => trim($search_term),
      'curated' => (bool) $this->configuration['curated'],
      'copied' => (bool) $this->configuration['copied'],
      'count' => (int) $this->configuration['items_per_page'],
    ];

    if ($this->configuration['debug']) {
      $this->crowdriffService->getLogger()->debug(print_r($params, TRUE));
    }

    return $params;
  }

  /**
   * Get valid orientations.
   *
   * @return array
   *   Returns array of valid asset orientations.
   */
  protected function getOrientations(): array {
    return [
      'landscape' => $this->t('Landscape'),
      'square' => $this->t('Square'),
      'portrait' => $this->t('Portrait'),
    ];
  }

  /**
   * Get valid media types.
   *
   * @return array
   *   Returns array of valid media types.
   */
  protected function getMediaTypes(): array {
    return [
      'image' => $this->t('Image'),
      'video' => $this->t('Video'),
    ];
  }

  /**
   * Get valid source options.
   *
   * @return array
   *   Returns array of valid sources.
   */
  protected function getSources(): array {
    return [
      'instagram' => $this->t('Instagram'),
      'facebook' => $this->t('Facebook'),
      'twitter' => $this->t('Twitter'),
      'owned' => $this->t('Owned'),
    ];
  }

  /**
   * Get valid sort direction options.
   *
   * @return array
   *   Returns array of valid sort directions.
   */
  protected function getSortDirections(): array {
    return [
      'asc' => $this->t('Ascending'),
      'dsc' => $this->t('Descending'),
    ];
  }

  /**
   * Get valid sort field options.
   *
   * @return array
   *   Returns array of valid sort fields.
   */
  protected function getSortFields(): array {
    return [
      'created_at' => $this->t('Date Created'),
      'added_at' => $this->t('Date Added'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array &$form, FormStateInterface $form_state): array {
    // Make form inline.
    $form['#attributes']['class'][] = 'crowdriff-media-library-filters';

    // Search term.
    $form['search_term'] = [
      "#prefix" => '<div class="form--inline form--even">',
      '#type' => 'textfield',
      '#title' => $this->t('Search keyword/term'),
      "#description" => $this->t('Filter assets by a specific keyword/term.'),
      '#size' => 30,
    ];

    // Albums.
    $albums = $this->crowdriffService->getAlbums();
    $album_options = [];
    foreach ($albums as $album) {
      if (!empty($album['id'])) {
        $album_options[$album['id']] = $album['label'];
      }
    }
    asort($album_options);
    $form['album'] = [
      '#type' => 'select',
      '#title' => $this->t('Album'),
      '#description' => $this->t('Assets by selected album.'),
      '#options' => $album_options,
      '#empty_option' => $this->t('- Select -'),
    ];

    // Apps.
    if ($this->configuration['apps']) {
      $apps = $this->crowdriffService->getApps();
      $app_options = [];
      foreach ($apps as $app) {
        if (!empty($app['id'])) {
          $app_options[$app['id']] = $app['label'];
        }
      }
      asort($app_options);
      $form['app'] = [
        '#type' => 'select',
        '#title' => $this->t('App'),
        '#description' => $this->t('Assets by selected app.'),
        '#options' => $app_options,
        '#empty_option' => $this->t('- Select -'),
        '#suffix' => '</div>',
      ];
    }
    else {
      $form['album']['#suffix'] = '</div>';
    }

    // Sources.
    $sources = $this->getSources();
    $form['sources'] = [
      "#prefix" => '<div class="form--inline form--even">',
      '#type' => 'select',
      '#title' => $this->t('Source'),
      '#description' => $this->t('Assets by source.'),
      '#options' => $sources,
      '#empty_option' => $this->t('- Select -'),
    ];

    // Sort direction.
    $sort_directions = $this->getSortDirections();
    $form['sort'] = [
      '#type' => 'select',
      '#title' => $this->t('Sort'),
      '#description' => $this->t('Sorting direction.'),
      '#options' => $sort_directions,
      '#empty_option' => $this->t('- Select -'),
      '#default_value' => $this->configuration['sort_direction'],
    ];

    // Sort field.
    $sort_fields = $this->getSortFields();
    $form['sort_field'] = [
      '#type' => 'select',
      '#title' => $this->t('Sort by'),
      '#description' => $this->t('Sorting field.'),
      '#options' => $sort_fields,
      '#empty_option' => $this->t('- Select -'),
      '#default_value' => $this->configuration['sort_field'],
    ];

    // Orientation.
    $orientations = $this->getOrientations();
    $form['orientation'] = [
      '#type' => 'select',
      '#title' => $this->t('Orientation'),
      '#description' => $this->t('Asset orientation.'),
      '#options' => $orientations,
      '#empty_option' => $this->t('- Select -'),
      '#default_value' => $this->configuration['orientation'],
      '#suffix' => '</div>',
    ];

    // Attach library to form.
    $form['#attached']['library'][] = 'media_library_extend_crowdriff/media_library';

    return $form;
  }

  /**
   * Build/generate cache key.
   *
   * @param array $values
   *   The array of submitted values.
   * @param int $page
   *   The current page.
   * @param string|null $paging_key
   *   The Crowdriff API paging_key, if set.
   *
   * @return string
   *   The final cache key.
   */
  protected function buildCacheKey(array $values, int $page, string $paging_key = NULL): string {
    // Build prefix part of key.
    $prefix_key = $this->getPluginId();

    // Build suffix part of key.
    $suffix_key = '';
    // Album filter.
    if (!empty($values['album'])) {
      $suffix_key .= ':' . implode(',', [$values['album']]);
    }

    // App filter.
    if (!empty($values['app'])) {
      $suffix_key .= ':' . implode(',', [$values['app']]);
    }

    // Source filter.
    if (!empty($values['sources'])) {
      $suffix_key .= ':' . implode(',', [$values['sources']]);
    }

    // Sort direction filter.
    if (!empty($values['sort'])) {
      $suffix_key .= ':' . $values['sort'];
    }

    // Sort field filter.
    if (!empty($values['sort_field'])) {
      $suffix_key .= ':' . $values['sort_field'];
    }

    // Orientation filter.
    if (!empty($values['orientation'])) {
      $suffix_key .= ':' . $values['orientation'];
    }

    // Media type configuration.
    if (!empty($this->configuration['media_type'])) {
      $suffix_key .= ':' . $this->configuration['media_type'];
    }

    // Orientation configuration.
    if (!empty($this->configuration['orientation'])) {
      $suffix_key .= ':' . $this->configuration['orientation'];
    }

    // Curated configuration.
    if ($this->configuration['curated']) {
      $suffix_key .= ':curated';
    }

    // Copied configuration.
    if ($this->configuration['copied']) {
      $suffix_key .= ':copied';
    }

    // Items per page configuration.
    if (!empty($this->configuration['items_per_page'])) {
      $suffix_key .= ':' . $this->configuration['items_per_page'];
    }

    // The current page.
    $suffix_key .= ':' . $page;

    // Crowdriff API paging key.
    if (!empty($paging_key)) {
      $suffix_key .= ':' . $paging_key;
    }

    // Return cache key.
    return $prefix_key . ':' . md5($suffix_key);
  }

  /**
   * Search API assets.
   *
   * @param array $values
   *   The submitted/filter values.
   *
   * @return array
   *   Return the assets/results.
   *
   * @throws \GuzzleHttp\Exception\GuzzleException
   */
  protected function searchAssets(array $values): array {
    // Get search params.
    $params = $this->getSearchParams($values);

    // Get current page.
    $page = $this->getValue('page');

    // Get paging.
    $paging = $this->getPaging();

    // Get paging key, if set.
    $paging_key = NULL;
    if (isset($paging[$page])) {
      $paging_key = $paging[$page];
    }
    else {
      $paging[$page] = $paging_key;
    }

    // Build cache key.
    $cache_key = $this->buildCacheKey($values, $page, $paging_key);

    // Get data.
    $data = $this->crowdriffService->makeRequest($this->crowdriffService::CROWDRIFF_SEARCH_URL, $cache_key, 'POST', $params, $paging_key);

    // Set paging key.
    if (!empty($data['paging_key'])) {
      // Increment current page.
      $page = $page + 1;

      // Set paging key for incremented page.
      $paging[$page] = $data['paging_key'];

      // Set paging array back to session.
      $this->setPaging($paging);
    }

    // Set total matched.
    $this->setTotalMatched($data['matched']);

    // Return results.
    return [
      'assets' => !empty($data['assets']) ? $data['assets'] : NULL,
      'paging_key' => !empty($data['paging_key']) ? $data['paging_key'] : NULL,
    ];
  }

  /**
   * Format bytes to readable format.
   *
   * @param int $bytes
   *   The file size in bytes.
   * @param int $precision
   *   The precision.
   *
   * @return string
   *   The conversion.
   */
  private function formatBytes(int $bytes, int $precision = 2): string {
    if ($bytes > pow(1024, 3)) {
      return round($bytes / pow(1024, 3), $precision) . "GB";
    }
    elseif ($bytes > pow(1024, 2)) {
      return round($bytes / pow(1024, 2), $precision) . "MB";
    }
    elseif ($bytes > 1024) {
      return round($bytes / 1024, $precision) . "KB";
    }
    else {
      return ($bytes) . "B";
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getResults(): array {
    // Get submitted values.
    $values = $this->getSubmittedValues();

    // Get assets.
    $results = $this->searchAssets($values);

    // Make sure we have assets to process.
    $assets = [];
    if (!empty($results['assets'])) {

      // Loop over assets and build up results.
      foreach ($results['assets'] as $asset) {

        // Check filesize.
        if (!$this->crowdriffAssetService->checkAssetFilesize($asset, $this->configuration['max_asset_size'])) {
          // Asset filesize exceeded max size. Skip asset.
          continue;
        }

        // Get thumbnail.
        $thumbnail = NULL;
        if (!empty($asset['thumbnail_mobile'])) {
          $thumbnail = $asset['thumbnail_mobile'];
        }
        if (!$thumbnail && !empty($asset['thumbnail_gallery'])) {
          $thumbnail = $asset['thumbnail_gallery'];
        }

        // Build up asset item.
        $asset_item = [
          'id' => $asset['uuid'],
          'label' => $asset['uuid'],
          'preview' => [
            [
              '#type' => 'html_tag',
              '#tag' => 'img',
              '#attributes' => [
                'src' => $thumbnail,
                'alt' => '',
                'title' => '',
                'caption' => strip_tags($asset['text']),
              ],
            ],
          ],
        ];

        // Add label to indicate asset is already stored as media entity.
        if ($this->crowdriffAssetService->assetExists($asset['uuid'], $this->getTargetBundle())) {
          // Hide asset if configuration flag is set.
          if ($this->configuration['hide_existing_assets']) {
            // Skip asset.
            continue;
          }
          $asset_item['preview']['asset_exists'] = [
            '#type' => 'html_tag',
            '#tag' => 'div',
            '#value' => $this->t('Asset already exists.'),
            '#attributes' => [
              'class' => ['media-library-item__asset-exists'],
            ],
          ];
        }

        // Show asset file size.
        if ($this->configuration['show_asset_size']) {

          // Show asset image file size.
          if ($asset['media_type'] == 'image' && !empty($asset['image_original']['size'])) {
            $asset_item['preview']['asset_image_size'] = [
              '#type' => 'html_tag',
              '#tag' => 'div',
              '#value' => $this->t('Size: @size', ['@size' => $this->formatBytes($asset['image_original']['size'])]),
              '#attributes' => [
                'class' => ['media-library-item__asset-size'],
              ],
            ];
          }

          // Show asset video file size.
          if ($asset['media_type'] == 'video' && !empty($asset['video_original']['size'])) {
            $asset_item['preview']['asset_video_size'] = [
              '#type' => 'html_tag',
              '#tag' => 'div',
              '#value' => $this->t('Size: @size', ['@size' => $this->formatBytes($asset['video_original']['size'])]),
              '#attributes' => [
                'class' => ['media-library-item__asset-size'],
              ],
            ];
          }
        }

        // Add item to assets.
        $assets[] = $asset_item;
      }
    }

    // Return assets/results.
    return $assets;
  }

  /**
   * {@inheritdoc}
   */
  public function getEntityId(string $selected_id) {
  }

}

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

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