entity_activity-8.x-1.0-beta13/modules/entity_activity_mass_subscribe/src/Form/MassSubscribeForm.php

modules/entity_activity_mass_subscribe/src/Form/MassSubscribeForm.php
<?php

namespace Drupal\entity_activity_mass_subscribe\Form;

use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * MassSubscribe form.
 */
class MassSubscribeForm extends FormBase implements ContainerInjectionInterface {

  /**
   * The entity repository service.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected $entityRepository;

  /**
   * Drupal\Core\Routing\RouteMatchInterface definition.
   *
   * @var \Drupal\Core\Routing\RouteMatchInterface
   */
  protected $routeMatch;

  /**
   * Drupal\Core\Entity\EntityTypeManagerInterface definition.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The subscription storage.
   *
   * @var \Drupal\entity_activity\SubscriptionStorageInterface
   */
  protected $storageSubscription;

  /**
   * The user storage.
   *
   * @var \Drupal\user\UserStorageInterface
   */
  protected $storageUser;

  /**
   * The default limit used to display users in the table select.
   *
   * @var int
   */
  protected $limit = 50;

  /**
   * The value corresponding to an unlimited query.
   *
   * @var int
   */
  protected $unlimited = -1;

  /**
   * ListSubscribersController constructor.
   *
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   *   The entity repository.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The route match service.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match) {
    $this->entityRepository = $entity_repository;
    $this->entityTypeManager = $entity_type_manager;
    $this->routeMatch = $route_match;
    $this->storageSubscription = $this->entityTypeManager->getStorage('entity_activity_subscription');
    $this->storageUser = $this->entityTypeManager->getStorage('user');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity.repository'),
      $container->get('entity_type.manager'),
      $container->get('current_route_match')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'entity_activity_mass_subscribe';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, AccountInterface $account = NULL, EntityInterface $entity = NULL) {
    if (!$account) {
      $account = $this->currentUser();
    }

    if (!$entity instanceof ContentEntityInterface) {
      $entity = $this->getCurrentEntity();
    }
    if (!$entity instanceof ContentEntityInterface) {
      return $form['#markup'] = $this->t('No entity found for mass subscribe feature');
    }

    $form['#theme'] = 'system_config_form';

    $form['entity'] = [
      '#type' => 'value',
      '#value' => $entity,
    ];

    $form['account'] = [
      '#type' => 'value',
      '#value' => $account,
    ];

    $methods = $this->getMethods($account, $entity);
    $form['method'] = [
      '#type' => 'select',
      '#title' => $this->t('Method'),
      '#description' => $this->t('Select a method to mass subscribe users to the @type <em>@label</em>.', ['@type' => $this->getEntityTypeLabel($entity), '@label' => $entity->label()]),
      '#options' => $methods,
      '#required' => TRUE,
      '#default_value' => '',
      '#ajax' => [
        'event' => 'change',
        'method' => 'replace',
        'wrapper' => 'subscribe-wrapper',
        'callback' => [$this, 'ajaxReplaceSubscribeCallback'],
      ],
    ];

    if (count($methods) <= 1) {
      $form['message'] = [
        '#type' => 'item',
        '#markup' => $this->t('Your current permissions do not allow you to access the mass subscription. Please contact an administrator.'),
        '#wrapper_attributes' => ['class' => ['messages', 'messages--warning']],
      ];
    }

    $form['subscribe'] = [
      '#type' => 'container',
      '#tree' => TRUE,
      '#prefix' => '<div id="subscribe-wrapper">',
      '#suffix' => '</div>',
    ];

    $method = $this->getElementPropertyValue('method', $form_state);
    if ($method) {
      switch ($method) {
        case 'role':
          $form['subscribe']['role'] = $this->getElementRole($form, $form_state);
          break;

        case 'user':
          $form['subscribe']['user'] = $this->getElementUser($form, $form_state);
          break;

        case 'list':
          $form['subscribe']['list'] = $this->getElementList($form, $form_state, $entity);
          $form['subscribe']['list']['#prefix'] = '<div id="user-list">';
          $form['subscribe']['list']['#suffix'] = '</div>';
          break;
      }
    }

    $form['unsubscribe'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Unsubscribe'),
      '#description' => $this->t('Check this option to <b>unsubcribe</b> users instead of subscribe them'),
      '#default_value' => 0,
      '#access' => (bool) $account->hasPermission('mass unsubscribe users'),
      '#states' => [
        'invisible' => [
          ':input[name="method"]' => ['value' => ''],
        ],
      ],
    ];

    $form['warning'] = [
      '#type' => 'item',
      '#markup' => $this->t('You have enabled the <b>Unsubscribe</b> mode. You are going to <b>unsubscribe</b> users selected instead of <em>subscribe</em> them.'),
      '#wrapper_attributes' => ['class' => ['messages', 'messages--warning']],
      '#access' => (bool) $account->hasPermission('mass unsubscribe users'),
      '#states' => [
        'visible' => [
          ':input[name="unsubscribe"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Mass subscribe'),
      '#button_type' => 'primary',
      '#access' => (bool) (count($methods) > 1),
    ];

    return $form;
  }

  /**
   * Ajax replace callback.
   *
   * @param array $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The form to return.
   */
  public function ajaxReplaceSubscribeCallback(array $form, FormStateInterface $form_state) {
    return $form['subscribe'];
  }

