diff --git a/doc/release-notes/rl-2605.section.md b/doc/release-notes/rl-2605.section.md
index ed1f18a3c3bb..78cec99ea0f0 100644
--- a/doc/release-notes/rl-2605.section.md
+++ b/doc/release-notes/rl-2605.section.md
@@ -319,3 +319,5 @@ gnuradioMinimal.override {
### Additions and Improvements {#sec-nixpkgs-release-26.05-lib-additions-improvements}
- The builder `php.buildComposerProject2` for PHP applications has been improved for better reliability and stability.
+
+- The `services.drupal` module has a few improvements aimed at making it better for installing custom Drupal instances, namely a `webRoot` option and a some new settings for managing variable content and filepaths.
diff --git a/nixos/modules/services/web-apps/drupal.nix b/nixos/modules/services/web-apps/drupal.nix
index 91ab0b773a17..f05005bec0c9 100644
--- a/nixos/modules/services/web-apps/drupal.nix
+++ b/nixos/modules/services/web-apps/drupal.nix
@@ -10,6 +10,7 @@ let
any
attrValues
flatten
+ getExe
literalExpression
mapAttrs
mapAttrs'
@@ -24,6 +25,9 @@ let
optionalAttrs
types
;
+ inherit (lib.strings)
+ removePrefix
+ ;
inherit (pkgs)
mariadb
stdenv
@@ -41,23 +45,42 @@ let
name = "drupal-${hostName}";
src = cfg.package;
+ buildInputs = [ pkgs.rsync ];
+
installPhase = ''
runHook preInstall
mkdir -p $out
- cp -r * $out/
+ rsync -aq * $out/ --exclude=${removePrefix "/" cfg.webRoot}/sites --exclude=sites
runHook postInstall
'';
postInstall = ''
- ln -s ${cfg.filesDir} $out/share/php/${cfg.package.pname}/sites/default/files
- ln -s ${cfg.stateDir}/sites/default/settings.php $out/share/php/${cfg.package.pname}/sites/default/settings.php
+ ln -s ${cfg.stateDir}/sites $out/share/php/${cfg.package.pname}${cfg.webRoot}
ln -s ${cfg.modulesDir} $out/share/php/${cfg.package.pname}/modules
ln -s ${cfg.themesDir} $out/share/php/${cfg.package.pname}/themes
'';
});
+ sites =
+ hostName: cfg:
+ stdenv.mkDerivation (finalAttrs: {
+ pname = "drupal-sites-${hostName}";
+ name = "drupal-sites-${hostName}";
+ src = cfg.package;
+ buildInputs = with pkgs; [ rsync ];
+
+ installPhase = ''
+ runHook preInstall
+
+ mkdir -p $out/sites
+ rsync -a ./share/php/${cfg.package.pname}${cfg.webRoot}/sites/* $out/sites/
+
+ runHook postInstall
+ '';
+ });
+
drupalSettings =
hostName: cfg:
pkgs.writeTextFile {
@@ -87,6 +110,75 @@ let
'';
};
+ # Required .htaccess for private files directory
+ # See: https://www.drupal.org/docs/getting-started/installing-drupal/securing-drupal-file-directories
+ privateFilesHtAccess = pkgs.writeTextFile {
+ name = "private-files-htaccess";
+ text = ''
+ # Turn off all options we don't need.
+ Options -Indexes -ExecCGI -Includes -MultiViews
+
+ # Set the catch-all handler to prevent scripts from being executed.
+ SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
+
+ # Override the handler again if we're run later in the evaluation list.
+ SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
+
+
+ # If we know how to do it safely, disable the PHP engine entirely.
+
+ php_flag engine off
+
+ '';
+ };
+
+ stateDirManage =
+ hostName: cfg:
+ pkgs.writeShellApplication {
+ name = "drupal-state-init-${hostName}";
+ excludeShellChecks = [ "SC2194" ];
+ runtimeInputs = with pkgs; [ rsync ];
+ text = ''
+ echo "Updating the sites directory for ${hostName}..."
+ rsync -auq "${sites hostName cfg}/sites/" "${cfg.stateDir}/sites/" \
+ --exclude "*/files" \
+ --delete-before
+
+ if [ ! -d "${cfg.filesDir}" ]; then
+ echo "Preparing files directory..."
+ mkdir -p "${cfg.filesDir}"
+ chown -R ${user}:${webserver.group} ${cfg.filesDir}
+ fi
+
+ case ${cfg.filesDir} in
+ ${cfg.stateDir}/sites*) echo "Files directory is in sites directory. Skipping optional link!";;
+ *) ln -sf "${cfg.filesDir}" "${cfg.stateDir}/sites/default/files";;
+ esac
+
+ if [ ! -f "${cfg.privateFilesDir}/.htaccess" ]; then
+ echo "Linking .htaccess file for private files directory..."
+ ln -s "${privateFilesHtAccess}" "${cfg.privateFilesDir}/.htaccess"
+ fi
+
+ echo "Preparing settings.php for ${hostName}..."
+ settings_file="${cfg.stateDir}/sites/default/settings.php"
+
+ if [ ! -f "$settings_file" ]; then
+ default_settings_file="${cfg.stateDir}/sites/default/default.settings.php";
+ cp "$default_settings_file" "$settings_file"
+ fi
+
+ cat < ${appendSettings hostName} >> "$settings_file"
+ chmod 644 "$settings_file"
+
+ # Link the NixOS-managed settings file to the state directory.
+ ln -sf ${drupalSettings hostName cfg} ${cfg.stateDir}/sites/default/settings.nixos-${hostName}.php
+
+ # Set or reset file permissions so that the web user and webserver owns them.
+ chown -R ${user}:${webserver.group} ${cfg.stateDir}
+ '';
+ };
+
siteOpts =
{
options,
@@ -106,6 +198,9 @@ let
defaultText = "/var/lib/drupal//sites/default/files";
description = ''
The location of the Drupal files directory.
+
+ Many of the files in this directory are variable, so they must be located
+ in a location writeable by users of the webgroup.
'';
};
@@ -120,7 +215,22 @@ let
type = types.path;
default = "/var/lib/drupal/${name}/config/sync";
defaultText = "/var/lib/drupal//config/sync";
- description = "The location of the Drupal config sync directory.";
+ description = ''
+ The location of the user-managed Drupal config sync directory.
+ Drupal will both read from and write to this directory when executing
+ configuration management operations.
+ '';
+ };
+
+ webRoot = mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ An optional path string with a leading slash
+ indicating the location of the Drupal webroot relative to the
+ project root directory, if one exists.
+ '';
+ example = "/web";
};
extraConfig = mkOption {
@@ -140,21 +250,41 @@ let
type = types.path;
default = "/var/lib/drupal/${name}";
defaultText = "/var/lib/drupal/";
- description = "The location of the Drupal site state directory.";
+ description = ''
+ The location of the user-managed Drupal site state directory.
+ This directory will contain the settings and configuration files for
+ your Drupal instance. It may also contain your files directory if the
+ `filesDir` option remains unchanged.
+
+ Many of the files in this directory are variable, so they must be located
+ in a location writeable by users of the webgroup.
+ '';
};
modulesDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}/modules";
defaultText = "/var/lib/drupal//modules";
- description = "The location for users to install Drupal modules.";
+ description = ''
+ The location for users to manually install Drupal modules.
+
+ Note: in most instances, it is preferable to install modules using
+ composer, or to package them with your source code repository, if
+ you are using a custom Drupal.
+ '';
};
themesDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}/themes";
defaultText = "/var/lib/drupal//themes";
- description = "The location for users to install Drupal themes.";
+ description = ''
+ The location for users to manually install Drupal themes.
+
+ Note: in most instances, it is preferable to install themes using
+ composer, or to package them with your source code repository, if
+ you are using a custom Drupal.
+ '';
};
phpOptions = mkOption {
@@ -357,6 +487,8 @@ in
"d '${cfg.themesDir}' 0750 ${user} ${webserver.group} - -"
"Z '${cfg.themesDir}' 0750 ${user} ${webserver.group} - -"
"d '${cfg.privateFilesDir}' 0750 ${user} ${webserver.group} - -"
+ "d '${cfg.filesDir}' 0750 ${user} ${webserver.group} - -"
+ "Z '${cfg.filesDir}' 0750 ${user} ${webserver.group} - -"
"d '${cfg.configSyncDir}' 0750 ${user} ${webserver.group} - -"
]) eachSite
);
@@ -382,42 +514,15 @@ in
User = "root";
RemainAfterExit = true;
- ExecStart = writeShellScript "drupal-state-init-${hostName}" ''
- set -e
-
- if [ ! -d "${cfg.stateDir}/sites" ]; then
- echo "Preparing sites directory..."
- cp -r "${cfg.package}/share/php/${cfg.package.pname}/sites" "${cfg.stateDir}"
- fi
-
- if [ ! -d "${cfg.filesDir}" ]; then
- echo "Preparing files directory..."
- mkdir -p "${cfg.filesDir}"
- chown -R ${user}:${webserver.group} ${cfg.filesDir}
- fi
-
- settings_file="${cfg.stateDir}/sites/default/settings.php"
- default_settings="${cfg.package}/share/php/${cfg.package.pname}/sites/default/default.settings.php"
-
- if [ ! -f "$settings_file" ]; then
- echo "Preparing settings.php for ${hostName}..."
- cp "$default_settings" "$settings_file"
- cat < ${appendSettings hostName} >> "$settings_file"
- chmod 644 "$settings_file"
- fi
-
- # Link the NixOS-managed settings file to the state directory.
- ln -sf ${drupalSettings hostName cfg} ${cfg.stateDir}/sites/default/settings.nixos-${hostName}.php
-
- # Set or reset file permissions so that the web user and webserver owns them.
- chown -R ${user}:${webserver.group} ${cfg.stateDir}
- '';
+ ExecStart = getExe (stateDirManage hostName cfg);
};
# Rerun this service if certain settings were updated
reloadTriggers = [
cfg.extraConfig
cfg.privateFilesDir
+ cfg.filesDir
+ cfg.stateDir
cfg.configSyncDir
];
})
@@ -434,9 +539,9 @@ in
enable = true;
virtualHosts = mapAttrs (hostName: cfg: {
serverName = mkDefault hostName;
- root = "${pkg hostName cfg}/share/php/${cfg.package.pname}";
+ root = "${pkg hostName cfg}/share/php/${cfg.package.pname}${cfg.webRoot}";
extraConfig = ''
- index index.php;
+ index index.php index.htm index.html;
'';
locations = {
"~ '\\.php$|^/update\\.php'" = {
@@ -515,11 +620,19 @@ in
};
"~ ^/sites/.*/files/styles/" = {
extraConfig = ''
+ alias ${cfg.filesDir}/;
+ try_files $uri @rewrite;
+ '';
+ };
+ "^~ /sites/.*/files/" = {
+ extraConfig = ''
+ alias ${cfg.filesDir}/;
try_files $uri @rewrite;
'';
};
"~ ^(/[a-z\\-]+)?/system/files/" = {
extraConfig = ''
+ alias ${cfg.privateFilesDir}/;
try_files $uri /index.php?$query_string;
'';
};
@@ -535,8 +648,9 @@ in
hostName: cfg:
(nameValuePair hostName {
extraConfig = ''
- root * ${pkg hostName cfg}/share/php/${cfg.package.pname}
+ root * ${pkg hostName cfg}/share/php/${cfg.package.pname}${cfg.webRoot}
file_server
+ root /sites/*/files ${cfg.filesDir}
encode zstd gzip
php_fastcgi unix/${config.services.phpfpm.pools."drupal-${hostName}".socket}