From 6fca4655e37bdf12680917d063fce5fb96c69908 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Thu, 7 Mar 2024 10:26:20 +0000
Subject: [PATCH] Issue #3408738 by omkar.podey, srishtiiee, kunal.sachdev,
 lauriii, Utkarsh_33, arisen, smustgrave, Wim Leers, catch, benjifisher:
 Create a new OpenModalDialogWithUrl command

---
 .../Core/Ajax/OpenModalDialogWithUrl.php      | 57 +++++++++++++++++++
 core/misc/dialog/dialog.ajax.js               | 23 ++++++++
 .../ajax_test/src/Form/AjaxTestDialogForm.php | 30 +++++++++-
 .../Ajax/DialogTest.php                       | 17 ++++++
 4 files changed, 124 insertions(+), 3 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Ajax/OpenModalDialogWithUrl.php

diff --git a/core/lib/Drupal/Core/Ajax/OpenModalDialogWithUrl.php b/core/lib/Drupal/Core/Ajax/OpenModalDialogWithUrl.php
new file mode 100644
index 000000000000..bd91415f810f
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/OpenModalDialogWithUrl.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Ajax;
+
+use Drupal\Component\Utility\UrlHelper;
+
+/**
+ * Provides an AJAX command for opening a modal with URL.
+ *
+ * OpenDialogCommand is a similar class which opens modals but works
+ * differently as it needs all data to be passed through dialogOptions while
+ * OpenModalDialogWithUrl fetches the data from routing info of the URL.
+ *
+ * @see \Drupal\Core\Ajax\OpenDialogCommand
+ */
+class OpenModalDialogWithUrl implements CommandInterface {
+
+  /**
+   * Constructs a OpenModalDialogWithUrl object.
+   *
+   * @param string $url
+   *   Only Internal URLs or URLs with the same domain and base path are
+   *   allowed.
+   * @param array $settings
+   *   The dialog settings.
+   */
+  public function __construct(
+    protected string $url,
+    protected array $settings,
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    // @see \Drupal\Core\Routing\LocalAwareRedirectResponseTrait::isLocal()
+    if (!UrlHelper::isExternal($this->url) || UrlHelper::externalIsLocal($this->url, $this->getBaseURL())) {
+      return [
+        'command' => 'openModalDialogWithUrl',
+        'url' => $this->url,
+        'dialogOptions' => $this->settings,
+      ];
+    }
+    throw new \LogicException('External URLs are not allowed.');
+  }
+
+  /**
+   * Gets the complete base URL.
+   */
+  private function getBaseUrl() {
+    $requestContext = \Drupal::service('router.request_context');
+    return $requestContext->getCompleteBaseUrl();
+  }
+
+}
diff --git a/core/misc/dialog/dialog.ajax.js b/core/misc/dialog/dialog.ajax.js
index d1fb5b7e94b2..f8a1b5f90cf8 100644
--- a/core/misc/dialog/dialog.ajax.js
+++ b/core/misc/dialog/dialog.ajax.js
@@ -291,4 +291,27 @@
   $(window).on('dialog:beforeclose', (e, dialog, $element) => {
     $element.off('.dialog');
   });
+
+  /**
+   * Ajax command to open URL in a modal dialog.
+   *
+   * @param {Drupal.Ajax} [ajax]
+   *   An Ajax object.
+   * @param {object} response
+   *   The Ajax response.
+   */
+  Drupal.AjaxCommands.prototype.openModalDialogWithUrl = function (
+    ajax,
+    response,
+  ) {
+    const dialogOptions = response.dialogOptions || {};
+    const elementSettings = {
+      progress: { type: 'throbber' },
+      dialogType: 'modal',
+      dialog: dialogOptions,
+      url: response.url,
+      httpMethod: 'GET',
+    };
+    Drupal.ajax(elementSettings).execute();
+  };
 })(jQuery, Drupal, window.tabbable);
diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php
index 6fa939c21703..fd31dc6ed946 100644
--- a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php
+++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestDialogForm.php
@@ -5,9 +5,11 @@
 use Drupal\ajax_test\Controller\AjaxTestController;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\OpenModalDialogCommand;
 use Drupal\Core\Ajax\OpenDialogCommand;
