Setup UI - Allow callers to wrap UI in their own page chrome. Clarify response format.
authorTim Otten <totten@civicrm.org>
Wed, 8 Jul 2020 00:10:17 +0000 (17:10 -0700)
committerTim Otten <totten@civicrm.org>
Wed, 8 Jul 2020 00:50:05 +0000 (17:50 -0700)
The general idiom for calling SetupController is like this:

```php
$ctrl = \Civi\Setup::instance()->createController()->getCtrl();
$ctrl->setUrls(...);
\Civi\Setup\BasicRunner::run($ctrl);
```

This changes the data exchanged between `BasicRunner` and `SetupController`
to allow more flexible runners. The `BasicRunner` is the only one currently
used by existing installers, so it's drop-in compatible.

Before
------

The `BasicRunner` calls `$ctrl->run(...)` and gets back an array with the response data
(`[0 => $headers, 1 => $body]`).

After
-----

The `BasicRunner` calls `$ctrl->run(...)` and gets back a `SetupResponse`. This
object provides the `[0]` and `[1]` keys (for backward compat), but it's better
to access the object properties (`$headers`, `$body`, etc).

It provides several additional properties - eg `$title`, `$assets`, `$code`.

Technical Details
------------------

This is a drop-in update for anything that uses `BasicRunner`.

Strictly speaking, if one wrote a different runner, it may or may not be
drop-in compatible.  But since I don't think there are any others, it's not
really worth spending much energy on verifying.

The bump in `Setup::PROTOCOL` (`1.0` => `1.1`) indicates that old installers
should generally be forward compatible with this protocol.  However, if one
writes a new installer and specifically uses the `SetupResponse` type,
then they should assert protocol `1.1`.

setup/src/Setup.php
setup/src/Setup/BasicRunner.php
setup/src/Setup/UI/SetupController.php
setup/src/Setup/UI/SetupResponse.php [new file with mode: 0644]

