Files
Michael Daniels 2e87c2f8f4 nixos/*: remove unused let bindings
Generated using deadnix (with some manual work); split from #514611.
2026-05-01 20:39:14 -04:00

727 lines
22 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
any
attrValues
flatten
getExe
literalExpression
mapAttrs
mapAttrs'
mapAttrsToList
mkDefault
mkEnableOption
mkIf
mkMerge
mkOption
mkPackageOption
nameValuePair
optionalAttrs
optionalString
types
;
inherit (lib.strings)
removePrefix
removeSuffix
;
inherit (pkgs)
mariadb
stdenv
;
cfg = config.services.drupal;
eachSite = cfg.sites;
user = "drupal";
webserver = config.services.${cfg.webserver};
pkg =
hostName: cfg:
stdenv.mkDerivation (finalAttrs: {
pname = "drupal-${hostName}";
name = "drupal-${hostName}";
src = cfg.package;
buildInputs = [ pkgs.rsync ];
installPhase = ''
runHook preInstall
mkdir -p $out
EXCLUDES="--exclude=${removePrefix "/" cfg.webRoot}/sites --exclude=sites"
if [ ! -z "${cfg.configRoot}" ]; then
EXCLUDES="$EXCLUDES --exclude=${removePrefix "/" cfg.configRoot}"
fi
rsync -aq * $out/ $EXCLUDES
runHook postInstall
'';
postInstall = ''
ln -s ${cfg.stateDir}/sites $out/share/php/${cfg.package.pname}${cfg.webRoot}
ln -s ${cfg.modulesDir} $out/share/php/${cfg.package.pname}${cfg.webRoot}/modules/nixos-modules
ln -s ${cfg.themesDir} $out/share/php/${cfg.package.pname}${cfg.webRoot}/themes/nixos-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
'';
});
configSync =
hostName: cfg:
optionalString (cfg.configRoot != "") (
stdenv.mkDerivation (finalAttrs: {
pname = "drupal-config-${hostName}";
name = "drupal-config-${hostName}";
src = cfg.package;
buildInputs = with pkgs; [ rsync ];
installPhase = ''
runHook preInstall
mkdir -p $out/config
rsync -a ./share/php/${cfg.package.pname}${cfg.configRoot}/* $out/config/
runHook postInstall
'';
})
);
drupalSettings =
hostName: cfg:
pkgs.writeTextFile {
name = "settings.nixos-${hostName}.php";
text = ''
<?php
// NixOS automatically generated settings
$settings['file_private_path'] = '${cfg.privateFilesDir}';
$settings['config_sync_directory'] = '${cfg.configSyncDir}';
// Extra config
${cfg.extraConfig}
'';
checkPhase = "${pkgs.php}/bin/php --syntax-check $target";
};
appendSettings =
hostName:
pkgs.writeTextFile {
name = "append-drupal-settings-${hostName}";
text = ''
// NixOS settings file import.
require dirname(__FILE__) . '/settings.nixos-${hostName}.php';
'';
};
# 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
<Files *>
# Override the handler again if we're run later in the evaluation list.
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>
# If we know how to do it safely, disable the PHP engine entirely.
<IfModule mod_php.c>
php_flag engine off
</IfModule>
'';
};
stateDirManage =
hostName: cfg:
pkgs.writeShellApplication {
name = "drupal-state-init-${hostName}";
excludeShellChecks = [
"SC2194"
"SC2157"
];
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 [ ! -z "${cfg.configRoot}" ]; then
echo "Updating config sync directory for ${hostName}..."
rsync -auq "${configSync hostName cfg}/config/" "${removeSuffix "/sync" cfg.configSyncDir}" \
--delete-before
fi
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
if ! grep -qF "require dirname(__FILE__) . '/settings.nixos-${hostName}.php';" ${cfg.stateDir}/sites/default/settings.php; then
echo "Appending NixOS generated settings..."
cat < ${appendSettings hostName} >> "$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}
'';
};
siteOpts =
{
options,
config,
lib,
name,
...
}:
{
options = {
enable = mkEnableOption "Drupal web application";
package = mkPackageOption pkgs "drupal" { };
filesDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}/sites/default/files";
defaultText = "/var/lib/drupal/<name>/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.
'';
};
privateFilesDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}/private";
defaultText = "/var/lib/drupal/<name>/private";
description = "The location of the Drupal private files directory.";
};
configSyncDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}/config/sync";
defaultText = "/var/lib/drupal/<name>/config/sync";
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.
This option differs from the `configRoot` option,
which this service uses to discover
the location of the config sync directory in the package's source code.
'';
};
webRoot = mkOption {
type = types.str;
default = "";
description = ''
An optional path string with a leading slash
indicating the location of the Drupal webroot
in your package's source code.
The path relative to the project root directory.
'';
example = "/web";
};
configRoot = mkOption {
type = types.str;
default = "";
description = ''
An optional path string with a leading slash
indicating the location of the config sync directory on
the Drupal package. Your package will probably have a config sync
directory if it has been significantly customized.
The path must be relative to the project root directory.
This option differs from the `configSyncDir` option, which
tells this service where to create a user-writeable config directory
on NixOS.
'';
example = "/config";
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration values that you want to insert into settings.php.
All configuration must be written as PHP script.
'';
example = ''
$config['user.settings']['anonymous'] = 'Visitor';
$settings['entity_update_backup'] = TRUE;
'';
};
stateDir = mkOption {
type = types.path;
default = "/var/lib/drupal/${name}";
defaultText = "/var/lib/drupal/<name>";
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/<name>/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/<name>/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 {
type = types.attrsOf types.str;
default = { };
description = ''
Options for PHP's php.ini file for this Drupal site.
'';
example = literalExpression ''
{
"opcache.interned_strings_buffer" = "8";
"opcache.max_accelerated_files" = "10000";
"opcache.memory_consumption" = "128";
"opcache.revalidate_freq" = "15";
"opcache.fast_shutdown" = "1";
}
'';
};
database = {
host = mkOption {
type = types.str;
default = "localhost";
description = "Database host address.";
};
port = mkOption {
type = types.port;
default = 3306;
description = "Database host port.";
};
name = mkOption {
type = types.str;
default = "drupal";
description = "Database name.";
};
user = mkOption {
type = types.str;
default = "drupal";
description = "Database user.";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/database-dbpassword";
description = ''
A file containing the password corresponding to
{option}`database.user`.
'';
};
tablePrefix = mkOption {
type = types.str;
default = "dp_";
description = ''
The $table_prefix is the value placed in the front of your database tables.
Change the value if you want to use something other than dp_ for your database
prefix. Typically this is changed if you are installing multiple Drupal sites
in the same database.
'';
};
socket = mkOption {
type = types.nullOr types.path;
default = null;
defaultText = literalExpression "/run/mysqld/mysqld.sock";
description = "Path to the unix socket file to use for authentication.";
};
createLocally = mkOption {
type = types.bool;
default = true;
description = "Create the database and database user locally.";
};
};
virtualHost = mkOption {
type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
example = literalExpression ''
{
adminAddr = "webmaster@example.org";
forceSSL = true;
enableACME = true;
}
'';
description = ''
Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
'';
};
poolConfig = mkOption {
type =
with types;
attrsOf (oneOf [
str
int
bool
]);
default = {
"pm" = "dynamic";
"pm.max_children" = 32;
"pm.start_servers" = 2;
"pm.min_spare_servers" = 2;
"pm.max_spare_servers" = 4;
"pm.max_requests" = 500;
};
description = ''
Options for the Drupal PHP pool. See the documentation on `php-fpm.conf`
for details on configuration directives.
'';
};
};
config.virtualHost.hostName = mkDefault name;
};
in
{
options = {
services.drupal = {
enable = mkEnableOption "drupal";
package = mkPackageOption pkgs "drupal" { };
sites = mkOption {
type = types.attrsOf (types.submodule siteOpts);
default = {
"localhost" = {
enable = true;
};
};
description = "Specification of one or more Drupal sites to serve";
};
webserver = mkOption {
type = types.enum [
"nginx"
"caddy"
];
default = "nginx";
description = ''
Whether to use nginx or caddy for virtual host management.
Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
See [](#opt-services.nginx.virtualHosts) for further information.
Further caddy configuration can be done by adapting `services.caddy.virtualHosts.<name>`.
See [](#opt-services.caddy.virtualHosts) for further information.
'';
};
};
};
config = mkIf (cfg.enable) (mkMerge [
{
assertions =
(mapAttrsToList (hostName: cfg: {
assertion = cfg.database.createLocally -> cfg.database.user == user;
message = ''services.drupal.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
}) eachSite)
++ (mapAttrsToList (hostName: cfg: {
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
message = ''services.drupal.sites."${hostName}".database.passwordFile cannot be specified if services.drupal.sites."${hostName}".database.createLocally is set to true.'';
}) eachSite);
services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
enable = true;
package = mkDefault mariadb;
ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
ensureUsers = mapAttrsToList (hostName: cfg: {
name = cfg.database.user;
ensurePermissions = {
"${cfg.database.name}.*" = "ALL PRIVILEGES";
};
}) eachSite;
};
services.phpfpm.pools = mapAttrs' (
hostName: cfg:
(nameValuePair "drupal-${hostName}" {
inherit user;
group = webserver.group;
settings = {
"listen.owner" = webserver.user;
"listen.group" = webserver.group;
}
// cfg.poolConfig;
})
) eachSite;
}
{
systemd.tmpfiles.rules = flatten (
mapAttrsToList (hostName: cfg: [
"d '${cfg.stateDir}' 0750 ${user} ${webserver.group} - -"
"d '${cfg.modulesDir}' 0750 ${user} ${webserver.group} - -"
"Z '${cfg.modulesDir}' 0750 ${user} ${webserver.group} - -"
"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
);
users.users.${user} = {
group = webserver.group;
isSystemUser = true;
};
}
{
# Run a service that prepares the state directory.
systemd.services = mkMerge [
(mapAttrs' (
hostName: cfg:
(nameValuePair "drupal-state-init-${hostName}" {
wantedBy = [ "multi-user.target" ];
before = [ "nginx.service" ];
after = [ "local-fs.target" ];
serviceConfig = {
Type = "oneshot";
User = "root";
RemainAfterExit = true;
ExecStart = getExe (stateDirManage hostName cfg);
};
# Rerun this service if certain settings were updated
reloadTriggers = [
cfg.extraConfig
cfg.privateFilesDir
cfg.filesDir
cfg.stateDir
cfg.configSyncDir
];
})
) eachSite)
(optionalAttrs (any (v: v.database.createLocally) (attrValues eachSite)) {
httpd.after = [ "mysql.service" ];
})
];
}
(mkIf (cfg.webserver == "nginx") {
services.nginx = {
enable = true;
virtualHosts = mapAttrs (hostName: cfg: {
serverName = mkDefault hostName;
root = "${pkg hostName cfg}/share/php/${cfg.package.pname}${cfg.webRoot}";
extraConfig = ''
index index.php index.htm index.html;
'';
locations = {
"~ '\\.php$|^/update\\.php'" = {
extraConfig = ''
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:${config.services.phpfpm.pools."drupal-${hostName}".socket};
fastcgi_index index.php;
include "${config.services.nginx.package}/conf/fastcgi.conf";
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
# Mitigate https://httpoxy.org/ vulnerabilities
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
'';
};
"= /favicon.ico" = {
extraConfig = ''
log_not_found off;
access_log off;
'';
};
"= /robots.txt" = {
extraConfig = ''
allow all;
log_not_found off;
access_log off;
'';
};
"~ \\..*/.*\\.php$" = {
extraConfig = ''
return 403;
'';
};
"~ ^/sites/.*/private/" = {
extraConfig = ''
return 403;
'';
};
"~ ^/sites/[^/]+/files/.*\\.php$" = {
extraConfig = ''
deny all;
'';
};
"~* ^/.well-known/" = {
extraConfig = ''
allow all;
'';
};
"/" = {
extraConfig = ''
try_files $uri /index.php?$query_string;
'';
};
"@rewrite" = {
extraConfig = ''
rewrite ^ /index.php;
'';
};
"~ /vendor/.*\\.php$" = {
extraConfig = ''
deny all;
return 404;
'';
};
"~* \\.(js|css|png|jpg|jpeg|gif|ico|svg)$" = {
extraConfig = ''
try_files $uri @rewrite;
expires max;
log_not_found off;
'';
};
"~ ^/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;
'';
};
};
}) eachSite;
};
})
(mkIf (cfg.webserver == "caddy") {
services.caddy = {
enable = true;
virtualHosts = mapAttrs' (
hostName: cfg:
(nameValuePair hostName {
extraConfig = ''
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}
'';
})
) cfg.sites;
};
})
]);
}