Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone - Add support for development-friendly file-layouts #26771

Merged
merged 23 commits into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b3268e8
Import Standalone.civi-setup.php
totten Jul 7, 2023
42096e6
Import civicrm.config.php.standalone.txt, htaccess.txt, and index.php…
totten Jul 7, 2023
7535bb4
Ignore ./srv
totten Jul 8, 2023
358583a
Add 'tools/standalone/bin/scaffold'
totten Jul 8, 2023
4be88dd
scaffold - Move key resources to a publishable folder
totten Jul 8, 2023
2c953e6
scaffold - Fix style errors from imported resources
totten Jul 8, 2023
e0a17db
scaffold - Overwrite symlinks
totten Jul 8, 2023
6353e27
(REF) scaffold - Make call-signature amenable composer-compile-plugin
totten Jul 8, 2023
4186fbe
router.php - First draft which handles static files for `/core`
totten Jul 8, 2023
a098c2c
router.php - Use class. Add more error handling. Add 'packages'.
totten Jul 8, 2023
a0dd2eb
router.php - Allow direct serving from 'web/'. Track routes as list.
totten Jul 8, 2023
29858a7
serve - Add script to fill scaffold and launch PHP built-in server
totten Jul 8, 2023
753ce25
serve - Add some preflight validation
totten Jul 8, 2023
5a16bab
Standalone.civi-setup.php - Allow installation for "$PWD/srv"
totten Jul 8, 2023
13bd93f
Standalone.civi-setup.php - Assign paths/URLs (when not using civicrm…
totten Jul 8, 2023
90f12e6
router.php - Add support for /core/vendor
totten Jul 8, 2023
9d60891
router.php - Fix mime type for *.css
totten Jul 9, 2023
78c9b0f
router.php - Call CRM_Core_Invoke. Add top-level redirect.
totten Jul 9, 2023
441a56c
Security Checks - Update error report for unsupported HTTPD.
totten Jul 9, 2023
67709b3
router.php - Fix loading of font files
totten Jul 9, 2023
5b2e7e4
Standalone.civi-setup.php - Fix for HAN-D layout
totten Jul 9, 2023
1ba0aad
router.php - Remove unused variable
totten Jul 9, 2023
67e0365
index.php - Compatibility with HAN-C and HAN-D
totten Jul 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ civicrm.settings.php
sql/dummy_processor.mysql
distmaker/distmaker.conf
distmaker/out
/srv
/tmp
34 changes: 34 additions & 0 deletions CRM/Utils/Check/Component/Security.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand All @@ -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 = [
Expand Down Expand Up @@ -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
*
Expand Down
118 changes: 118 additions & 0 deletions setup/plugins/init/Standalone.civi-setup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
/**
* @file
*
* Determine default settings for Standalone.
*/

if (!defined('CIVI_SETUP')) {
exit("Installation plugins must only be loaded by the installer.\n");
}

\Civi\Setup::dispatcher()
->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/
*/
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: 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 ($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']);
// 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 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';

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';
}
});
13 changes: 13 additions & 0 deletions setup/res/civicrm.config.php.standalone.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

/*
This is just a flag file? Not really used?

$settingsFile = __DIR__ . '/data/civicrm.settings.php';
define('CIVICRM_SETTINGS_PATH', $settingsFile);
$error = @include_once($settingsFile);
if ($error == false) {
echo "Could not load the settings file at: {$settingsFile}\n";
exit();
}
*/
117 changes: 117 additions & 0 deletions setup/res/htaccess.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#
# Apache/PHP settings for CiviCRM Standalone
# (based on Drupal7)
#

# Protect files and directories from prying eyes.
<FilesMatch "\.(inc|info|test|mo|po|sh|.*sql|tpl(\.php)?)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig\.save)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
</IfModule>
</FilesMatch>

# 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.
<IfModule mod_expires.c>
# Enable expirations.
ExpiresActive On

# Cache all files for 2 weeks after access (A).
ExpiresDefault A1209600

<FilesMatch \.php$>
# 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
</FilesMatch>
</IfModule>

# Various rewrite rules.
<IfModule mod_rewrite.c>
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
# <DirectoryMatch> 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]
</IfModule>

# Various header fixes.
<IfModule mod_headers.c>
# 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
</IfModule>
Loading