You are here

AjaxCommentsController.php in AJAX Comments 8

File

src/Controller/AjaxCommentsController.php
View source
<?php

namespace Drupal\ajax_comments\Controller;

use Drupal\ajax_comments\TempStore;
use Drupal\ajax_comments\Utility;
use Drupal\comment\CommentInterface;
use Drupal\comment\Controller\CommentController;
use Drupal\Core\Ajax\AfterCommand;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\BeforeCommand;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\RouterInterface;

/**
 * Controller routines for AJAX comments routes.
 */
class AjaxCommentsController extends ControllerBase {

  /**
   * Class prefix to apply to each comment.
   *
   * @var string
   *   A prefix used to build class name applied to each comment.
   */
  public static $commentClassPrefix = 'js-ajax-comments-id-';

  /**
   * Service to turn render arrays into HTML strings.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The Router service.
   *
   * A router class for Drupal.
   *
   * @var \Symfony\Component\Routing\RouterInterface
   */
  protected $router;

  /**
   * The TempStore service.
   *
   * This service stores temporary data to be used across HTTP requests.
   *
   * @var \Drupal\ajax_comments\TempStore
   */
  protected $tempStore;

  /**
   * The messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * Constructs a AjaxCommentsController object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The render service.
   * @param \Symfony\Component\Routing\RouterInterface $router
   *   The Router service.
   * @param \Drupal\ajax_comments\TempStore $temp_store
   *   The TempStore service.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The Messenger service.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, RendererInterface $renderer, RouterInterface $router, TempStore $temp_store, MessengerInterface $messenger) {
    $this->entityTypeManager = $entity_type_manager;
    $this->currentUser = $current_user;
    $this->renderer = $renderer;
    $this->router = $router;
    $this->tempStore = $temp_store;
    $this->messenger = $messenger;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('entity_type.manager'), $container
      ->get('current_user'), $container
      ->get('renderer'), $container
      ->get('router.no_access_checks'), $container
      ->get('ajax_comments.temp_store'), $container
      ->get('messenger'));
  }

  /**
   * Get the prefix for a selector class for an individual comment.
   *
   * @return string
   *   The portion of a CSS class name that prepends the comment ID.
   */
  public static function getCommentSelectorPrefix() {
    return '.' . static::$commentClassPrefix;
  }

  /**
   * Build a comment field render array for the ajax response.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity that has the comment field.
   * @param string $field_name
   *   The machine name of the comment field.
   *
   * @return array
   *   A render array for the updated comment field.
   */
  protected function renderCommentField(EntityInterface $entity, $field_name) {
    $comment_field = $entity
      ->get($field_name);

    // Load the display settings to ensure that the field formatter
    // configuration is properly applied to the rendered field when it is
    // returned in the ajax response.
    $display_options = $this->entityTypeManager
      ->getStorage('entity_view_display')
      ->load($entity
      ->getEntityTypeId() . '.' . $entity
      ->bundle() . '.default')
      ->getComponent($field_name);
    $comment_display = $comment_field
      ->view($display_options);

    // To avoid infinite nesting of #theme_wrappers elements on subsequent
    // ajax responses, unset them here.
    unset($comment_display['#theme_wrappers']);

    // Remove unneeded route parameters.
    unset($comment_display[0]['comments']['pager']['#route_parameters']['entity_type']);
    unset($comment_display[0]['comments']['pager']['#route_parameters']['entity']);
    unset($comment_display[0]['comments']['pager']['#route_parameters']['field_name']);
    unset($comment_display[0]['comments']['pager']['#route_parameters']['pid']);
    $entity_type = $entity
      ->getEntityType();

    // For replies, the passed $entity is the parent comment.
    // However, for the pager we want the parent entity.
    if ($entity_type
      ->id() === 'comment') {
      $entity = $entity
        ->getCommentedEntity();
      $entity_type = $entity
        ->getEntityType();
    }
    $handler = $this
      ->entityTypeManager()
      ->getRouteProviders($entity_type
      ->id())['html'];
    $route_collection = $handler
      ->getRoutes($entity_type);
    $name = 'entity.' . $entity_type
      ->get('id') . '.canonical';
    $route = $route_collection
      ->get($name);

    // Override the ajax route object with the actual entity route.
    $entity_url = $entity
      ->toURL();
    if ($route) {
      $comment_display[0]['comments']['pager']['#route_name'] = $route;
      $comment_display[0]['comments']['pager']['#route_parameters'] = $entity_url
        ->getRouteParameters();
    }
    return $comment_display;
  }

