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}