at 22.05-pre 18 kB view raw
1{ config, pkgs, lib, ... }: 2 3let 4 inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types; 5 inherit (lib) any attrValues concatMapStringsSep flatten literalExpression; 6 inherit (lib) filterAttrs mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString; 7 8 cfg = migrateOldAttrs config.services.wordpress; 9 eachSite = cfg.sites; 10 user = "wordpress"; 11 webserver = config.services.${cfg.webserver}; 12 stateDir = hostName: "/var/lib/wordpress/${hostName}"; 13 14 # Migrate config.services.wordpress.<hostName> to config.services.wordpress.sites.<hostName> 15 oldSites = filterAttrs (o: _: o != "sites" && o != "webserver"); 16 migrateOldAttrs = cfg: cfg // { sites = cfg.sites // oldSites cfg; }; 17 18 pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec { 19 pname = "wordpress-${hostName}"; 20 version = src.version; 21 src = cfg.package; 22 23 installPhase = '' 24 mkdir -p $out 25 cp -r * $out/ 26 27 # symlink the wordpress config 28 ln -s ${wpConfig hostName cfg} $out/share/wordpress/wp-config.php 29 # symlink uploads directory 30 ln -s ${cfg.uploadsDir} $out/share/wordpress/wp-content/uploads 31 32 # https://github.com/NixOS/nixpkgs/pull/53399 33 # 34 # Symlinking works for most plugins and themes, but Avada, for instance, fails to 35 # understand the symlink, causing its file path stripping to fail. This results in 36 # requests that look like: https://example.com/wp-content//nix/store/...plugin/path/some-file.js 37 # Since hard linking directories is not allowed, copying is the next best thing. 38 39 # copy additional plugin(s) and theme(s) 40 ${concatMapStringsSep "\n" (theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${theme.name}") cfg.themes} 41 ${concatMapStringsSep "\n" (plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${plugin.name}") cfg.plugins} 42 ''; 43 }; 44 45 wpConfig = hostName: cfg: pkgs.writeText "wp-config-${hostName}.php" '' 46 <?php 47 define('DB_NAME', '${cfg.database.name}'); 48 define('DB_HOST', '${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}'); 49 define('DB_USER', '${cfg.database.user}'); 50 ${optionalString (cfg.database.passwordFile != null) "define('DB_PASSWORD', file_get_contents('${cfg.database.passwordFile}'));"} 51 define('DB_CHARSET', 'utf8'); 52 $table_prefix = '${cfg.database.tablePrefix}'; 53 54 require_once('${stateDir hostName}/secret-keys.php'); 55 56 # wordpress is installed onto a read-only file system 57 define('DISALLOW_FILE_EDIT', true); 58 define('AUTOMATIC_UPDATER_DISABLED', true); 59 60 ${cfg.extraConfig} 61 62 if ( !defined('ABSPATH') ) 63 define('ABSPATH', dirname(__FILE__) . '/'); 64 65 require_once(ABSPATH . 'wp-settings.php'); 66 ?> 67 ''; 68 69 secretsVars = [ "AUTH_KEY" "SECURE_AUTH_KEY" "LOGGED_IN_KEY" "NONCE_KEY" "AUTH_SALT" "SECURE_AUTH_SALT" "LOGGED_IN_SALT" "NONCE_SALT" ]; 70 secretsScript = hostStateDir: '' 71 # The match in this line is not a typo, see https://github.com/NixOS/nixpkgs/pull/124839 72 grep -q "LOOGGED_IN_KEY" "${hostStateDir}/secret-keys.php" && rm "${hostStateDir}/secret-keys.php" 73 if ! test -e "${hostStateDir}/secret-keys.php"; then 74 umask 0177 75 echo "<?php" >> "${hostStateDir}/secret-keys.php" 76 ${concatMapStringsSep "\n" (var: '' 77 echo "define('${var}', '`tr -dc a-zA-Z0-9 </dev/urandom | head -c 64`');" >> "${hostStateDir}/secret-keys.php" 78 '') secretsVars} 79 echo "?>" >> "${hostStateDir}/secret-keys.php" 80 chmod 440 "${hostStateDir}/secret-keys.php" 81 fi 82 ''; 83 84 siteOpts = { lib, name, ... }: 85 { 86 options = { 87 package = mkOption { 88 type = types.package; 89 default = pkgs.wordpress; 90 defaultText = literalExpression "pkgs.wordpress"; 91 description = "Which WordPress package to use."; 92 }; 93 94 uploadsDir = mkOption { 95 type = types.path; 96 default = "/var/lib/wordpress/${name}/uploads"; 97 description = '' 98 This directory is used for uploads of pictures. The directory passed here is automatically 99 created and permissions adjusted as required. 100 ''; 101 }; 102 103 plugins = mkOption { 104 type = types.listOf types.path; 105 default = []; 106 description = '' 107 List of path(s) to respective plugin(s) which are copied from the 'plugins' directory. 108 <note><para>These plugins need to be packaged before use, see example.</para></note> 109 ''; 110 example = literalExpression '' 111 let 112 # Wordpress plugin 'embed-pdf-viewer' installation example 113 embedPdfViewerPlugin = pkgs.stdenv.mkDerivation { 114 name = "embed-pdf-viewer-plugin"; 115 # Download the theme from the wordpress site 116 src = pkgs.fetchurl { 117 url = "https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip"; 118 sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd"; 119 }; 120 # We need unzip to build this package 121 nativeBuildInputs = [ pkgs.unzip ]; 122 # Installing simply means copying all files to the output directory 123 installPhase = "mkdir -p $out; cp -R * $out/"; 124 }; 125 # And then pass this theme to the themes list like this: 126 in [ embedPdfViewerPlugin ] 127 ''; 128 }; 129 130 themes = mkOption { 131 type = types.listOf types.path; 132 default = []; 133 description = '' 134 List of path(s) to respective theme(s) which are copied from the 'theme' directory. 135 <note><para>These themes need to be packaged before use, see example.</para></note> 136 ''; 137 example = literalExpression '' 138 let 139 # Let's package the responsive theme 140 responsiveTheme = pkgs.stdenv.mkDerivation { 141 name = "responsive-theme"; 142 # Download the theme from the wordpress site 143 src = pkgs.fetchurl { 144 url = "https://downloads.wordpress.org/theme/responsive.3.14.zip"; 145 sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3"; 146 }; 147 # We need unzip to build this package 148 nativeBuildInputs = [ pkgs.unzip ]; 149 # Installing simply means copying all files to the output directory 150 installPhase = "mkdir -p $out; cp -R * $out/"; 151 }; 152 # And then pass this theme to the themes list like this: 153 in [ responsiveTheme ] 154 ''; 155 }; 156 157 database = { 158 host = mkOption { 159 type = types.str; 160 default = "localhost"; 161 description = "Database host address."; 162 }; 163 164 port = mkOption { 165 type = types.port; 166 default = 3306; 167 description = "Database host port."; 168 }; 169 170 name = mkOption { 171 type = types.str; 172 default = "wordpress"; 173 description = "Database name."; 174 }; 175 176 user = mkOption { 177 type = types.str; 178 default = "wordpress"; 179 description = "Database user."; 180 }; 181 182 passwordFile = mkOption { 183 type = types.nullOr types.path; 184 default = null; 185 example = "/run/keys/wordpress-dbpassword"; 186 description = '' 187 A file containing the password corresponding to 188 <option>database.user</option>. 189 ''; 190 }; 191 192 tablePrefix = mkOption { 193 type = types.str; 194 default = "wp_"; 195 description = '' 196 The $table_prefix is the value placed in the front of your database tables. 197 Change the value if you want to use something other than wp_ for your database 198 prefix. Typically this is changed if you are installing multiple WordPress blogs 199 in the same database. 200 201 See <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php#table_prefix'/>. 202 ''; 203 }; 204 205 socket = mkOption { 206 type = types.nullOr types.path; 207 default = null; 208 defaultText = literalExpression "/run/mysqld/mysqld.sock"; 209 description = "Path to the unix socket file to use for authentication."; 210 }; 211 212 createLocally = mkOption { 213 type = types.bool; 214 default = true; 215 description = "Create the database and database user locally."; 216 }; 217 }; 218 219 virtualHost = mkOption { 220 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix); 221 example = literalExpression '' 222 { 223 adminAddr = "webmaster@example.org"; 224 forceSSL = true; 225 enableACME = true; 226 } 227 ''; 228 description = '' 229 Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>. 230 ''; 231 }; 232 233 poolConfig = mkOption { 234 type = with types; attrsOf (oneOf [ str int bool ]); 235 default = { 236 "pm" = "dynamic"; 237 "pm.max_children" = 32; 238 "pm.start_servers" = 2; 239 "pm.min_spare_servers" = 2; 240 "pm.max_spare_servers" = 4; 241 "pm.max_requests" = 500; 242 }; 243 description = '' 244 Options for the WordPress PHP pool. See the documentation on <literal>php-fpm.conf</literal> 245 for details on configuration directives. 246 ''; 247 }; 248 249 extraConfig = mkOption { 250 type = types.lines; 251 default = ""; 252 description = '' 253 Any additional text to be appended to the wp-config.php 254 configuration file. This is a PHP script. For configuration 255 settings, see <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php'/>. 256 ''; 257 example = '' 258 define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds 259 ''; 260 }; 261 }; 262 263 config.virtualHost.hostName = mkDefault name; 264 }; 265in 266{ 267 # interface 268 options = { 269 services.wordpress = mkOption { 270 type = types.submodule { 271 # Used to support old interface 272 freeformType = types.attrsOf (types.submodule siteOpts); 273 274 # New interface 275 options.sites = mkOption { 276 type = types.attrsOf (types.submodule siteOpts); 277 default = {}; 278 description = "Specification of one or more WordPress sites to serve"; 279 }; 280 281 options.webserver = mkOption { 282 type = types.enum [ "httpd" "nginx" "caddy" ]; 283 default = "httpd"; 284 description = '' 285 Whether to use apache2 or nginx for virtual host management. 286 287 Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>. 288 See <xref linkend="opt-services.nginx.virtualHosts"/> for further information. 289 290 Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>. 291 See <xref linkend="opt-services.httpd.virtualHosts"/> for further information. 292 ''; 293 }; 294 }; 295 default = {}; 296 description = "Wordpress configuration"; 297 }; 298 299 }; 300 301 # implementation 302 config = mkIf (eachSite != {}) (mkMerge [{ 303 304 assertions = mapAttrsToList (hostName: cfg: 305 { assertion = cfg.database.createLocally -> cfg.database.user == user; 306 message = ''services.wordpress.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned''; 307 } 308 ) eachSite; 309 310 warnings = mapAttrsToList (hostName: _: ''services.wordpress."${hostName}" is deprecated use services.wordpress.sites."${hostName}"'') (oldSites cfg); 311 312 services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) { 313 enable = true; 314 package = mkDefault pkgs.mariadb; 315 ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite; 316 ensureUsers = mapAttrsToList (hostName: cfg: 317 { name = cfg.database.user; 318 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 319 } 320 ) eachSite; 321 }; 322 323 services.phpfpm.pools = mapAttrs' (hostName: cfg: ( 324 nameValuePair "wordpress-${hostName}" { 325 inherit user; 326 group = webserver.group; 327 settings = { 328 "listen.owner" = webserver.user; 329 "listen.group" = webserver.group; 330 } // cfg.poolConfig; 331 } 332 )) eachSite; 333 334 } 335 336 (mkIf (cfg.webserver == "httpd") { 337 services.httpd = { 338 enable = true; 339 extraModules = [ "proxy_fcgi" ]; 340 virtualHosts = mapAttrs (hostName: cfg: mkMerge [ cfg.virtualHost { 341 documentRoot = mkForce "${pkg hostName cfg}/share/wordpress"; 342 extraConfig = '' 343 <Directory "${pkg hostName cfg}/share/wordpress"> 344 <FilesMatch "\.php$"> 345 <If "-f %{REQUEST_FILENAME}"> 346 SetHandler "proxy:unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket}|fcgi://localhost/" 347 </If> 348 </FilesMatch> 349 350 # standard wordpress .htaccess contents 351 <IfModule mod_rewrite.c> 352 RewriteEngine On 353 RewriteBase / 354 RewriteRule ^index\.php$ - [L] 355 RewriteCond %{REQUEST_FILENAME} !-f 356 RewriteCond %{REQUEST_FILENAME} !-d 357 RewriteRule . /index.php [L] 358 </IfModule> 359 360 DirectoryIndex index.php 361 Require all granted 362 Options +FollowSymLinks 363 </Directory> 364 365 # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php 366 <Files wp-config.php> 367 Require all denied 368 </Files> 369 ''; 370 } ]) eachSite; 371 }; 372 }) 373 374 { 375 systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [ 376 "d '${stateDir hostName}' 0750 ${user} ${webserver.group} - -" 377 "d '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -" 378 "Z '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -" 379 ]) eachSite); 380 381 systemd.services = mkMerge [ 382 (mapAttrs' (hostName: cfg: ( 383 nameValuePair "wordpress-init-${hostName}" { 384 wantedBy = [ "multi-user.target" ]; 385 before = [ "phpfpm-wordpress-${hostName}.service" ]; 386 after = optional cfg.database.createLocally "mysql.service"; 387 script = secretsScript (stateDir hostName); 388 389 serviceConfig = { 390 Type = "oneshot"; 391 User = user; 392 Group = webserver.group; 393 }; 394 })) eachSite) 395 396 (optionalAttrs (any (v: v.database.createLocally) (attrValues eachSite)) { 397 httpd.after = [ "mysql.service" ]; 398 }) 399 ]; 400 401 users.users.${user} = { 402 group = webserver.group; 403 isSystemUser = true; 404 }; 405 } 406 407 (mkIf (cfg.webserver == "nginx") { 408 services.nginx = { 409 enable = true; 410 virtualHosts = mapAttrs (hostName: cfg: { 411 serverName = mkDefault hostName; 412 root = "${pkg hostName cfg}/share/wordpress"; 413 extraConfig = '' 414 index index.php; 415 ''; 416 locations = { 417 "/" = { 418 priority = 200; 419 extraConfig = '' 420 try_files $uri $uri/ /index.php$is_args$args; 421 ''; 422 }; 423 "~ \\.php$" = { 424 priority = 500; 425 extraConfig = '' 426 fastcgi_split_path_info ^(.+\.php)(/.+)$; 427 fastcgi_pass unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket}; 428 fastcgi_index index.php; 429 include "${config.services.nginx.package}/conf/fastcgi.conf"; 430 fastcgi_param PATH_INFO $fastcgi_path_info; 431 fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; 432 # Mitigate https://httpoxy.org/ vulnerabilities 433 fastcgi_param HTTP_PROXY ""; 434 fastcgi_intercept_errors off; 435 fastcgi_buffer_size 16k; 436 fastcgi_buffers 4 16k; 437 fastcgi_connect_timeout 300; 438 fastcgi_send_timeout 300; 439 fastcgi_read_timeout 300; 440 ''; 441 }; 442 "~ /\\." = { 443 priority = 800; 444 extraConfig = "deny all;"; 445 }; 446 "~* /(?:uploads|files)/.*\\.php$" = { 447 priority = 900; 448 extraConfig = "deny all;"; 449 }; 450 "~* \\.(js|css|png|jpg|jpeg|gif|ico)$" = { 451 priority = 1000; 452 extraConfig = '' 453 expires max; 454 log_not_found off; 455 ''; 456 }; 457 }; 458 }) eachSite; 459 }; 460 }) 461 462 (mkIf (cfg.webserver == "caddy") { 463 services.caddy = { 464 enable = true; 465 virtualHosts = mapAttrs' (hostName: cfg: ( 466 nameValuePair "http://${hostName}" { 467 extraConfig = '' 468 root * /${pkg hostName cfg}/share/wordpress 469 file_server 470 471 php_fastcgi unix/${config.services.phpfpm.pools."wordpress-${hostName}".socket} 472 473 @uploads { 474 path_regexp path /uploads\/(.*)\.php 475 } 476 rewrite @uploads / 477 478 @wp-admin { 479 path not ^\/wp-admin/* 480 } 481 rewrite @wp-admin {path}/index.php?{query} 482 ''; 483 } 484 )) eachSite; 485 }; 486 }) 487 488 489 ]); 490}