  /**
   * Create an ajax response to replace the comment field.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\Core\Ajax\AjaxResponse $response
   *   The response object being built.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity that has the comment field.
   * @param string $field_name
   *   The machine name of the comment field.
   * @param int|null $pid
   *   The entity id of the parent comment, if applicable, NULL otherwise.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The modified ajax response.
   */
  protected function buildCommentFieldResponse(Request $request, AjaxResponse $response, EntityInterface $entity, $field_name, $pid = NULL) {

    // Build a comment field render array for the ajax response.
    $comment_display = $this
      ->renderCommentField($entity, $field_name);

    // Get the wrapper HTML id selector.
    $selectors = $this->tempStore
      ->getSelectors($request);
    $wrapper_html_id = $selectors['wrapper_html_id'];

    // Rendering the comment form below (as part of comment_display) triggers
    // form processing.
    $response
      ->addCommand(new ReplaceCommand($wrapper_html_id, $comment_display));

    // Store the new wrapper_html_id, in case it is needed for other commands.
    $this->tempStore
      ->setSelector('wrapper_html_id', $comment_display['#attributes']['id']);
    return $response;
  }

  /**
   * Add messages to the ajax response.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\Core\Ajax\AjaxResponse $response
   *   The response object being built.
   * @param string $selector
   *   The DOM selector used to insert status messages.
   * @param string $position
   *   Indicates whether to use PrependCommand, BeforeCommand, AppendCommand,
   *   or AfterCommand.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The modified ajax response.
   */
  protected function addMessages(Request $request, AjaxResponse $response, $selector = '', $position = 'prepend') {
    $settings = \Drupal::config('ajax_comments.settings');
    $notify = $settings
      ->get('notify');
    if ($notify || !empty($this->messenger
      ->messagesByType(MessengerInterface::TYPE_ERROR))) {
      if (empty($selector)) {

        // Use the first id found in the ajax replacement markup to be
        // inserted into the page as the selector, if none was provided.
        foreach ($response
          ->getCommands() as $command) {
          if ($command['command'] === 'insert' && $command['method'] === 'replaceWith') {
            $markup = $command['data']
              ->__toString();
            if (preg_match('/\\sid="(.*)"/', $markup, $matches)) {
              $selector = '#' . $matches[1];
              break;
            }
          }
        }
      }

      // Add any status messages.
      $status_messages = [
        '#type' => 'status_messages',
      ];
      switch ($position) {
        case 'replace':
          $command = new ReplaceCommand($selector, $this->renderer
            ->renderRoot($status_messages));
          break;
        case 'after':
          $command = new AfterCommand($selector, $this->renderer
            ->renderRoot($status_messages));
          break;
        case 'before':
          $command = new BeforeCommand($selector, $this->renderer
            ->renderRoot($status_messages));
          break;
        case 'append':
          $command = new AppendCommand($selector, $this->renderer
            ->renderRoot($status_messages));
          break;
        case 'prepend':
        default:
          $command = new PrependCommand($selector, $this->renderer
            ->renderRoot($status_messages));
      }
      $response
        ->addCommand($command);
    }
    else {

      // Render messages to avoid display them when reloading the page.
      $status_messages = [
        '#type' => 'status_messages',
      ];
      $this->renderer
        ->renderRoot($status_messages);
    }
    return $response;
  }