  /**
   * Ajax replace callback.
   *
   * @param array $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The form to return.
   */
  public function ajaxFilterCallback(array &$form, FormStateInterface &$form_state) {
    return $form['subscribe'];
  }

  /**
   * Get element property value.
   *
   * @param string|array $property
   *   The property to check.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param mixed $default
   *   The default value returned if property not found.
   *
   * @return array|mixed|null
   *   The value of the property found or the default value provided.
   */
  protected function getElementPropertyValue($property, FormStateInterface $form_state, $default = '') {
    return $form_state->hasValue($property)
      ? $form_state->getValue($property)
      : $default;
  }

  /**
   * Get the form element role.
   *
   * @param array $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The form element.
   */
  protected function getElementRole(array $form, FormStateInterface $form_state) {
    $element = [
      '#type' => 'checkboxes',
      '#title' => $this->t('Role'),
      '#description' => $this->t('Select the role(s) for which mass subscribe users.'),
      '#options' => $this->getRoles(),
      '#default_value' => $this->getElementPropertyValue(['subscribe', 'role'], $form_state, []),
      '#required' => TRUE,
    ];
    return $element;
  }

  /**
   * Get the form element user.
   *
   * @param array $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The form element.
   */
  protected function getElementUser(array $form, FormStateInterface $form_state) {
    $element = [
      '#type' => 'entity_autocomplete',
      '#size' => 180,
      '#title' => $this->t('User'),
      '#description' => $this->t('Select the user(s) you want mass subscribe. Use a comma-separated list (,) to select multiple users. (For example: User1, User2, User3'),
      '#target_type' => 'user',
      '#tags' => TRUE,
      '#validate_reference' => TRUE,
      '#autocreate' => FALSE,
      '#required' => TRUE,
      '#selection_settings' => [
        'include_anonymous' => FALSE,
      ],
    ];
    // @TODO provide options to filter per role.
    // $element['#selection_settings']['filter'] = [
    //   'role' => [],
    // ];
    return $element;
  }