index fb225ddb259376c7d5525316bc126849744dd558..04dd426b2b3fb4eb033306c9655fe9021d4ab585 100644 (file)
@@ -16,7 +16,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
 
 class Setup {
 
-  const PROTOCOL = '1.0';
+  const PROTOCOL = '1.1';
 
   const PRIORITY_START = 2000;
   const PRIORITY_PREPARE = 1000;
index f0aaaefb16c1f8b8b2aeaca365bd041f5840e1f8..783f50dee9b2c257ad490afd9a484f57e8fb7978 100644 (file)
@@ -11,16 +11,45 @@ class BasicRunner {
    * it may be easier to work directly with `getCtrl()->run(...)` which
    * handles inputs/outputs in a more abstract fashion.
    *
-   * @param object $ctrl
+   * @param \Civi\Setup\UI\SetupController $ctrl
    *    A web controller.
    */
   public static function run($ctrl) {
     $method = $_SERVER['REQUEST_METHOD'];
-    list ($headers, $body) = $ctrl->run($method, ($method === 'GET' ? $_GET : $_POST));
-    foreach ($headers as $k => $v) {
+
+    /** @var \Civi\Setup\UI\SetupResponse $response */
+    $response = $ctrl->run($method, ($method === 'GET' ? $_GET : $_POST));
+
+    self::send($ctrl, $response);
+  }
+
+  /**
+   * @param \Civi\Setup\UI\SetupController $ctrl
+   * @param \Civi\Setup\UI\SetupResponse $response
+   */
+  public static function send($ctrl, $response) {
+    http_response_code($response->code);
+    foreach ($response->headers as $k => $v) {
       header("$k: $v");
     }
-    echo $body;
+
+    /** @var \Civi\Setup\Model $model */
+    $model = \Civi\Setup::instance()->getModel();
+
+    if ($response->isComplete) {
+      echo $response->body;
+    }
+    else {
+      $pageVars = [
+        'pageAssets' => $response->assets,
+        'pageTitle' => $response->title,
+        'pageBody' => $response->body,
+        'shortLangCode' => \CRM_Core_I18n_PseudoConstant::shortForLong($model->lang),
+        'textDirection' => (\CRM_Core_I18n::isLanguageRTL($model->lang) ? 'rtl' : 'ltr'),
+      ];
+
+      echo $ctrl->render($ctrl->getResourcePath('page.tpl.php'), $pageVars);
+    }
   }
 
 }
index 2431e54e80c51dc61b4369260c15b436ddbca9ee..74fd0d434157fa3ecabbb860ec82dd706db27a98 100644 (file)
@@ -48,9 +48,7 @@ class SetupController implements SetupControllerInterface {
    *   Ex: 'GET' or 'POST'.
    * @param array $fields
    *   List of any HTTP GET/POST fields.
-   * @return array
-   *   The HTTP headers and response text.
-   *   [0 => array $headers, 1 => string $body].
+   * @return SetupResponse
    */
   public function run($method, $fields = array()) {
     $this->setup->getDispatcher()->dispatch('civi.setupui.run', new UIBootEvent($this, $method, $fields));
@@ -74,9 +72,7 @@ class SetupController implements SetupControllerInterface {
    *   Ex: 'GET' or 'POST'.
    * @param array $fields
    *   List of any HTTP GET/POST fields.
-   * @return array
-   *   The HTTP headers and response text.
-   *   [0 => array $headers, 1 => string $body].
+   * @return SetupResponse
    */
   public function runStart($method, $fields) {
     $checkInstalled = $this->setup->checkInstalled();
@@ -96,9 +92,7 @@ class SetupController implements SetupControllerInterface {
     ];
 
     // $body = "<pre>" . htmlentities(print_r(['method' => $method, 'urls' => $this->urls, 'data' => $fields], 1)) . "</pre>";
-    $body = $this->renderPage(ts('CiviCRM Installer'), $this->render($tplFile, $tplVars));
-
-    return array(array(), $body);
+    return $this->createPage(ts('CiviCRM Installer'), $this->render($tplFile, $tplVars));
   }
 
   /**
@@ -108,9 +102,7 @@ class SetupController implements SetupControllerInterface {
    *   Ex: 'GET' or 'POST'.
    * @param array $fields
    *   List of any HTTP GET/POST fields.
-   * @return array
-   *   The HTTP headers and response text.
-   *   [0 => array $headers, 1 => string $body].
+   * @return SetupResponse
    */
   public function runInstall($method, $fields) {
     $checkInstalled = $this->setup->checkInstalled();
@@ -173,40 +165,42 @@ class SetupController implements SetupControllerInterface {
     $this->setup->getDispatcher()->dispatch('civi.setupui.boot', new UIBootEvent($this, $method, $fields));
   }
 
+  /**
+   * @param string $message
+   * @param string $title
+   * @return SetupResponse
+   */
   public function createError($message, $title = 'Error') {
-    return [
-      [],
-      $this->renderPage($title, sprintf('<h1>%s</h1>\n%s', htmlentities($title), htmlentities($message))),
-    ];
+    return $this->createPage($title, sprintf('<h1>%s</h1>\n%s', htmlentities($title), htmlentities($message)));
   }
 
   /**
    * @param string $title
    * @param string $body
-   * @return string
+   * @return SetupResponse
    */
-  public function renderPage($title, $body) {
+  public function createPage($title, $body) {
     /** @var \Civi\Setup\Model $model */
     $model = $this->setup->getModel();
 
-    $pageAssets = [
+    $r = new SetupResponse();
+    $r->code = 200;
+    $r->headers = [];
+    $r->isComplete = FALSE;
+    $r->title = $title;
+    $r->body = $body;
+    $r->assets = [
       ['type' => 'script-url', 'url' => $this->getUrl('jquery.js')],
       ['type' => 'script-code', 'code' => 'window.csj$ = jQuery.noConflict();'],
       ['type' => 'style-url', 'url' => $this->urls['res'] . "template.css"],
       ['type' => 'style-url', 'url' => $this->getUrl('font-awesome.css')],
     ];
+
     if (\CRM_Core_I18n::isLanguageRTL($model->lang)) {
-      $pageAssets[] = ['type' => 'style-url', 'url' => $this->urls['res'] . "template-rtl.css"];
+      $r->assets[] = ['type' => 'style-url', 'url' => $this->urls['res'] . "template-rtl.css"];
     }
 
-    $pageVars = [
-      'pageAssets' => $pageAssets,
-      'pageTitle' => $title,
-      'pageBody' => $body,
-      'shortLangCode' => \CRM_Core_I18n_PseudoConstant::shortForLong($model->lang),
-      'textDirection' => (\CRM_Core_I18n::isLanguageRTL($model->lang) ? 'rtl' : 'ltr'),
-    ];
-    return $this->render($this->getResourcePath('page.tpl.php'), $pageVars);
+    return $r;
   }
 
   /**
@@ -321,13 +315,13 @@ class SetupController implements SetupControllerInterface {
   }
 
   /**
-   * @return array
+   * @return SetupResponse
    */
   private function renderFinished() {
     $m = $this->setup->getModel();
     $tplFile = $this->getResourcePath('finished.' . $m->cms . '.php');
     if (file_exists($tplFile)) {
-      return [[], $this->renderPage(ts('CiviCRM Installed'), $this->render($tplFile))];
+      return $this->createPage(ts('CiviCRM Installed'), $this->render($tplFile));
     }
     else {
       return $this->createError("Installation succeeded. However, the final page ($tplFile) was not available.");
diff --git a/setup/src/Setup/UI/SetupResponse.php b/setup/src/Setup/UI/SetupResponse.php
new file mode 100644 (file)
index 0000000..43c4245
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+namespace Civi\Setup\UI;
+
+/**
+ * This represents a response from the Setup UI.
+ *
+ * Previously, responses where an array of the form:
+ *   [0 => array $headers, 1 => string $body].
+ *
+ * This implements \ArrayAccess for backward compatibility.
+ */
+class SetupResponse implements \ArrayAccess {
+
+  /**
+   * @var bool
+   *
+   * TRUE if the body represents a fully formed HTML page.
+   * FALSE if the body is a fragment of an HTML page.
+   */
+  public $isComplete = TRUE;
+
+  /**
+   * @var array
+   *   Ex: ['Content-Type': 'text/html']
+   */
+  public $headers = [];
+
+  /**
+   * @var array
+   *   Ex: $assets[0] = ['type' => 'script-url', 'url' => 'http://foobar'];
+   */
+  public $assets = [];
+
+  /**
+   * @var string
+   *   Ex: '<h1>Hello world</h1>'
+   */
+  public $body = '';
+
+  /**
+   * @var string|null
+   *   The title of the response page (if it's an HTML response).
+   */
+  public $title = NULL;
+
+  /**
+   * @var int
+   */
+  public $code = 200;
+
+  /**
+   * @var array
+   *   Array(int $oldPos => string $newName).
+   */
+  protected $oldFieldMap;
+
+  /**
+   * SetupResponse constructor.
+   */
+  public function __construct() {
+    $this->oldFieldMap = [
+      0 => 'headers',
+      1 => 'body',
+    ];
+  }
+
+  public function offsetExists($offset) {
+    return isset($this->oldFieldMap[$offset]);
+  }
+
+  public function &offsetGet($offset) {
+    if (isset($this->oldFieldMap[$offset])) {
+      $field = $this->oldFieldMap[$offset];
+      return $this->{$field};
+    }
+    else {
+      return NULL;
+    }
+  }
+
+  public function offsetSet($offset, $value) {
+    if (isset($this->oldFieldMap[$offset])) {
+      $field = $this->oldFieldMap[$offset];
+      $this->{$field} = $value;
+    }
+  }
+
+  public function offsetUnset($offset) {
+    if (isset($this->oldFieldMap[$offset])) {
+      $field = $this->oldFieldMap[$offset];
+      unset($this->{$field});
+    }
+  }
+
+}