  /**
   * Returns the comment edit form.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\comment\CommentInterface $comment
   *   The comment entity.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse|\Symfony\Component\HttpFoundation\RedirectResponse
   *   The Ajax response, or a redirect response if not using ajax.
   */
  public function edit(Request $request, CommentInterface $comment) {
    $is_ajax = Utility::isAjaxRequest($request);
    if ($is_ajax) {
      $response = new AjaxResponse();

      // Get the selectors.
      $selectors = $this->tempStore
        ->getSelectors($request, $overwrite = TRUE);
      $wrapper_html_id = $selectors['wrapper_html_id'];

      // Hide anchor.
      $response
        ->addCommand(new InvokeCommand('a#comment-' . $comment
        ->id(), 'hide'));

      // Hide comment.
      $response
        ->addCommand(new InvokeCommand(static::getCommentSelectorPrefix() . $comment
        ->id(), 'hide'));

      // Remove any existing status messages in the comment field,
      // if applicable.
      $response
        ->addCommand(new RemoveCommand($wrapper_html_id . ' .js-ajax-comments-messages'));

      // Insert the comment form.
      $form = $this
        ->entityFormBuilder()
        ->getForm($comment);
      $response
        ->addCommand(new AfterCommand(static::getCommentSelectorPrefix() . $comment
        ->id(), $form));

      // TODO: Get this custom ajax command working later.
      // if (\Drupal::config('ajax_comments.settings')->get('enable_scroll')) {
      //   $response->addCommand(new ajaxCommentsScrollToElementCommand('.ajax-comments-reply-form-' . $comment->getCommentedEntityId() . '-' . $comment->get('pid')->target_id . '-' . $comment->id()));
      // }
      // Don't delete the tempStore variables here; we need them
      // to persist for the save() method below, where the form returned
      // here will be submitted.
      // Instead, return the response without calling $this->tempStore->deleteAll().
      return $response;
    }
    else {

      // If the user attempts to access the edit link directly (e.g., at
      // /ajax_comments/1/edit), redirect to the core comment edit form.
      $redirect = Url::fromRoute('entity.comment.edit_form', [
        'comment' => $comment
          ->id(),
      ])
        ->setAbsolute()
        ->toString();
      $response = new RedirectResponse($redirect);
      return $response;
    }
  }