  /**
   * Get the form element list.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The current entity.
   *
   * @return array
   *   The element form.
   */
  protected function getElementList(array &$form, FormStateInterface &$form_state, ContentEntityInterface $entity) {
    $triggering_element = $form_state->getTriggeringElement();
    if ($triggering_element['#name'] == 'reset') {
      $form_state->setValue(['subscribe', 'list', 'filters'], []);
      $user_input = $form_state->getUserInput();
      $user_input['subscribe']['list']['filters'] = [];
      $form_state->setUserInput($user_input);
    }

    $elements_limit = ['subscribe', 'list', 'filters', 'limit'];
    $limit = (int) $this->getElementPropertyValue($elements_limit, $form_state, $this->limit);
    // @TODO no offset form currently on the form element.
    $elements_offset = ['subscribe', 'list', 'filters', 'offset'];
    $offset = $this->getElementPropertyValue($elements_offset, $form_state, 0);
    $is_filtered = FALSE;

    // @TODO filter user given roles configured on the futur steeings form.
    $total_query = $this->storageUser->getQuery()
      ->condition('status', 1);

    $query = $this->storageUser->getQuery();
    $query->condition('status', 1);
    $query->sort('name', 'ASC');
    if ($limit) {
      if ($limit !== $this->limit) {
        $is_filtered = TRUE;
      }
      if ($limit !== $this->unlimited) {
        $query->range($offset, $limit);
      }
    }

    $elements_search = ['subscribe', 'list', 'filters', 'search'];
    $search = $this->getElementPropertyValue($elements_search, $form_state, '');
    if (!empty($search)) {
      $is_filtered = TRUE;
      $search = Database::getConnection()->escapeLike($search);
      $condition_or = $query->orConditionGroup();
      $condition_or->condition('name', '%' . ($search) . '%', 'like');
      $condition_or->condition('mail', '%' . ($search) . '%', 'like');
      $query->condition($condition_or);
      $total_query->condition($condition_or);
    }

    $elements_role = ['subscribe', 'list', 'filters', 'role'];
    $role = $this->getElementPropertyValue($elements_role, $form_state, '');
    if (!empty($role)) {
      $is_filtered = TRUE;
      $role = Database::getConnection()->escapeLike($role);
      $condition_or = $query->orConditionGroup();
      $condition_or->condition('roles', $role, 'IN');
      $query->condition($condition_or);
      $total_query->condition($condition_or);
    }

    $clone_query = clone $query;
    $count = $clone_query->count()->execute();
    $total = $total_query->count()->execute();

    $results = $query->execute();

    /** @var \Drupal\user\UserInterface[] $users */
    $users = $this->storageUser->loadMultiple($results);

    $element['filters'] = [
      '#type' => 'details',
      '#title' => $this->t('Filter'),
      '#open' => $is_filtered,
      '#attributes' => [
        'class' => [
          'inline-children',
        ],
      ],
    ];
    $element['filters']['search'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Search'),
      '#size' => 20,
      '#default_value' => $this->getElementPropertyValue($elements_search, $form_state, ''),
      '#attributes' => [
        'class' => [
          'filter-element',
        ],
      ],
    ];

    $element['filters']['role'] = [
      '#type' => 'select',
      '#title' => $this->t('Role'),
      '#options' => ['' => $this->t('All')] + $this->getRoles(TRUE),
      '#default_value' => $this->getElementPropertyValue($elements_search, $form_state, ''),
      '#attributes' => [
        'class' => [
          'filter-element',
        ],
      ],
    ];

    $element['filters']['limit'] = [
      '#type' => 'select',
      '#title' => $this->t('Limit'),
      '#options' => $this->getRanges(),
      '#default_value' => $this->getElementPropertyValue($elements_limit, $form_state, '50'),
      '#attributes' => [
        'class' => [
          'filter-element',
        ],
      ],
    ];

    $element['filters']['actions'] = [
      '#type' => 'actions',
    ];
    $element['filters']['actions']['filter'] = [
      '#type' => 'button',
      '#value' => $this->t('Filter'),
      '#name' => 'filter',
      '#ajax' => [
        'callback' => [$this, 'ajaxFilterCallback'],
        'wrapper' => 'subscribe-wrapper',
        'method' => 'replace',
      ],
    ];
    $element['filters']['actions']['reset'] = [
      '#type' => 'button',
      '#value' => $this->t('Reset'),
      '#name' => 'reset',
      '#ajax' => [
        'callback' => [$this, 'ajaxFilterCallback'],
        'wrapper' => 'subscribe-wrapper',
        'method' => 'replace',
      ],
    ];

    $element['total'] = [
      '#type' => 'markup',
      '#markup' => $this->stringTranslation->formatPlural($count, '1 user displayed (for a total of @total users).', '@count users displayed (for a total of @total users).', ['@count' => $count, '@total' => $total])->render(),
    ];

    $element['table'] = [
      '#type' => 'tableselect',
      '#title' => $this->t('Users'),
      '#header' => $this->getHeaders(),
      '#options' => $this->getOptions($users, $entity),
      '#default_value' => [],
      '#empty' => $this->t('No users found'),
    ];

    $element['table']['#attached']['library'][] = 'core/drupal.tableselect';
    $element['table']['#attached']['library'][] = 'entity_activity_mass_subscribe/mass_subscribe';
    return $element;
  }

