From: Tim Otten Date: Wed, 8 Jul 2020 00:10:17 +0000 (-0700) Subject: Setup UI - Allow callers to wrap UI in their own page chrome. Clarify response format. X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=30bef769ba3d313c23c2fd6c907b032e53a72826;p=civicrm-core.git Setup UI - Allow callers to wrap UI in their own page chrome. Clarify response format. 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`. --- diff --git a/setup/src/Setup.php b/setup/src/Setup.php index fb225ddb25..04dd426b2b 100644 --- a/setup/src/Setup.php +++ b/setup/src/Setup.php @@ -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; diff --git a/setup/src/Setup/BasicRunner.php b/setup/src/Setup/BasicRunner.php index f0aaaefb16..783f50dee9 100644 --- a/setup/src/Setup/BasicRunner.php +++ b/setup/src/Setup/BasicRunner.php @@ -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); + } } } diff --git a/setup/src/Setup/UI/SetupController.php b/setup/src/Setup/UI/SetupController.php index 2431e54e80..74fd0d4341 100644 --- a/setup/src/Setup/UI/SetupController.php +++ b/setup/src/Setup/UI/SetupController.php @@ -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 = "
" . htmlentities(print_r(['method' => $method, 'urls' => $this->urls, 'data' => $fields], 1)) . "
"; - $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('

%s

\n%s', htmlentities($title), htmlentities($message))), - ]; + return $this->createPage($title, sprintf('

%s

\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 index 0000000000..43c42451f2 --- /dev/null +++ b/setup/src/Setup/UI/SetupResponse.php @@ -0,0 +1,95 @@ + 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: '

Hello world

' + */ + 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}); + } + } + +}