  /**
   * Submit handler for the comment reply and edit forms.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\comment\CommentInterface $comment
   *   The comment entity.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The Ajax response.
   */
  public function save(Request $request, CommentInterface $comment) {
    $response = new AjaxResponse();

    // Store the selectors from the incoming request, if applicable.
    // If the selectors are not in the request, the stored ones will
    // not be overwritten.
    $this->tempStore
      ->getSelectors($request, $overwrite = TRUE);

    // Rebuild the form to trigger form submission.
    $form = $this
      ->entityFormBuilder()
      ->getForm($comment, 'default', [
      'editing' => TRUE,
    ]);

    // Check for errors.
    if (empty($this->messenger
      ->messagesByType('error'))) {
      $errors = FALSE;

      // If there are no errors, set the ajax-updated
      // selector value for the form.
      $this->tempStore
        ->setSelector('form_html_id', $form['#attributes']['id']);

      // Build the updated comment field and insert into a replaceWith
      // response. Also prepend any status messages in the response.
      $response = $this
        ->buildCommentFieldResponse($request, $response, $comment
        ->getCommentedEntity(), $comment
        ->get('field_name')->value);
    }
    else {
      $errors = TRUE;

      // Retrieve the selector values for use in building the response.
      $selectors = $this->tempStore
        ->getSelectors($request, $overwrite = TRUE);
      $wrapper_html_id = $selectors['wrapper_html_id'];
      $form_html_id = $selectors['form_html_id'];

      // If there are errors, remove old messages and reload the form.
      $response
        ->addCommand(new RemoveCommand($wrapper_html_id . ' .js-ajax-comments-messages'));
      $response
        ->addCommand(new ReplaceCommand($form_html_id, $form));
    }

    // If this is a new comment being added from this method, it is being
    // inserted as a reply to another comment, and there is no $comment->id()
    // yet. Insert the message after the parent comment instead.
    if ($comment
      ->isNew()) {

      // Retrieve the comment id of the new comment, which was saved in
      // AjaxCommentsForm::save() during the previous HTTP request.
      $cid = $this->tempStore
        ->getCid();

      // Try to insert the message above the new comment.
      if (!empty($cid) && !$errors && \Drupal::currentUser()
        ->hasPermission('skip comment approval')) {
        $selector = static::getCommentSelectorPrefix() . $cid;
        $response = $this
          ->addMessages($request, $response, $selector, 'before');
      }
      else {
        $response = $this
          ->addMessages($request, $response, static::getCommentSelectorPrefix() . $comment
          ->get('pid')->target_id, 'after');
      }
    }
    else {
      $response = $this
        ->addMessages($request, $response, static::getCommentSelectorPrefix() . $comment
        ->id(), 'before');
    }

    // Clear out the tempStore variables.
    $this->tempStore
      ->deleteAll();

    // Remove the libraries from the response, otherwise when
    // core/misc/drupal.js is reinserted into the DOM, the following line of
    // code will execute, causing Drupal.attachBehaviors() to run on the entire
    // document, and reattach behaviors to DOM elements that already have them:
    // @code
    // // Attach all behaviors.
    // domready(function () { Drupal.attachBehaviors(document, drupalSettings); });
    // @endcode
    $attachments = $response
      ->getAttachments();

    // Need to have only 'core/drupalSettings' in the asset library list.
    // If neither 'core/drupalSettings', nor a library with a dependency on it,
    // is in the list of libraries, drupalSettings will be stripped out of the
    // ajax response by \Drupal\Core\Asset\AssetResolver::getJsAssets().
    $attachments['library'] = [
      'core/drupalSettings',
    ];

    // We need to keep the drupalSettings in the response, otherwise the
    // #ajax properties in the form definition won't be properly attached to
    // the rebuilt comment field returned in the ajax response, and subsequent
    // ajax interactions will be broken.
    $response
      ->setAttachments($attachments);
    return $response;
  }

  /**
   * Cancel handler for the comment edit form.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param int $cid
   *   The id of the comment being edited, or 0 if this is a new comment.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The Ajax response.
   */
  public function cancel(Request $request, $cid) {
    $response = new AjaxResponse();

    // Get the selectors.
    $selectors = $this->tempStore
      ->getSelectors($request, $overwrite = TRUE);
    $wrapper_html_id = $selectors['wrapper_html_id'];
    $form_html_id = $selectors['form_html_id'];
    if ($cid != 0) {

      // Show the hidden anchor.
      $response
        ->addCommand(new InvokeCommand('a#comment-' . $cid, 'show', [
        200,
        'linear',
      ]));

      // Show the hidden comment.
      $response
        ->addCommand(new InvokeCommand(static::getCommentSelectorPrefix() . $cid, 'show', [
        200,
        'linear',
      ]));
    }

    // Remove the form.
    $response
      ->addCommand(new RemoveCommand($form_html_id));

    // Remove any messages, if applicable.
    $response
      ->addCommand(new RemoveCommand($wrapper_html_id . ' .js-ajax-comments-messages'));

    // Clear out the tempStore variables.
    $this->tempStore
      ->deleteAll();
    return $response;
  }

  /**
   * Builds ajax response for deleting a comment.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\comment\CommentInterface $comment
   *   The comment entity.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The Ajax response.
   */
  public function delete(Request $request, CommentInterface $comment) {
    $response = new AjaxResponse();

    // Store the selectors from the incoming request, if applicable.
    // If the selectors are not in the request, the stored ones will
    // not be overwritten.
    $this->tempStore
      ->getSelectors($request, $overwrite = TRUE);
    $response
      ->addCommand(new CloseModalDialogCommand());

    // Rebuild the form to trigger form submission.
    $this
      ->entityFormBuilder()
      ->getForm($comment, 'delete');

    // Build the updated comment field and insert into a replaceWith response.
    // Also prepend any status messages in the response.
    $response = $this
      ->buildCommentFieldResponse($request, $response, $comment
      ->getCommentedEntity(), $comment
      ->get('field_name')->value);

    // Calling $this->buildCommentFieldResponse() updates the stored selectors.
    $selectors = $this->tempStore
      ->getSelectors($request);
    $wrapper_html_id = $selectors['wrapper_html_id'];
    $response = $this
      ->addMessages($request, $response, $wrapper_html_id);

    // Clear out the tempStore variables.
    $this->tempStore
      ->deleteAll();
    return $response;
  }