  /**
   * Get the table select headers.
   *
   * @return array
   *   An array of headers for the table select form.
   */
  protected function getHeaders() {
    $headers = [
      'name' => $this->t('Username'),
      'role' => $this->t('Role'),
      'subscribed' => $this->t('Subscribed'),
    ];
    return $headers;
  }

  /**
   * Get the table select options.
   *
   * @param \Drupal\user\UserInterface[] $users
   *   An array of users object.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The current entity against check if a subscription exists.
   *
   * @return array
   *   An array of options for the table select form.
   */
  protected function getOptions(array $users, ContentEntityInterface $entity) {
    $options = [];
    /** @var \Drupal\user\UserInterface $user */
    foreach ($users as $id => $user) {
      $options[$id]['name'] = $user->getDisplayName();
      $options[$id]['role'] = implode(', ', $this->getUserRoles($user));
      $options[$id]['subscribed'] = !empty($this->storageSubscription->loadMultipleByEntityAndOwner($entity, $user, $entity->language()->getId())) ? $this->t('Yes') : '';
    }
    return $options;
  }

  /**
   * Get the user roles.
   *
   * @param \Drupal\user\UserInterface $user
   *   The given user entity.
   *
   * @return array
   *   An array of roles of the user.
   */
  protected function getUserRoles(UserInterface $user) {
    $roles = user_role_names(TRUE);
    unset($roles[RoleInterface::AUTHENTICATED_ID]);
    $users_roles = [];
    foreach ($user->getRoles() as $role) {
      if (isset($roles[$role])) {
        $users_roles[] = $roles[$role];
      }
    }
    asort($users_roles);
    return $users_roles;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->cleanValues()->getValues();
    /** @var \Drupal\Core\Session\AccountInterface $account */
    $account = $values['account'];
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $values['entity'];
    // The mass subscribe global permissions can be removed to a user. So we
    // check again if the current user has yet one the the permission.
    if (!$this->hasGlobalPermission($account, $entity)) {
      $form_state->setError($form['method'], $this->t('Access denied'));
    }

    $unsubscribe = !empty($values['unsubscribe']);
    if ($unsubscribe && !$account->hasPermission('mass unsubscribe users')) {
      $form_state->setError($form['unsubscribe'], $this->t('Access denied'));
    }

    $method = !empty($values['method']) ? $values['method'] : '';
    if (empty($method)) {
      $form_state->setError($form['method'], $this->t('You must select a method.'));
    }
    if ($method && empty($values['subscribe'][$method])) {
      $form_state->setError($form['subscribe'], $this->t('An error occurs with your selection. Please retry.'));
    }
    if ($method) {
      $permission = 'mass subscribe per ' . $method;
      if (!$account->hasPermission($permission)) {
        $form_state->setError($form['method'], $this->t('Access denied'));
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->cleanValues()->getValues();
    $method = !empty($values['method']) ? $values['method'] : '';
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $values['entity'];
    /** @var \Drupal\Core\Session\AccountInterface $account */
    $account = $values['account'];
    $unsubscribe = !empty($values['unsubscribe']);
    // A last check about the unsubscribe feature. Certainly a bit too much.
    if (!$account->hasPermission('mass unsubscribe users')) {
      $unsubscribe = FALSE;
    }

    $subscribe = !empty($values['subscribe'][$method]) ? $values['subscribe'][$method] : [];
    if (empty($subscribe)) {
      $this->messenger()->addStatus($this->t('No results found to launch a mass subscribe process.'));
      return;
    }

    $user_ids = [];
    switch ($method) {
      case 'role':
        $roles = array_filter($subscribe);
        $query = $this->storageUser->getQuery()
          ->condition('status', 1)
          ->condition('roles', $roles, 'IN');
        $user_ids = $query->execute();
        break;

      case 'user':
        foreach ($subscribe as $value) {
          if (empty($value['target_id'])) {
            continue;
          }
          $id = (string) $value['target_id'];
          $user_ids[$id] = $id;
        }
        break;

      case 'list':
        if (!empty($subscribe['table'])) {
          $user_ids = array_filter($subscribe['table']);
        }
        break;
    }

    if (!empty($user_ids)) {
      $batch = [
        'title' => $this->t('Subscribing users on @type @label', [
          '@type' => $this->getEntityTypeLabel($entity),
          '@label' => $entity->label(),
        ]),
        'operations' => [
          [
            '\Drupal\entity_activity_mass_subscribe\MassSubscribeBatch::subscribe',
            [$user_ids, $entity, $account, $unsubscribe],
          ],
        ],
        'finished' => '\Drupal\entity_activity_mass_subscribe\MassSubscribeBatch::finished',
      ];
      batch_set($batch);
    }
    else {
      $this->messenger()->addWarning($this->t('No results found to launch a mass subscribe process.'));
      return;
    }

  }

  /**
   * The _title_callback for the mass subscribe form.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The current entity.
   *
   * @return string
   *   The page title.
   */
  public function title(EntityInterface $entity = NULL) {
    if (!$entity instanceof ContentEntityInterface) {
      $entity = $this->getCurrentEntity();
    }
    if (!$entity instanceof ContentEntityInterface) {
      return $this->t('Mass subscribe unavailable');
    }
    return $this->t('Mass subscribe users on @label', ['@label' => $entity->label()]);
  }

  /**
   * Get the entity type label given an entity.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity object.
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup|string
   *   The entity type label.
   */
  protected function getEntityTypeLabel(ContentEntityInterface $entity) {
    return $entity->getEntityType()->getLabel();
  }

  /**
   * Get the roles.
   *
   * @param bool $exclude_authenticated
   *   Exclude the authenticated role.
   *
   * @return array
   *   An array of roles available on the site.
   */
  protected function getRoles($exclude_authenticated = FALSE) {
    $roles = [];
    /** @var \Drupal\user\RoleInterface $role */
    foreach (Role::loadMultiple() as $role) {
      if ($role->id() === RoleInterface::ANONYMOUS_ID) {
        continue;
      }
      if ($exclude_authenticated && $role->id() === RoleInterface::AUTHENTICATED_ID) {
        continue;
      }
      $roles[$role->id()] = $role->label();
    }
    return $roles;
  }

  /**
   * Get available ranges.
   *
   * @return array
   *   An array of range value.
   */
  protected function getRanges() {
    $ranges = [];
    $ranges['50'] = $this->t('50 results');
    $ranges['100'] = $this->t('100 results');
    $ranges['150'] = $this->t('150 results');
    $ranges['200'] = $this->t('200 results');
    $ranges['300'] = $this->t('300 results');
    $ranges['400'] = $this->t('400 results');
    $ranges['500'] = $this->t('500 results');
    $ranges['-1'] = $this->t('All results');
    return $ranges;
  }

  /**
   * Get methods available given a user.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The current user performing the action.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The related entity to the mass subscribe action..
   *
   * @return array
   *   An array of methods.
   */
  protected function getMethods(AccountInterface $account, ContentEntityInterface $entity) {
    $methods = [
      '' => $this->t('Select a method'),
    ];
    if (!$this->hasGlobalPermission($account, $entity)) {
      return $methods;
    }
    if ($account->hasPermission('mass subscribe per role')) {
      $methods['role'] = $this->t('Per role');
    }
    if ($account->hasPermission('mass subscribe per user')) {
      $methods['user'] = $this->t('Per user');
    }
    if ($account->hasPermission('mass subscribe per list')) {
      $methods['list'] = $this->t('Per list');
    }
    return $methods;
  }

  /**
   * Check global permissions.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account object.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity object.
   *
   * @return bool
   *   TRUE if account has the permission on the entity. Otherwise FALSE;
   */
  protected function hasGlobalPermission(AccountInterface $account, ContentEntityInterface $entity) {
    $has_permission = FALSE;
    if ($account->hasPermission('mass subscribe users')) {
      $has_permission = TRUE;
    }
    elseif ($account->hasPermission('mass subscribe users on editable entities')) {
      if ($entity->access('update', $account)) {
        $has_permission = TRUE;
      }
    }
    return $has_permission;
  }

  /**
   * Get the current entity from the route.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface|null
   *   The current entity found in the route or NULL.
   */
  protected function getCurrentEntity() {
    $entity = NULL;
    $keys = $this->routeMatch->getParameters()->keys();
    $entity_type_id = isset($keys[0]) ? $keys[0] : '';
    if (empty($entity_type_id)) {
      return $entity;
    }
    $entity = $this->routeMatch->getParameter($entity_type_id);
    return $entity;
  }

}

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

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