+use Drupal\Core\Ajax\OpenModalDialogCommand;
+use Drupal\Core\Ajax\OpenModalDialogWithUrl;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
 
 /**
  * Dummy form for testing DialogRenderer with _form routes.
@@ -43,6 +45,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         'callback' => '::nonModal',
       ],
     ];
+    $form['button3'] = [
+      '#type' => 'submit',
+      '#name' => 'button3',
+      '#value' => 'Button 3 (modal from url)',
+      '#ajax' => [
+        'callback' => '::modalFromUrl',
+      ],
+    ];
 
     return $form;
   }
@@ -68,6 +78,13 @@ public function modal(&$form, FormStateInterface $form_state) {
     return $this->dialog(TRUE);
   }
 
+  /**
+   * AJAX callback handler for Url modal, AjaxTestDialogForm.
+   */
+  public function modalFromUrl(&$form, FormStateInterface $form_state) {
+    return $this->dialog(TRUE, TRUE);
+  }
+
   /**
    * AJAX callback handler for AjaxTestDialogForm.
    */
@@ -80,11 +97,13 @@ public function nonModal(&$form, FormStateInterface $form_state) {
    *
    * @param bool $is_modal
    *   (optional) TRUE if modal, FALSE if plain dialog. Defaults to FALSE.
+   * @param bool $is_url
+   *   (optional) True if modal is from a URL, Defaults to FALSE.
    *
    * @return \Drupal\Core\Ajax\AjaxResponse
    *   An ajax response object.
    */
-  protected function dialog($is_modal = FALSE) {
+  protected function dialog(bool $is_modal = FALSE, bool $is_url = FALSE) {
     $content = AjaxTestController::dialogContents();
     $response = new AjaxResponse();
     $title = $this->t('AJAX Dialog & contents');
@@ -94,7 +113,12 @@ protected function dialog($is_modal = FALSE) {
     $content['#attached']['library'][] = 'core/drupal.dialog.ajax';
 
     if ($is_modal) {
-      $response->addCommand(new OpenModalDialogCommand($title, $content));
+      if ($is_url) {
+        $response->addCommand(new OpenModalDialogWithUrl(Url::fromRoute('ajax_test.dialog_form')->toString(), []));
+      }
+      else {
+        $response->addCommand(new OpenModalDialogCommand($title, $content));
+      }
     }
     else {
       $selector = '#ajax-test-dialog-wrapper-1';
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
index 3d9f82bfb007..7438e1af1ca1 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\FunctionalJavascriptTests\Ajax;
 
 use Drupal\ajax_test\Controller\AjaxTestController;
+use Drupal\Core\Ajax\OpenModalDialogWithUrl;
 use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 
 /**
@@ -129,6 +130,22 @@ public function testDialog() {
     // Use a link to close the panel opened by button 2.
     $this->getSession()->getPage()->clickLink('Link 4 (close non-modal if open)');
 
+    // Test dialogs opened using OpenModalDialogWithUrl.
+    $this->getSession()->getPage()->findButton('Button 3 (modal from url)')->press();
+    // Check that title was fetched properly.
+    // @see \Drupal\ajax_test\Form\AjaxTestDialogForm::dialog.
+    $form_dialog_title = $this->assertSession()->waitForElement('css', "span.ui-dialog-title:contains('Ajax Form contents')");
+    $this->assertNotNull($form_dialog_title, 'Dialog form has the expected title.');
+    $button1_dialog->findButton('Close')->press();
+    // Test external URL.
+    $dialog_obj = new OpenModalDialogWithUrl('http://example.com', []);
+    try {
+      $dialog_obj->render();
+    }
+    catch (\LogicException $e) {
+      $this->assertEquals('External URLs are not allowed.', $e->getMessage());
+    }
+
     // Form modal.
     $this->clickLink('Link 5 (form)');
     // Two links have been clicked in succession - This time wait for a change
-- 
GitLab