  /**
   * Builds ajax response for adding a new comment without a parent comment.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity this comment belongs to.
   * @param string $field_name
   *   The field_name to which the comment belongs.
   * @param int $pid
   *   (optional) Some comments are replies to other comments. In those cases,
   *   $pid is the parent comment's comment ID. Defaults to NULL.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The Ajax response.
   *
   * @see \Drupal\comment\Controller\CommentController::getReplyForm()
   */
  public function add(Request $request, EntityInterface $entity, $field_name, $pid = NULL) {
    $response = new AjaxResponse();

    // Store the selectors from the incoming request, if applicable.
    // If the selectors are not in the request, the stored ones will
    // not be overwritten.
    $this->tempStore
      ->getSelectors($request, $overwrite = TRUE);

    // Check the user's access to reply.
    // The user should not have made it this far without proper permission,
    // but adding this access check as a fallback.
    $this
      ->replyAccess($request, $response, $entity, $field_name, $pid);

    // If $this->replyAccess() added any commands to the AjaxResponse,
    // it means that access was denied, so we should NOT submit the form
    // and rebuild the comment field. Just return the response with the
    // error message.
    if (!empty($response
      ->getCommands())) {
      return $response;
    }

    // Build the comment entity form.
    // This approach is very similar to the one taken in
    // \Drupal\comment\CommentLazyBuilders::renderForm().
    $comment = $this
      ->entityTypeManager()
      ->getStorage('comment')
      ->create([
      'entity_id' => $entity
        ->id(),
      'pid' => $pid,
      'entity_type' => $entity
        ->getEntityTypeId(),
      'field_name' => $field_name,
    ]);

    // Rebuild the form to trigger form submission.
    $form = $this
      ->entityFormBuilder()
      ->getForm($comment);

    // Check for errors.
    if (empty($this->messenger
      ->messagesByType('error'))) {

      // If there are no errors, set the ajax-updated
      // selector value for the form.
      $this->tempStore
        ->setSelector('form_html_id', $form['#attributes']['id']);

      // Build the updated comment field and insert into a replaceWith
      // response.
      $response = $this
        ->buildCommentFieldResponse($request, $response, $entity, $field_name);
    }
    else {

      // Retrieve the selector values for use in building the response.
      $selectors = $this->tempStore
        ->getSelectors($request, $overwrite = TRUE);
      $wrapper_html_id = $selectors['wrapper_html_id'];

      // If there are errors, remove old messages.
      $response
        ->addCommand(new RemoveCommand($wrapper_html_id . ' .js-ajax-comments-messages'));
    }

    // The form_html_id should have been updated by the form constructor when
    // $this->buildCommentFieldResponse() was called, so retrieve the updated
    // selector values for use in building the response.
    $selectors = $this->tempStore
      ->getSelectors($request);
    $form_html_id = $selectors['form_html_id'];

    // Prepend any status messages in the response.
    $response = $this
      ->addMessages($request, $response, $form_html_id, 'before');

    // Clear out the tempStore variables.
    $this->tempStore
      ->deleteAll();

    // Remove the libraries from the response, otherwise when
    // core/misc/drupal.js is reinserted into the DOM, the following line of
    // code will execute, causing Drupal.attachBehaviors() to run on the entire
    // document, and reattach behaviors to DOM elements that already have them:
    // @code
    // // Attach all behaviors.
    // domready(function () { Drupal.attachBehaviors(document, drupalSettings); });
    // @endcode
    $attachments = $response
      ->getAttachments();

    // Need to have only 'core/drupalSettings' in the asset library list.
    // If neither 'core/drupalSettings', nor a library with a dependency on it,
    // is in the list of libraries, drupalSettings will be stripped out of the
    // ajax response by \Drupal\Core\Asset\AssetResolver::getJsAssets().
    $attachments['library'] = [
      'core/drupalSettings',
    ];

    // We need to keep the drupalSettings in the response, otherwise the
    // #ajax properties in the form definition won't be properly attached to
    // the rebuilt comment field returned in the ajax response, and subsequent
    // ajax interactions will be broken.
    $response
      ->setAttachments($attachments);
    return $response;
  }

