From b3268e824ee33adad69416581333d07c44ed5d87 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 7 Jul 2023 16:25:24 -0700 Subject: [PATCH 01/23] Import Standalone.civi-setup.php --- setup/plugins/init/Standalone.civi-setup.php | 80 ++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 setup/plugins/init/Standalone.civi-setup.php diff --git a/setup/plugins/init/Standalone.civi-setup.php b/setup/plugins/init/Standalone.civi-setup.php new file mode 100644 index 000000000000..4c22d61b9944 --- /dev/null +++ b/setup/plugins/init/Standalone.civi-setup.php @@ -0,0 +1,80 @@ +addListener('civi.setup.checkAuthorized', function (\Civi\Setup\Event\CheckAuthorizedEvent $e) { + $model = $e->getModel(); + if ($model->cms !== 'Standalone') { + return; + } + + \Civi\Setup::log()->info(sprintf('[%s] Handle %s', basename(__FILE__), 'checkAuthorized')); + $e->setAuthorized(TRUE); + }); + + +\Civi\Setup::dispatcher() + ->addListener('civi.setup.init', function (\Civi\Setup\Event\InitEvent $e) { + $model = $e->getModel(); + if ($model->cms !== 'Standalone') { + return; + } + \Civi\Setup::log()->info(sprintf('[%s] Handle %s', basename(__FILE__), 'init')); + + // error_log('artfulrobot: ' . __FILE__ . ' listener for civi.setup.init'); + // Compute settingsPath. + // We use this structure: /var/www/standalone/data/{civicrm.settings.php,templates_c} + // to reduce the number of directories that admins have to chmod + + /** + * @var string $projectRootPath + * refers to the root of the *application*, not the actual webroot as reachable by http. + * Typically, this means that $projectRootPath might be like /var/www/example.org/ and + * the actual web root would be /var/www/example.org/web/ + */ + $projectRootPath = dirname($model->srcPath, 3); + $model->settingsPath = implode(DIRECTORY_SEPARATOR, [$projectRootPath, 'data', 'civicrm.settings.php']); + $model->templateCompilePath = implode(DIRECTORY_SEPARATOR, [$projectRootPath, 'data', 'templates_c']); + // print "\n-------------------------\nSet model values:\n" . json_encode($model->getValues(), JSON_PRETTY_PRINT) . "\n-----------------------------\n"; + + // Compute DSN. + // print "=======================\n". json_encode(['model' => $model->getValues(), 'server' => $_SERVER], JSON_PRETTY_PRINT) ."\n"; + $model->db = $model->cmsDb = [ + 'server' => 'localhost', + 'username' => '', + 'password' => '', + 'database' => '', + ]; + + // Compute URLs (@todo?) + // original: $model->cmsBaseUrl = $_SERVER['HTTP_ORIGIN'] ?: $_SERVER['HTTP_REFERER']; + if (empty($model->cmsBaseUrl)) { + // A buildkit install (which uses cv core:install) sets this correctly. But a standard composer-then-website type install does not. + $model->cmsBaseUrl = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']; + } + + // These mandatorySettings become 'Additional settings from installer' (via 'extraSettings') and set values in + // $civicrm_setting['domain'][k] = v; + $model->mandatorySettings['userFrameworkResourceURL'] = $model->cmsBaseUrl . '/assets/civicrm/core'; + + // These paths get set as + // $civicrm_paths[k]['url'|'path'] = v + $model->paths['cms.root'] = [ + 'path' => $projectRootPath . DIRECTORY_SEPARATOR . 'web', + ]; + $model->paths['civicrm.files'] = [ + 'path' => rtrim($projectRootPath . DIRECTORY_SEPARATOR . 'web') . DIRECTORY_SEPARATOR . 'upload', + 'url' => $model->cmsBaseUrl . '/upload', + ]; + + // Compute default locale. + $model->lang = $_REQUEST['lang'] ?? 'en_US'; + }); From 42096e62ef2550d1a662d921345941536f030c48 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 7 Jul 2023 16:37:33 -0700 Subject: [PATCH 02/23] Import civicrm.config.php.standalone.txt, htaccess.txt, and index.php.txt --- .../civicrm.config.php.standalone.txt | 9 ++ tools/standalone/templates/htaccess.txt | 117 ++++++++++++++++++ tools/standalone/templates/index.php.txt | 69 +++++++++++ 3 files changed, 195 insertions(+) create mode 100644 tools/standalone/templates/civicrm.config.php.standalone.txt create mode 100644 tools/standalone/templates/htaccess.txt create mode 100644 tools/standalone/templates/index.php.txt diff --git a/tools/standalone/templates/civicrm.config.php.standalone.txt b/tools/standalone/templates/civicrm.config.php.standalone.txt new file mode 100644 index 000000000000..565ff6a52ac2 --- /dev/null +++ b/tools/standalone/templates/civicrm.config.php.standalone.txt @@ -0,0 +1,9 @@ + + + Require all denied + + + Order allow,deny + + + +# Don't show directory listings for URLs which map to a directory. +Options -Indexes + +# Follow symbolic links in this directory. +Options +FollowSymLinks + +# Make CiviCRM handle any 404 errors. +ErrorDocument 404 /index.php + +# Set the default handler. +DirectoryIndex index.php index.html index.htm + +# Requires mod_expires to be enabled. + + # Enable expirations. + ExpiresActive On + + # Cache all files for 2 weeks after access (A). + ExpiresDefault A1209600 + + + # Do not allow PHP scripts to be cached unless they explicitly send cache + # headers themselves. Otherwise all scripts would have to overwrite the + # headers set by mod_expires if they want another caching behavior. This may + # fail if an error occurs early in the bootstrap process, and it may cause + # problems if a non-CiviCRM PHP file is installed in a subdirectory. + ExpiresActive Off + + + +# Various rewrite rules. + + RewriteEngine on + + # Set "protossl" to "s" if we were accessed via https://. This is used later + # if you enable "www." stripping or enforcement, in order to ensure that + # you don't bounce between http and https. + RewriteRule ^ - [E=protossl] + RewriteCond %{HTTPS} on + RewriteRule ^ - [E=protossl:s] + + # Make sure Authorization HTTP header is available to PHP + # even when running as CGI or FastCGI. + RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Block access to "hidden" directories whose names begin with a period. This + # includes directories used by version control systems such as Subversion or + # Git to store control files. Files whose names begin with a period, as well + # as the control files used by CVS, are protected by the FilesMatch directive + # above. + # + # NOTE: This only works when mod_rewrite is loaded. Without mod_rewrite, it is + # not possible to block access to entire directories from .htaccess, because + # is not allowed here. + # + # If you do not have mod_rewrite installed, you should remove these + # directories from your webroot or otherwise protect them from being + # downloaded. + RewriteRule "/\.|^\.(?!well-known/)" - [F] + + # If your site can be accessed both with and without the 'www.' prefix, you + # can use one of the following settings to redirect users to your preferred + # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option: + # + # To redirect all users to access the site WITH the 'www.' prefix, + # (http://example.com/... will be redirected to http://www.example.com/...) + # uncomment the following: + # RewriteCond %{HTTP_HOST} . + # RewriteCond %{HTTP_HOST} !^www\. [NC] + # RewriteRule ^ http%{ENV:protossl}://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + # + # To redirect all users to access the site WITHOUT the 'www.' prefix, + # (http://www.example.com/... will be redirected to http://example.com/...) + # uncomment the following: + # RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + # RewriteRule ^ http%{ENV:protossl}://%1%{REQUEST_URI} [L,R=301] + + # Modify the RewriteBase if you are using CiviCRM in a subdirectory or in a + # VirtualDocumentRoot and the rewrite rules are not working properly. + # For example if your site is at http://example.com/crm uncomment and + # modify the following line: + # RewriteBase /crm + # + # If your site is running in a VirtualDocumentRoot at http://example.com/, + # uncomment the following line: + # RewriteBase / + + # Pass all requests not referring directly to files in the filesystem to + # index.php. + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} !=/favicon.ico + RewriteRule ^ index.php [L] + + +# Various header fixes. + + # Disable content sniffing, since it's an attack vector. + Header always set X-Content-Type-Options nosniff + # Disable Proxy header, since it's an attack vector. + RequestHeader unset Proxy + diff --git a/tools/standalone/templates/index.php.txt b/tools/standalone/templates/index.php.txt new file mode 100644 index 000000000000..525d85277580 --- /dev/null +++ b/tools/standalone/templates/index.php.txt @@ -0,0 +1,69 @@ +initialize(); + // Add CSS, JS, etc. that is required for this page. + \CRM_Core_Resources::singleton()->addCoreResources(); + + $parts = explode('?', $_SERVER['REQUEST_URI']); + $args = explode('/', $parts[0]); + // Remove empty values + $args = array_values(array_filter($args)); + // Set this for compatibility + $_GET['q'] = implode('/', $args); + // And finally render the page + print CRM_Core_Invoke::invoke($args); + } + else { + // @todo Is it necessary to support this? + // Apache has not been tested yet, but presumably not required. + $config = CRM_Core_Config::singleton(); + $urlVar = $config->userFrameworkURLVar; + print CRM_Core_Invoke::invoke(explode('/', $_GET[$urlVar])); + } +} + +if (file_exists('../data/civicrm.settings.php')) { + require_once '../vendor/autoload.php'; + require_once '../data/civicrm.settings.php'; + invoke(); +} +else { + $coreUrl = '/assets/civicrm/core'; + // This is the path to the root of the composer project (as opposed to the web/doc root path, which would be this + /web/) + $projectRootPath = dirname(__DIR__); + $civiCorePath = implode(DIRECTORY_SEPARATOR, [$projectRootPath, 'vendor', 'civicrm', 'civicrm-core']); + $classLoader = implode(DIRECTORY_SEPARATOR, [$civiCorePath, 'CRM', 'Core', 'ClassLoader.php']); + + if (file_exists($classLoader)) { + require_once $classLoader; + CRM_Core_ClassLoader::singleton()->register(); + \Civi\Setup::assertProtocolCompatibility(1.0); + + \Civi\Setup::init([ + // This is just enough information to get going. + 'cms' => 'Standalone', + 'srcPath' => $civiCorePath, + ]); + $ctrl = \Civi\Setup::instance()->createController()->getCtrl(); + + $ctrl->setUrls([ + // The URL of this setup controller. May be used for POST-backs + 'ctrl' => '/civicrm', // @todo this had url('civicrm') ? + // The base URL for loading resource files (images/javascripts) for this project. Includes trailing slash. + 'res' => $coreUrl . '/setup/res/', + 'jquery.js' => $coreUrl . '/bower_components/jquery/dist/jquery.min.js', + 'font-awesome.css' => $coreUrl . '/bower_components/font-awesome/css/font-awesome.min.css', + ]); + \Civi\Setup\BasicRunner::run($ctrl); + exit(); + } +} From 7535bb42b62351f1e093fada3c165e4fad6134e1 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 7 Jul 2023 17:18:51 -0700 Subject: [PATCH 03/23] Ignore ./srv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d5466f017624..08f64303562e 100644 --- a/.gitignore +++ b/.gitignore @@ -75,4 +75,5 @@ civicrm.settings.php sql/dummy_processor.mysql distmaker/distmaker.conf distmaker/out +/srv /tmp From 358583a32fe5b1ef565b30c9fb363eba2d5c7ba2 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 7 Jul 2023 17:18:22 -0700 Subject: [PATCH 04/23] Add 'tools/standalone/bin/scaffold' --- tools/standalone/bin/scaffold | 44 ++++++++++++++++++++++ tools/standalone/src/scaffold.php | 62 +++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100755 tools/standalone/bin/scaffold create mode 100644 tools/standalone/src/scaffold.php diff --git a/tools/standalone/bin/scaffold b/tools/standalone/bin/scaffold new file mode 100755 index 000000000000..51be8c7e08c0 --- /dev/null +++ b/tools/standalone/bin/scaffold @@ -0,0 +1,44 @@ +#!/usr/bin/env php + [--copy|--symlink|--auto] \n", basename(__FILE__)); + exit(1); +} +else { + standalone_scaffold($destDir, $mode); +} diff --git a/tools/standalone/src/scaffold.php b/tools/standalone/src/scaffold.php new file mode 100644 index 000000000000..d5896ebb6d03 --- /dev/null +++ b/tools/standalone/src/scaffold.php @@ -0,0 +1,62 @@ + 'civicrm.config.php.standalone', + 'templates/index.php.txt' => 'web/index.php', + 'templates/htaccess.txt' => 'web/.htaccess', + ]; + foreach ($files as $srcFile => $destFile) { + switch ($mode) { + case 'copy': + copy("$srcDir/$srcFile", "$destDir/$destFile"); + break; + + case 'symlink': + symlink("$srcDir/$srcFile", "$destDir/$destFile"); + break; + } + } +} From 4be88dd5e38df71d23a4936351692deb67ab940f Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 7 Jul 2023 17:36:23 -0700 Subject: [PATCH 05/23] scaffold - Move key resources to a publishable folder The `tools/` folder is not in public releases. We want to make sure it's available for HAN-C deployments. --- .../res}/civicrm.config.php.standalone.txt | 0 .../templates => setup/res}/htaccess.txt | 0 .../templates => setup/res}/index.php.txt | 0 setup/src/Setup/StandaloneScaffold.php | 76 +++++++++++++++++++ tools/standalone/bin/scaffold | 4 +- tools/standalone/src/scaffold.php | 62 --------------- 6 files changed, 78 insertions(+), 64 deletions(-) rename {tools/standalone/templates => setup/res}/civicrm.config.php.standalone.txt (100%) rename {tools/standalone/templates => setup/res}/htaccess.txt (100%) rename {tools/standalone/templates => setup/res}/index.php.txt (100%) create mode 100644 setup/src/Setup/StandaloneScaffold.php delete mode 100644 tools/standalone/src/scaffold.php diff --git a/tools/standalone/templates/civicrm.config.php.standalone.txt b/setup/res/civicrm.config.php.standalone.txt similarity index 100% rename from tools/standalone/templates/civicrm.config.php.standalone.txt rename to setup/res/civicrm.config.php.standalone.txt diff --git a/tools/standalone/templates/htaccess.txt b/setup/res/htaccess.txt similarity index 100% rename from tools/standalone/templates/htaccess.txt rename to setup/res/htaccess.txt diff --git a/tools/standalone/templates/index.php.txt b/setup/res/index.php.txt similarity index 100% rename from tools/standalone/templates/index.php.txt rename to setup/res/index.php.txt diff --git a/setup/src/Setup/StandaloneScaffold.php b/setup/src/Setup/StandaloneScaffold.php new file mode 100644 index 000000000000..2ab33078c540 --- /dev/null +++ b/setup/src/Setup/StandaloneScaffold.php @@ -0,0 +1,76 @@ + 'civicrm.config.php.standalone', + 'index.php.txt' => 'web/index.php', + 'htaccess.txt' => 'web/.htaccess', + ]; + foreach ($files as $srcFile => $destFile) { + switch ($mode) { + case 'copy': + copy("$srcDir/$srcFile", "$destDir/$destFile"); + break; + + case 'symlink': + symlink("$srcDir/$srcFile", "$destDir/$destFile"); + break; + } + } + } + +} diff --git a/tools/standalone/bin/scaffold b/tools/standalone/bin/scaffold index 51be8c7e08c0..21762111b37b 100755 --- a/tools/standalone/bin/scaffold +++ b/tools/standalone/bin/scaffold @@ -24,7 +24,7 @@ if (PHP_SAPI !== 'cli') { die("GenCode can only be run from command line."); } -require_once dirname(__DIR__) . '/src/scaffold.php'; +require_once dirname(__DIR__, 3) . '/setup/src/Setup/StandaloneScaffold.php'; $destDir = rtrim($argv[1] ?? '', DIRECTORY_SEPARATOR); $mode = 'copy'; /* TODO: Try out symlinks... */ @@ -40,5 +40,5 @@ if (empty($destDir)) { exit(1); } else { - standalone_scaffold($destDir, $mode); + \Civi\Setup\StandaloneScaffold::create($destDir, $mode); } diff --git a/tools/standalone/src/scaffold.php b/tools/standalone/src/scaffold.php deleted file mode 100644 index d5896ebb6d03..000000000000 --- a/tools/standalone/src/scaffold.php +++ /dev/null @@ -1,62 +0,0 @@ - 'civicrm.config.php.standalone', - 'templates/index.php.txt' => 'web/index.php', - 'templates/htaccess.txt' => 'web/.htaccess', - ]; - foreach ($files as $srcFile => $destFile) { - switch ($mode) { - case 'copy': - copy("$srcDir/$srcFile", "$destDir/$destFile"); - break; - - case 'symlink': - symlink("$srcDir/$srcFile", "$destDir/$destFile"); - break; - } - } -} From 2c953e6106067d75b78bf53b9a30e671602a0bc7 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 7 Jul 2023 17:38:04 -0700 Subject: [PATCH 06/23] scaffold - Fix style errors from imported resources --- setup/res/civicrm.config.php.standalone.txt | 6 +++++- setup/res/index.php.txt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/setup/res/civicrm.config.php.standalone.txt b/setup/res/civicrm.config.php.standalone.txt index 565ff6a52ac2..a08f7a18c2d1 100644 --- a/setup/res/civicrm.config.php.standalone.txt +++ b/setup/res/civicrm.config.php.standalone.txt @@ -1,9 +1,13 @@ setUrls([ // The URL of this setup controller. May be used for POST-backs - 'ctrl' => '/civicrm', // @todo this had url('civicrm') ? + 'ctrl' => '/civicrm', /* @todo this had url('civicrm') ? */ // The base URL for loading resource files (images/javascripts) for this project. Includes trailing slash. 'res' => $coreUrl . '/setup/res/', 'jquery.js' => $coreUrl . '/bower_components/jquery/dist/jquery.min.js', From e0a17db2161a820dfb4b1e01e41325e3a0045649 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 12:12:12 -0700 Subject: [PATCH 07/23] scaffold - Overwrite symlinks --- setup/src/Setup/StandaloneScaffold.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup/src/Setup/StandaloneScaffold.php b/setup/src/Setup/StandaloneScaffold.php index 2ab33078c540..ac22f11ac6bc 100644 --- a/setup/src/Setup/StandaloneScaffold.php +++ b/setup/src/Setup/StandaloneScaffold.php @@ -67,6 +67,9 @@ public static function create(string $destDir, string $mode = 'auto'): void { break; case 'symlink': + if (file_exists("$destDir/$destFile")) { + unlink("$destDir/$destFile"); + } symlink("$srcDir/$srcFile", "$destDir/$destFile"); break; } From 6353e27dd45061aa4adee0c837f1d4381d1498b2 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 14:02:44 -0700 Subject: [PATCH 08/23] (REF) scaffold - Make call-signature amenable composer-compile-plugin --- setup/src/Setup/StandaloneScaffold.php | 26 ++++++++++++++++---------- tools/standalone/bin/scaffold | 5 ++++- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/setup/src/Setup/StandaloneScaffold.php b/setup/src/Setup/StandaloneScaffold.php index ac22f11ac6bc..804996ae968f 100644 --- a/setup/src/Setup/StandaloneScaffold.php +++ b/setup/src/Setup/StandaloneScaffold.php @@ -25,18 +25,24 @@ class StandaloneScaffold { * Install basic scaffolding for standalone. This creates a handful of small, static * folders and files. * - * @param string $destDir - * Ex: '/var/www/example.com' - * Ex: '/home/myuser/src/civicrm/srv' - * @param string $mode - * How to install files. Options: - * - 'copy': Make an exact copy - * - 'symlink': Make a symbolic link - * - 'auto': Choose 'copy' or 'symlink' based on OS compat - * + * @param array $task + * - 'scaffold-dir': Where to place files + * - Ex: '/var/www/example.com' + * - Ex: '/home/myuser/src/civicrm/srv' + * - 'scaffold-mode': How to install files. Options: + * - 'copy': Make an exact copy + * - 'symlink': Make a symbolic link + * - 'auto': Choose 'copy' or 'symlink' based on OS compat * @return void */ - public static function create(string $destDir, string $mode = 'auto'): void { + public static function create(array $task): void { + $destDir = $task['scaffold-dir']; + $mode = $task['scaffold-mode'] ?? 'auto'; + + if (empty($destDir)) { + throw new \RuntimeException("Missing required parameter: scaffold-dir"); + } + $srcDir = dirname(__DIR__, 3) . '/setup/res'; if ($mode === 'auto') { diff --git a/tools/standalone/bin/scaffold b/tools/standalone/bin/scaffold index 21762111b37b..6c3442fdffbb 100755 --- a/tools/standalone/bin/scaffold +++ b/tools/standalone/bin/scaffold @@ -40,5 +40,8 @@ if (empty($destDir)) { exit(1); } else { - \Civi\Setup\StandaloneScaffold::create($destDir, $mode); + \Civi\Setup\StandaloneScaffold::create([ + 'scaffold-dir' => $destDir, + 'scaffold-mode' => $mode, + ]); } From 4186fbed802ce64a9d8ddf6426aaf805d8d2942d Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 7 Jul 2023 17:57:09 -0700 Subject: [PATCH 09/23] router.php - First draft which handles static files for `/core` --- tools/standalone/router.php | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tools/standalone/router.php diff --git a/tools/standalone/router.php b/tools/standalone/router.php new file mode 100644 index 000000000000..0094be1fd057 --- /dev/null +++ b/tools/standalone/router.php @@ -0,0 +1,41 @@ + Date: Fri, 7 Jul 2023 18:55:55 -0700 Subject: [PATCH 10/23] router.php - Use class. Add more error handling. Add 'packages'. --- tools/standalone/router.php | 99 ++++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/tools/standalone/router.php b/tools/standalone/router.php index 0094be1fd057..2d1cf25244e0 100644 --- a/tools/standalone/router.php +++ b/tools/standalone/router.php @@ -9,33 +9,84 @@ +--------------------------------------------------------------------+ */ -/** - * Send a static file. - * - * @param string $path - * @return bool - */ -function send_file(string $path): bool { - header('Content-Type: '.mime_content_type($path)); - $fh = fopen($path, 'r'); - fpassthru($fh); - fclose($fh); - return TRUE; -} +class StandaloneRouter { + + const ALLOW_FILES = ';\.(jpg|png|css|js|html|txt|json|yml|xml|md)$;'; + + public function main(): bool { + if (preg_match(';^/core/packages(/.*);', $_SERVER['PHP_SELF'], $m)) { + return $this->sendFileFromFolder($this->findPackages(), $m[1]); + } + if (preg_match(';^/core(/.*);', $_SERVER['PHP_SELF'], $m)) { + return $this->sendFileFromFolder($this->findCore(), $m[1]); + } + else { + return $this->invoke($_SERVER['REQUEST_URI']); + } + } + + /** + * Send a static file. + * + * @param string $path + * + * @return bool + */ + public function sendFile(string $path): bool { + if (file_exists($path)) { + header('Content-Type: ' . mime_content_type($path)); + $fh = fopen($path, 'r'); + fpassthru($fh); + fclose($fh); + return TRUE; + } + else { + return $this->sendError(404, "File not found"); + } + return TRUE; + } -$civicrm_root = dirname(__DIR__, 2); + public function sendFileFromFolder(string $basePath, string $relPath): bool { + $absFile = $basePath . $relPath; + if (!preg_match(static::ALLOW_FILES, $relPath)) { + return $this->sendError(403, "File type not allowed"); + } + if (strpos($_SERVER['REQUEST_URI'], '..') !== FALSE) { + $realFile = realpath($absFile); + if (strpos($realFile, $basePath) !== 0) { + return $this->sendError(403, "Malformed path"); + } + } + return $this->sendFile($absFile); + } + + public function sendError(int $code, string $message): bool { + http_response_code($code); + printf("

HTTP %s: %s

", $code, htmlentities($message)); + return TRUE; + } + + public function invoke(string $uri) { + echo "Invoke route: " . htmlentities($uri) . "
"; + return TRUE; + } + + + public function findCore(): string { + return dirname(__DIR__, 2); + } -if (preg_match(';^/core/;', $_SERVER['REQUEST_URI'])) { - $file = $civicrm_root . substr($_SERVER['REQUEST_URI'], 5); - if (strpos($_SERVER['REQUEST_URI'], '..') !== FALSE) { - $realFile = realpath($file); - if (strpos($realFile, $civicrm_root) !== 0) { - http_send_status(403); - echo "Malformed path"; - return; + public function findPackages(): string { + $core = $this->findCore(); + if (file_exists($core . '/packages')) { + return $core . '/packages'; + } + if (file_exists(dirname($core) . '/civicrm-packages')) { + return dirname($core) . '/civicrm-packages'; } + throw new \RuntimeException("Failed to find civicrm-packages"); } - return send_file($file); + } -echo $_SERVER['REQUEST_URI']; +(new StandaloneRouter())->main(); From a0dd2eb082277249b3abd38def4b4fdaa5eec74f Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 11:53:47 -0700 Subject: [PATCH 11/23] router.php - Allow direct serving from 'web/'. Track routes as list. --- tools/standalone/router.php | 153 ++++++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 40 deletions(-) diff --git a/tools/standalone/router.php b/tools/standalone/router.php index 2d1cf25244e0..b3f66901c7fe 100644 --- a/tools/standalone/router.php +++ b/tools/standalone/router.php @@ -9,55 +9,117 @@ +--------------------------------------------------------------------+ */ +if (PHP_SAPI !== 'cli-server') { + http_response_code(403); + die("Forbidden"); +} + +/** + * The StandaloneRouter allows you to run CiviCRM's "Standalone" UF with the PHP built-in server. + * It is intended for local development. + * + * Ex: php -S localhost:8000 -t srv/web/ tools/standalone/router.php + */ class StandaloneRouter { - const ALLOW_FILES = ';\.(jpg|png|css|js|html|txt|json|yml|xml|md)$;'; + private const ALLOW_VIRTUAL_FILES = ';\.(jpg|png|css|js|html|txt|json|yml|xml|md)$;'; - public function main(): bool { - if (preg_match(';^/core/packages(/.*);', $_SERVER['PHP_SELF'], $m)) { - return $this->sendFileFromFolder($this->findPackages(), $m[1]); - } - if (preg_match(';^/core(/.*);', $_SERVER['PHP_SELF'], $m)) { - return $this->sendFileFromFolder($this->findCore(), $m[1]); - } - else { - return $this->invoke($_SERVER['REQUEST_URI']); - } + private $routes = []; + + public function __construct() { + // Note: Routing rules are processed sequentially, until one handles the request. + + // If it looks like a Civi route, then call CRM_Core_Invoke. + $this->addRoute(';^/(civicrm(/.*)?)$;', fn($m) => $this->invoke($m[1])); + + // If there's a concrete file in HTTP root (`web/`), then serve that. + $this->addRoute(';/(.*);', function($m) { + $file = $this->findFile($_SERVER['DOCUMENT_ROOT'], $m[1]); + return ($file === NULL) ? FALSE : $this->sendDirect(); + }); + + // Virtually mount civicrm-{core,packages}. This allows us to serve their static assets directly (even on systems that lack symlinks). + // TODO: Decide which convention we like more... + + $this->addRoute(';^/core/packages/(.*);', fn($m) => $this->sendFileFromFolder($this->findPackages(), $m[1])); + $this->addRoute(';^/core/(.*);', fn($m) => $this->sendFileFromFolder($this->findCore(), $m[1])); + + $this->addRoute(';^/civicrm-packages/(.*);', fn($m) => $this->sendFileFromFolder($this->findPackages(), $m[1])); + $this->addRoute(';^/civicrm-core/(.*);', fn($m) => $this->sendFileFromFolder($this->findCore(), $m[1])); + + $this->addRoute(';^/assets/civicrm/core/(.*);', fn($m) => $this->sendFileFromFolder($this->findPackages(), $m[1])); + $this->addRoute(';^/assets/civicrm/packages/(.*);', fn($m) => $this->sendFileFromFolder($this->findCore(), $m[1])); + + // TODO: Consider allowing CRM_Core_Invoke to handle any route. May affect UF interop. } /** - * Send a static file. - * - * @param string $path + * Receive a request through the PHP built-in HTTP server. Decide how to process it. * + * @link https://www.php.net/manual/en/features.commandline.webserver.php * @return bool + * TRUE if the request has been handled. + * FALSE if the request has not been handled. */ - public function sendFile(string $path): bool { - if (file_exists($path)) { - header('Content-Type: ' . mime_content_type($path)); - $fh = fopen($path, 'r'); - fpassthru($fh); - fclose($fh); - return TRUE; - } - else { - return $this->sendError(404, "File not found"); + public function main(): bool { + $url = parse_url($_SERVER['REQUEST_URI']); + foreach ($this->routes as $route) { + if (preg_match($route['regex'], $url['path'], $matches)) { + $handled = call_user_func($route['handler'], $matches); + if ($handled === TRUE) { + return TRUE; + } + if ($handled === '*SEND-DIRECT*') { + return FALSE; + } + } } + return $this->sendError(404, "Not found"); + } + + /** + * Register another route. + * + * @param string $regex + * Regular expression to run against the request-path. + * Ex: ';^/foobar/(.*);' + * @param callable $handler + * Function to call when routing the match. Receives the regex-matches as input. + * Ex: fn($m) => $this->sendError(500, 'This path is foobared: ' . $m[1]); + * The handler should return TRUE (if handled), FALSE (if skipped), or selected string constants. + * @return void + */ + public function addRoute(string $regex, callable $handler): void { + $this->routes[] = [ + 'regex' => $regex, + 'handler' => $handler, + ]; + } + + public function invoke(string $path) { + echo "Invoke route: " . htmlentities($path) . "
"; return TRUE; } public function sendFileFromFolder(string $basePath, string $relPath): bool { - $absFile = $basePath . $relPath; - if (!preg_match(static::ALLOW_FILES, $relPath)) { + if (!preg_match(static::ALLOW_VIRTUAL_FILES, $relPath)) { return $this->sendError(403, "File type not allowed"); } - if (strpos($_SERVER['REQUEST_URI'], '..') !== FALSE) { - $realFile = realpath($absFile); - if (strpos($realFile, $basePath) !== 0) { - return $this->sendError(403, "Malformed path"); - } + + $absFile = $this->findFile($basePath, $relPath); + if ($absFile === NULL) { + return $this->sendError(404, "File not found"); } - return $this->sendFile($absFile); + + $stat = stat($absFile); + header('Content-Type: ' . mime_content_type($absFile)); + header('Content-Length: ' . $stat['size']); + readfile($absFile, FALSE); + return TRUE; + } + + public function sendDirect(): string { + return '*SEND-DIRECT*'; } public function sendError(int $code, string $message): bool { @@ -66,14 +128,9 @@ public function sendError(int $code, string $message): bool { return TRUE; } - public function invoke(string $uri) { - echo "Invoke route: " . htmlentities($uri) . "
"; - return TRUE; - } - - public function findCore(): string { - return dirname(__DIR__, 2); + $r = dirname(__DIR__, 2); + return $r; } public function findPackages(): string { @@ -87,6 +144,22 @@ public function findPackages(): string { throw new \RuntimeException("Failed to find civicrm-packages"); } + /** + * @param string $basePath + * @param string $relPath + * @return string|null + * If file exists, then return the combined (absolute) path. + * If file does not exist, then return NULL. + */ + private function findFile(string $basePath, string $relPath): ?string { + $realBase = realpath($basePath); + $realRel = realpath($basePath . '/' . $relPath); + if ($realBase && $realRel && strpos($realRel, $realBase) === 0) { + return $basePath . '/' . $relPath; + } + return NULL; + } + } -(new StandaloneRouter())->main(); +return (new StandaloneRouter())->main(); From 29858a7c339877d56a8ae150ead5ed5da61820d0 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 12:32:14 -0700 Subject: [PATCH 12/23] serve - Add script to fill scaffold and launch PHP built-in server --- tools/standalone/bin/serve | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100755 tools/standalone/bin/serve diff --git a/tools/standalone/bin/serve b/tools/standalone/bin/serve new file mode 100755 index 000000000000..6ff21dcff590 --- /dev/null +++ b/tools/standalone/bin/serve @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +{ # https://stackoverflow.com/a/21100710 + set -e + + CORE="$PWD" + SRV="$CORE/srv" + ADDR_PORT="localhost:8000" + PHP_CLI_SERVER_WORKERS=5 + + export PHP_CLI_SERVER_WORKERS + + echo "Update scaffolding ($SRV)" + php "$CORE/tools/standalone/bin/scaffold" $SRV --auto + + echo "Launch PHP web server" + php -S "$ADDR_PORT" -t "$SRV/web" "$CORE/tools/standalone/router.php" + + exit +} From 753ce2502a02a8757614e0b7638d517327a79874 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 12:57:58 -0700 Subject: [PATCH 13/23] serve - Add some preflight validation --- tools/standalone/bin/serve | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tools/standalone/bin/serve b/tools/standalone/bin/serve index 6ff21dcff590..074461c6b821 100755 --- a/tools/standalone/bin/serve +++ b/tools/standalone/bin/serve @@ -2,12 +2,25 @@ { # https://stackoverflow.com/a/21100710 set -e + function fatal() { + echo >&2 "$@" + exit 1 + } + CORE="$PWD" SRV="$CORE/srv" ADDR_PORT="localhost:8000" - PHP_CLI_SERVER_WORKERS=5 + export PHP_CLI_SERVER_WORKERS=5 + + if [ ! -d "$CORE/packages" ]; then + fatal "Missing: \"$CORE/packages\"" + fi + if [ ! -e "$CORE/vendor/autoload.php" ]; then + fatal "Missing: \"$CORE/vendor/autoload.php\"" + fi - export PHP_CLI_SERVER_WORKERS + php -r 'if (version_compare(PHP_VERSION, "8", "<")) echo "\n\n===========\nWARNING: In PHP 7.x, the built-in HTTP server may handle multiprocessing poorly!!\n===========\n";' + ## Specifically, the security-checks require auxiliary HTTP requests. echo "Update scaffolding ($SRV)" php "$CORE/tools/standalone/bin/scaffold" $SRV --auto From 5a16baba556fc8ac8732e841768ed46a8ae04495 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 13:06:27 -0700 Subject: [PATCH 14/23] Standalone.civi-setup.php - Allow installation for "$PWD/srv" --- setup/plugins/init/Standalone.civi-setup.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/setup/plugins/init/Standalone.civi-setup.php b/setup/plugins/init/Standalone.civi-setup.php index 4c22d61b9944..1d5b3fcf61b1 100644 --- a/setup/plugins/init/Standalone.civi-setup.php +++ b/setup/plugins/init/Standalone.civi-setup.php @@ -40,7 +40,23 @@ * Typically, this means that $projectRootPath might be like /var/www/example.org/ and * the actual web root would be /var/www/example.org/web/ */ - $projectRootPath = dirname($model->srcPath, 3); + $projectRootCandidates = [ + // Manual configuration + $model->extras['standaloneRoot'], + + // Ex: Clone ~/src/civicrm-core; use PHP built-in server and standalone. + $model->srcPath . '/srv', + + // Ex: Clone `civicrm-standalone` which depends on `civicrm-core`. Use Apache/nginx/etc. + dirname($model->srcPath, 3), + ]; + foreach ($projectRootCandidates as $projectRootCandidate) { + if ($projectRootCandidate && file_exists($projectRootCandidate)) { + $projectRootPath = $model->extras['standaloneRoot'] = $projectRootCandidate; + break; + } + } + $model->settingsPath = implode(DIRECTORY_SEPARATOR, [$projectRootPath, 'data', 'civicrm.settings.php']); $model->templateCompilePath = implode(DIRECTORY_SEPARATOR, [$projectRootPath, 'data', 'templates_c']); // print "\n-------------------------\nSet model values:\n" . json_encode($model->getValues(), JSON_PRETTY_PRINT) . "\n-----------------------------\n"; From 13bd93f1be02b50bf741ba60ee5e3c5334865b07 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 14:59:48 -0700 Subject: [PATCH 15/23] Standalone.civi-setup.php - Assign paths/URLs (when not using civicrm-asset-plugin) This should still work the same way when `civicrm-asset-plugin` is present (HAN-C). It affects other builds (HAN-D, SRV). --- setup/plugins/init/Standalone.civi-setup.php | 22 ++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/setup/plugins/init/Standalone.civi-setup.php b/setup/plugins/init/Standalone.civi-setup.php index 1d5b3fcf61b1..f71f091ba32a 100644 --- a/setup/plugins/init/Standalone.civi-setup.php +++ b/setup/plugins/init/Standalone.civi-setup.php @@ -77,10 +77,6 @@ $model->cmsBaseUrl = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']; } - // These mandatorySettings become 'Additional settings from installer' (via 'extraSettings') and set values in - // $civicrm_setting['domain'][k] = v; - $model->mandatorySettings['userFrameworkResourceURL'] = $model->cmsBaseUrl . '/assets/civicrm/core'; - // These paths get set as // $civicrm_paths[k]['url'|'path'] = v $model->paths['cms.root'] = [ @@ -93,4 +89,22 @@ // Compute default locale. $model->lang = $_REQUEST['lang'] ?? 'en_US'; + + if (\Composer\InstalledVersions::isInstalled('civicrm/civicrm-asset-plugin')) { + $model->mandatorySettings['userFrameworkResourceURL'] = $model->cmsBaseUrl . '/assets/civicrm/core'; + // civicrm-asset-plugin will fill-in various $paths. + } + else { + $model->mandatorySettings['userFrameworkResourceURL'] = $model->cmsBaseUrl . '/core'; + $model->paths['civicrm.core']['url'] = $model->cmsBaseUrl . '/core'; + $model->paths['civicrm.core']['path'] = $model->srcPath; + $model->paths['civicrm.vendor']['url'] = $model->cmsBaseUrl . '/core/vendor'; + $model->paths['civicrm.vendor']['path'] = $model->srcPath . '/vendor'; + $model->paths['civicrm.bower']['url'] = $model->cmsBaseUrl . '/core/bower_components'; + $model->paths['civicrm.bower']['path'] = $model->srcPath . '/bower_components'; + $model->paths['civicrm.packages']['url'] = $model->cmsBaseUrl . '/core/packages'; + $model->paths['civicrm.packages']['path'] = file_exists($model->srcPath . '/packages') + ? $model->srcPath . '/packages' + : dirname($model->srcPath) . '/civicrm-packages'; + } }); From 90f12e6cd26c5ba60ec23053a3181f11c9bd3c66 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 14:59:14 -0700 Subject: [PATCH 16/23] router.php - Add support for /core/vendor --- tools/standalone/router.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/standalone/router.php b/tools/standalone/router.php index b3f66901c7fe..a47de077265b 100644 --- a/tools/standalone/router.php +++ b/tools/standalone/router.php @@ -42,6 +42,7 @@ public function __construct() { // TODO: Decide which convention we like more... $this->addRoute(';^/core/packages/(.*);', fn($m) => $this->sendFileFromFolder($this->findPackages(), $m[1])); + $this->addRoute(';^/core/vendor/(.*);', fn($m) => $this->sendFileFromFolder($this->findVendor(), $m[1])); $this->addRoute(';^/core/(.*);', fn($m) => $this->sendFileFromFolder($this->findCore(), $m[1])); $this->addRoute(';^/civicrm-packages/(.*);', fn($m) => $this->sendFileFromFolder($this->findPackages(), $m[1])); @@ -129,8 +130,11 @@ public function sendError(int $code, string $message): bool { } public function findCore(): string { - $r = dirname(__DIR__, 2); - return $r; + return dirname(__DIR__, 2); + } + + public function findVendor(): string { + return $this->findCore() . '/vendor'; } public function findPackages(): string { From 9d60891baa771bc38eb3b0e3a1b08a814c97d6a1 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 20:14:36 -0700 Subject: [PATCH 17/23] router.php - Fix mime type for *.css --- tools/standalone/router.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/standalone/router.php b/tools/standalone/router.php index a47de077265b..05a028ddd4ad 100644 --- a/tools/standalone/router.php +++ b/tools/standalone/router.php @@ -112,9 +112,12 @@ public function sendFileFromFolder(string $basePath, string $relPath): bool { return $this->sendError(404, "File not found"); } - $stat = stat($absFile); - header('Content-Type: ' . mime_content_type($absFile)); - header('Content-Length: ' . $stat['size']); + require_once $this->findVendor() . '/autoload.php'; + + $info = new SplFileInfo($absFile); + $mimeRepository = new \MimeTyper\Repository\MimeDbRepository(); + header('Content-Type: ' . $mimeRepository->findType($info->getExtension())); + header('Content-Length: ' . $info->getSize()); readfile($absFile, FALSE); return TRUE; } From 78c9b0f2162131895de0c04cbbbd41f83a4f6d13 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 20:15:37 -0700 Subject: [PATCH 18/23] router.php - Call CRM_Core_Invoke. Add top-level redirect. --- tools/standalone/router.php | 52 ++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/tools/standalone/router.php b/tools/standalone/router.php index 05a028ddd4ad..23d68988b2a0 100644 --- a/tools/standalone/router.php +++ b/tools/standalone/router.php @@ -29,8 +29,12 @@ class StandaloneRouter { public function __construct() { // Note: Routing rules are processed sequentially, until one handles the request. + // Redirect common entry points + $this->addRoute(';^/$;', fn($m) => $this->sendRedirect('/civicrm/')); + $this->addRoute(';^/civicrm$;', fn($m) => $this->sendRedirect('/civicrm/')); + // If it looks like a Civi route, then call CRM_Core_Invoke. - $this->addRoute(';^/(civicrm(/.*)?)$;', fn($m) => $this->invoke($m[1])); + $this->addRoute(';^/(civicrm/.*)$;', fn($m) => $this->invoke($m[1])); // If there's a concrete file in HTTP root (`web/`), then serve that. $this->addRoute(';/(.*);', function($m) { @@ -97,8 +101,46 @@ public function addRoute(string $regex, callable $handler): void { ]; } - public function invoke(string $path) { - echo "Invoke route: " . htmlentities($path) . "
"; + /** + * Invoke a civicrm route. + * + * @param string $path + * Ex: 'civicrm/admin/foobar' + * @return bool + * @throws \CRM_Core_Exception + */ + public function invoke(string $path): bool { + // Do we need this? + $_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'index.php'; + $_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR . 'index.php'; + $_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR . 'index.php'; + + // echo "Invoke route: " . htmlentities($path) . "
"; + + // require_once $this->findVendor() . '/autoload.php'; + // require_once 'CRM/Core/ClassLoader.php'; + // CRM_Core_ClassLoader::singleton()->register(); + require_once $this->findSettingsPhp(); + + // Required so that the userID is set before generating the menu + \CRM_Core_Session::singleton()->initialize(); + // Add CSS, JS, etc. that is required for this page. + \CRM_Core_Resources::singleton()->addCoreResources(); + + $parts = explode('?', $_SERVER['REQUEST_URI']); + $args = explode('/', $path); + // Remove empty values + $args = array_values(array_filter($args)); + // Set this for compatibility + $_GET['q'] = implode('/', $args); + // And finally render the page + print CRM_Core_Invoke::invoke($args); + + return TRUE; + } + + public function sendRedirect($path) { + header('Location: ' . $path); return TRUE; } @@ -151,6 +193,10 @@ public function findPackages(): string { throw new \RuntimeException("Failed to find civicrm-packages"); } + public function findSettingsPhp(): string { + return dirname($_SERVER['DOCUMENT_ROOT']) . '/data/civicrm.settings.php'; + } + /** * @param string $basePath * @param string $relPath From 441a56c8e16af704f3371293efc1456ad67d7b18 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 20:09:11 -0700 Subject: [PATCH 19/23] Security Checks - Update error report for unsupported HTTPD. Some security checks don't work on PHP 7.4's built-in HTTPD -- they cause the system hang for >1min (*while internal HTTP calls timeout*). The checks run whenever the admin logs in -- so having it hang is fairly annoying (*for the developer who uses PHP built-in HTTPD*). This only affects local development, so it's not critical to run the checks. We'll skip the affected tests and show a simple (quick) error instead. --- CRM/Utils/Check/Component/Security.php | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CRM/Utils/Check/Component/Security.php b/CRM/Utils/Check/Component/Security.php index 90510c8e6213..9cb3d077693c 100644 --- a/CRM/Utils/Check/Component/Security.php +++ b/CRM/Utils/Check/Component/Security.php @@ -109,6 +109,10 @@ public function checkLogFileIsNotAccessible() { * @todo Test with WordPress, Joomla. */ public function checkUploadsAreNotAccessible() { + if ($this->isLimitedDevelopmentServer()) { + return []; + } + $messages = []; $config = CRM_Core_Config::singleton(); @@ -140,6 +144,28 @@ public function checkUploadsAreNotAccessible() { return $messages; } + /** + * Some security checks require sending a real HTTP request. This breaks the single-threading + * model historically used by the PHP built-in webserver (for local development). There is some + * experimental support for multi-threading in PHP 7.4+. Anecdotally, this is still insufficient + * on PHP 7.4 -- but it works well enough on PHP 8.1. + * + * @return CRM_Utils_Check_Message[] + */ + public function checkHttpAuditable() { + $messages = []; + if ($this->isLimitedDevelopmentServer()) { + $messages[] = new CRM_Utils_Check_Message( + __FUNCTION__, + ts('In PHP 7.x, the built-in HTTP server cannot execute some security checks. This problem only affects local development on older versions of PHP.'), + ts('Incomplete Security Checks'), + \Psr\Log\LogLevel::WARNING, + 'fa-lock' + ); + } + return $messages; + } + /** * Check if our uploads or ConfigAndLog directories have browseable * listings. @@ -156,6 +182,10 @@ public function checkUploadsAreNotAccessible() { * @todo Test with WordPress, Joomla. */ public function checkDirectoriesAreNotBrowseable() { + if ($this->isLimitedDevelopmentServer()) { + return []; + } + $messages = []; $config = CRM_Core_Config::singleton(); $publicDirs = [ @@ -356,6 +386,10 @@ public function checkAnonPermissions() { return $messages; } + public function isLimitedDevelopmentServer(): bool { + return PHP_SAPI === 'cli-server' && version_compare(PHP_VERSION, '8.0', '<'); + } + /** * Determine whether $url is a public, browsable listing for $dir * From 67709b31a64217bd6e2a760c036f294b679f0b4b Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 20:20:26 -0700 Subject: [PATCH 20/23] router.php - Fix loading of font files --- tools/standalone/router.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/standalone/router.php b/tools/standalone/router.php index 23d68988b2a0..818f462fed8c 100644 --- a/tools/standalone/router.php +++ b/tools/standalone/router.php @@ -22,7 +22,7 @@ */ class StandaloneRouter { - private const ALLOW_VIRTUAL_FILES = ';\.(jpg|png|css|js|html|txt|json|yml|xml|md)$;'; + private const ALLOW_VIRTUAL_FILES = ';\.(jpg|png|css|js|html|txt|json|yml|xml|md|woff2)$;'; private $routes = []; From 5b2e7e4bb7687ef353d0800d0c99482f34e026cf Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 22:13:25 -0700 Subject: [PATCH 21/23] Standalone.civi-setup.php - Fix for HAN-D layout --- setup/plugins/init/Standalone.civi-setup.php | 32 ++++++++++++-------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/setup/plugins/init/Standalone.civi-setup.php b/setup/plugins/init/Standalone.civi-setup.php index f71f091ba32a..ca71d74ea9d7 100644 --- a/setup/plugins/init/Standalone.civi-setup.php +++ b/setup/plugins/init/Standalone.civi-setup.php @@ -40,22 +40,30 @@ * Typically, this means that $projectRootPath might be like /var/www/example.org/ and * the actual web root would be /var/www/example.org/web/ */ - $projectRootCandidates = [ - // Manual configuration - $model->extras['standaloneRoot'], + if (!empty($model->extras['standaloneRoot'])) { + $projectRootPath = $model->extras['standaloneRoot']; + } + else { + $candidates = [ + // Ex: Clone ~/src/civicrm-core; use PHP built-in server and standalone. + $model->srcPath . '/srv', - // Ex: Clone ~/src/civicrm-core; use PHP built-in server and standalone. - $model->srcPath . '/srv', + // Ex: Make a vhost and clone `civicrm-core` as `HTTP_ROOT/core` + dirname($model->srcPath, 2), - // Ex: Clone `civicrm-standalone` which depends on `civicrm-core`. Use Apache/nginx/etc. - dirname($model->srcPath, 3), - ]; - foreach ($projectRootCandidates as $projectRootCandidate) { - if ($projectRootCandidate && file_exists($projectRootCandidate)) { - $projectRootPath = $model->extras['standaloneRoot'] = $projectRootCandidate; - break; + // Ex: Clone `civicrm-standalone` which depends on `civicrm-core`. Use Apache/nginx/etc. + dirname($model->srcPath, 3), + ]; + foreach ($candidates as $candidate) { + if (file_exists($candidate . '/civicrm.config.php.standalone')) { + $projectRootPath = $model->extras['standaloneRoot'] = $candidate; + break; + } } } + if (empty($projectRootPath)) { + throw new \RuntimeException("Failed to identify standalone root. (TIP: Set extras.standaloneRoot)"); + } $model->settingsPath = implode(DIRECTORY_SEPARATOR, [$projectRootPath, 'data', 'civicrm.settings.php']); $model->templateCompilePath = implode(DIRECTORY_SEPARATOR, [$projectRootPath, 'data', 'templates_c']); From 1ba0aaddc198a0018c6609a7cc9c5ca2295f8df4 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 22:25:02 -0700 Subject: [PATCH 22/23] router.php - Remove unused variable --- tools/standalone/router.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/standalone/router.php b/tools/standalone/router.php index 818f462fed8c..42d0894732d7 100644 --- a/tools/standalone/router.php +++ b/tools/standalone/router.php @@ -127,7 +127,6 @@ public function invoke(string $path): bool { // Add CSS, JS, etc. that is required for this page. \CRM_Core_Resources::singleton()->addCoreResources(); - $parts = explode('?', $_SERVER['REQUEST_URI']); $args = explode('/', $path); // Remove empty values $args = array_values(array_filter($args)); From 67e0365d1a6ab68b5ef86ecf77d8ff2668471e08 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 8 Jul 2023 22:25:58 -0700 Subject: [PATCH 23/23] index.php - Compatibility with HAN-C and HAN-D --- setup/res/index.php.txt | 83 +++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/setup/res/index.php.txt b/setup/res/index.php.txt index 1b934732d474..6f34d377e5f9 100644 --- a/setup/res/index.php.txt +++ b/setup/res/index.php.txt @@ -31,39 +31,66 @@ function invoke() { } } -if (file_exists('../data/civicrm.settings.php')) { - require_once '../vendor/autoload.php'; - require_once '../data/civicrm.settings.php'; +function findStandaloneSettings(): string { + return dirname($_SERVER['DOCUMENT_ROOT']) . '/data/civicrm.settings.php'; +} + +function findStandaloneCore(): ?string { + $candidates = [ + implode(DIRECTORY_SEPARATOR, [$_SERVER['DOCUMENT_ROOT'], 'core']), + implode(DIRECTORY_SEPARATOR, [dirname($_SERVER['DOCUMENT_ROOT']), 'vendor', 'civicrm', 'civicrm-core']), + ]; + foreach ($candidates as $candidate) { + if (file_exists($candidate)) { + return $candidate; + } + } + return NULL; +} + +function findStandaloneAutoload(): ?string { + $candidates = [ + implode(DIRECTORY_SEPARATOR, [dirname($_SERVER['DOCUMENT_ROOT']), 'vendor', 'autoload.php']), + implode(DIRECTORY_SEPARATOR, [$_SERVER['DOCUMENT_ROOT'], 'core', 'vendor', 'autoload.php']), + ]; + foreach ($candidates as $candidate) { + if (file_exists($candidate)) { + return $candidate; + } + } + return NULL; +} + +require_once findStandaloneAutoload(); +$civiCorePath = findStandaloneCore(); +$classLoader = implode(DIRECTORY_SEPARATOR, [$civiCorePath, 'CRM', 'Core', 'ClassLoader.php']); +require_once $classLoader; +CRM_Core_ClassLoader::singleton()->register(); + +if (file_exists(findStandaloneSettings())) { + require_once findStandaloneSettings(); invoke(); } else { $coreUrl = '/assets/civicrm/core'; - // This is the path to the root of the composer project (as opposed to the web/doc root path, which would be this + /web/) - $projectRootPath = dirname(__DIR__); - $civiCorePath = implode(DIRECTORY_SEPARATOR, [$projectRootPath, 'vendor', 'civicrm', 'civicrm-core']); - $classLoader = implode(DIRECTORY_SEPARATOR, [$civiCorePath, 'CRM', 'Core', 'ClassLoader.php']); - if (file_exists($classLoader)) { - require_once $classLoader; - CRM_Core_ClassLoader::singleton()->register(); - \Civi\Setup::assertProtocolCompatibility(1.0); + \Civi\Setup::assertProtocolCompatibility(1.0); - \Civi\Setup::init([ - // This is just enough information to get going. - 'cms' => 'Standalone', - 'srcPath' => $civiCorePath, - ]); - $ctrl = \Civi\Setup::instance()->createController()->getCtrl(); + \Civi\Setup::init([ + // This is just enough information to get going. + 'cms' => 'Standalone', + 'srcPath' => $civiCorePath, + ]); + $ctrl = \Civi\Setup::instance()->createController()->getCtrl(); - $ctrl->setUrls([ - // The URL of this setup controller. May be used for POST-backs - 'ctrl' => '/civicrm', /* @todo this had url('civicrm') ? */ - // The base URL for loading resource files (images/javascripts) for this project. Includes trailing slash. - 'res' => $coreUrl . '/setup/res/', - 'jquery.js' => $coreUrl . '/bower_components/jquery/dist/jquery.min.js', - 'font-awesome.css' => $coreUrl . '/bower_components/font-awesome/css/font-awesome.min.css', - ]); - \Civi\Setup\BasicRunner::run($ctrl); - exit(); - } + $ctrl->setUrls([ + // The URL of this setup controller. May be used for POST-backs + 'ctrl' => '/civicrm', /* @todo this had url('civicrm') ? */ + // The base URL for loading resource files (images/javascripts) for this project. Includes trailing slash. + 'res' => $coreUrl . '/setup/res/', + 'jquery.js' => $coreUrl . '/bower_components/jquery/dist/jquery.min.js', + 'font-awesome.css' => $coreUrl . '/bower_components/font-awesome/css/font-awesome.min.css', + ]); + \Civi\Setup\BasicRunner::run($ctrl); + exit(); }