  /**
   * Builds ajax response to display a form to reply to another comment.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity this comment belongs to.
   * @param string $field_name
   *   The field_name to which the comment belongs.
   * @param int $pid
   *   The parent comment's comment ID.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse|\Symfony\Component\HttpFoundation\RedirectResponse
   *   The Ajax response, or a redirect response if not using ajax.
   *
   * @see \Drupal\comment\Controller\CommentController::getReplyForm()
   */
  public function reply(Request $request, EntityInterface $entity, $field_name, $pid) {
    $is_ajax = Utility::isAjaxRequest($request);
    if ($is_ajax) {
      $response = new AjaxResponse();

      // Get the selectors.
      $selectors = $this->tempStore
        ->getSelectors($request, $overwrite = TRUE);
      $wrapper_html_id = $selectors['wrapper_html_id'];

      // Check the user's access to reply.
      // The user should not have made it this far without proper permission,
      // but adding this access check as a fallback.
      $this
        ->replyAccess($request, $response, $entity, $field_name, $pid);

      // If $this->replyAccess() added any commands to the AjaxResponse,
      // it means that access was denied, so we should NOT ajax load the
      // reply form. Instead, return the response with the error messages
      // immediately.
      if (!empty($response
        ->getCommands())) {
        return $response;
      }

      // Remove any existing status messages and ajax reply forms in the
      // comment field, if applicable.
      $response
        ->addCommand(new RemoveCommand($wrapper_html_id . ' .js-ajax-comments-messages'));
      $response
        ->addCommand(new RemoveCommand($wrapper_html_id . ' .ajax-comments-form-reply'));

      // Build the comment entity form.
      // This approach is very similar to the one taken in
      // \Drupal\comment\CommentLazyBuilders::renderForm().
      $comment = $this
        ->entityTypeManager()
        ->getStorage('comment')
        ->create([
        'entity_id' => $entity
          ->id(),
        'pid' => $pid,
        'entity_type' => $entity
          ->getEntityTypeId(),
        'field_name' => $field_name,
      ]);

      // Build the comment form.
      $form = $this
        ->entityFormBuilder()
        ->getForm($comment);
      $response
        ->addCommand(new AfterCommand(static::getCommentSelectorPrefix() . $pid, $form));

      // Don't delete the tempStore variables here; we need them
      // to persist for the saveReply() method, where the form returned
      // here will be submitted.
      // Instead, return the response without calling $this->tempStore->deleteAll().
      return $response;
    }
    else {

      // If the user attempts to access the comment reply form with JavaScript
      // disabled, degrade gracefully by redirecting to the core comment
      // reply form.
      $redirect = Url::fromRoute('comment.reply', [
        'entity_type' => $entity
          ->getEntityTypeId(),
        'entity' => $entity
          ->id(),
        'field_name' => $field_name,
        'pid' => $pid,
      ])
        ->setAbsolute()
        ->toString();
      $response = new RedirectResponse($redirect);
      return $response;
    }
  }

  /**
   * Builds ajax response to save a submitted reply to another comment.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity this comment belongs to.
   * @param string $field_name
   *   The field_name to which the comment belongs.
   * @param int $pid
   *   The parent comment's comment ID.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The Ajax response.
   */
  public function saveReply(Request $request, EntityInterface $entity, $field_name, $pid) {
    $response = new AjaxResponse();

    // Check the user's access to reply.
    // The user should not have made it this far without proper permission,
    // but adding this access check as a fallback.
    $this
      ->replyAccess($request, $response, $entity, $field_name, $pid);

    // If $this->replyAccess() added any commands to the AjaxResponse,
    // it means that access was denied, so we should NOT submit the form
    // and rebuild the comment field. Instead, return the response
    // immediately and abort the save.
    if (!empty($response
      ->getCommands())) {
      return $response;
    }

    // Build a dummy comment entity to pass to $this->save(), which will use
    // it to rebuild the comment entity form to trigger form submission.
    // @code
    // $form = $this->entityFormBuilder()->getForm($comment, 'default', ['editing' => TRUE]);
    // @endcode
    // Note that this approach will correctly process the form submission
    // even though we are passing in an empty, dummy comment, because two steps
    // later in the call stack, \Drupal\Core\Form\FormBuilder::buildForm() is
    // called, and it checks the current request object for form submission
    // values if there aren't any in the form state, yet:
    // @code
    // $input = $form_state->getUserInput();
    // if (!isset($input)) {
    //   $input = $form_state->isMethodType('get') ? $request->query->all() : $request->request->all();
    //   $form_state->setUserInput($input);
    // }
    // @endcode
    // This approach is very similar to the one taken in
    // \Drupal\comment\CommentLazyBuilders::renderForm().
    $comment = $this
      ->entityTypeManager()
      ->getStorage('comment')
      ->create([
      'entity_id' => $entity
        ->id(),
      'pid' => $pid,
      'entity_type' => $entity
        ->getEntityTypeId(),
      'field_name' => $field_name,
    ]);

    // Rebuild the form to trigger form submission.
    return $this
      ->save($request, $comment);
  }

  /**
   * Check the user's permission to post a comment.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   * @param \Drupal\Core\Ajax\AjaxResponse $response
   *   The response object being built.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity this comment belongs to.
   * @param string $field_name
   *   The field_name to which the comment belongs.
   * @param int $pid
   *   (optional) Some comments are replies to other comments. In those cases,
   *   $pid is the parent comment's comment ID. Defaults to NULL.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse $response
   *   The ajax response, if access is denied.
   */
  public function replyAccess(Request $request, AjaxResponse $response, EntityInterface $entity, $field_name, $pid = NULL) {

    // Get the selectors.
    $selectors = $this->tempStore
      ->getSelectors($request);
    $wrapper_html_id = $selectors['wrapper_html_id'];
    $form_html_id = $selectors['form_html_id'];
    $access = CommentController::create(\Drupal::getContainer())
      ->replyFormAccess($entity, $field_name, $pid);

    // If access is not explicitly allowed, then we forbid it.
    if (!$access
      ->isAllowed()) {
      $selector = $form_html_id;
      if (empty($selector)) {
        $selector = $wrapper_html_id;
      }
      $this->messenger
        ->addError(t('You do not have permission to post a comment.'));

      // If this is a new top-level comment (not a reply to another comment so
      // no $pid), replace the comment form with the error message.
      if (empty($pid)) {

        // Remove any existing status messages in the comment field,
        // if applicable.
        $response
          ->addCommand(new RemoveCommand($wrapper_html_id . ' .js-ajax-comments-messages'));

        // Add the error message.
        $response = $this
          ->addMessages($request, $response, $selector, 'replace');
      }
      else {
        $response = $this
          ->buildCommentFieldResponse($request, $response, $entity, $field_name, $pid);

        // The wrapper_html_id should have been updated when
        // $this->buildCommentFieldResponse() was called, so retrieve
        // the updated selector values for use in building the response.
        $selectors = $this->tempStore
          ->getSelectors($request);
        $selector = $selectors['wrapper_html_id'];
        $response = $this
          ->addMessages($request, $response, $selector, 'prepend');
      }

      // Clear out the tempStore variables.
      $this->tempStore
        ->deleteAll();
      return $response;
    }
  }

}

Classes

Namesort descending Description
AjaxCommentsController Controller routines for AJAX comments routes.