at 21.11-pre 13 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 literalExample; 6 inherit (lib) mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString; 7 8 eachSite = config.services.wordpress; 9 user = "wordpress"; 10 group = config.services.httpd.group; 11 stateDir = hostName: "/var/lib/wordpress/${hostName}"; 12 13 pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec { 14 pname = "wordpress-${hostName}"; 15 version = src.version; 16 src = cfg.package; 17 18 installPhase = '' 19 mkdir -p $out 20 cp -r * $out/ 21 22 # symlink the wordpress config 23 ln -s ${wpConfig hostName cfg} $out/share/wordpress/wp-config.php 24 # symlink uploads directory 25 ln -s ${cfg.uploadsDir} $out/share/wordpress/wp-content/uploads 26 27 # https://github.com/NixOS/nixpkgs/pull/53399 28 # 29 # Symlinking works for most plugins and themes, but Avada, for instance, fails to 30 # understand the symlink, causing its file path stripping to fail. This results in 31 # requests that look like: https://example.com/wp-content//nix/store/...plugin/path/some-file.js 32 # Since hard linking directories is not allowed, copying is the next best thing. 33 34 # copy additional plugin(s) and theme(s) 35 ${concatMapStringsSep "\n" (theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${theme.name}") cfg.themes} 36 ${concatMapStringsSep "\n" (plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${plugin.name}") cfg.plugins} 37 ''; 38 }; 39 40 wpConfig = hostName: cfg: pkgs.writeText "wp-config-${hostName}.php" '' 41 <?php 42 define('DB_NAME', '${cfg.database.name}'); 43 define('DB_HOST', '${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}'); 44 define('DB_USER', '${cfg.database.user}'); 45 ${optionalString (cfg.database.passwordFile != null) "define('DB_PASSWORD', file_get_contents('${cfg.database.passwordFile}'));"} 46 define('DB_CHARSET', 'utf8'); 47 $table_prefix = '${cfg.database.tablePrefix}'; 48 49 require_once('${stateDir hostName}/secret-keys.php'); 50 51 # wordpress is installed onto a read-only file system 52 define('DISALLOW_FILE_EDIT', true); 53 define('AUTOMATIC_UPDATER_DISABLED', true); 54 55 ${cfg.extraConfig} 56 57 if ( !defined('ABSPATH') ) 58 define('ABSPATH', dirname(__FILE__) . '/'); 59 60 require_once(ABSPATH . 'wp-settings.php'); 61 ?> 62 ''; 63 64 secretsVars = [ "AUTH_KEY" "SECURE_AUTH_KEY" "LOOGGED_IN_KEY" "NONCE_KEY" "AUTH_SALT" "SECURE_AUTH_SALT" "LOGGED_IN_SALT" "NONCE_SALT" ]; 65 secretsScript = hostStateDir: '' 66 if ! test -e "${hostStateDir}/secret-keys.php"; then 67 umask 0177 68 echo "<?php" >> "${hostStateDir}/secret-keys.php" 69 ${concatMapStringsSep "\n" (var: '' 70 echo "define('${var}', '`tr -dc a-zA-Z0-9 </dev/urandom | head -c 64`');" >> "${hostStateDir}/secret-keys.php" 71 '') secretsVars} 72 echo "?>" >> "${hostStateDir}/secret-keys.php" 73 chmod 440 "${hostStateDir}/secret-keys.php" 74 fi 75 ''; 76 77 siteOpts = { lib, name, ... }: 78 { 79 options = { 80 package = mkOption { 81 type = types.package; 82 default = pkgs.wordpress; 83 description = "Which WordPress package to use."; 84 }; 85 86 uploadsDir = mkOption { 87 type = types.path; 88 default = "/var/lib/wordpress/${name}/uploads"; 89 description = '' 90 This directory is used for uploads of pictures. The directory passed here is automatically 91 created and permissions adjusted as required. 92 ''; 93 }; 94 95 plugins = mkOption { 96 type = types.listOf types.path; 97 default = []; 98 description = '' 99 List of path(s) to respective plugin(s) which are copied from the 'plugins' directory. 100 <note><para>These plugins need to be packaged before use, see example.</para></note> 101 ''; 102 example = '' 103 # Wordpress plugin 'embed-pdf-viewer' installation example 104 embedPdfViewerPlugin = pkgs.stdenv.mkDerivation { 105 name = "embed-pdf-viewer-plugin"; 106 # Download the theme from the wordpress site 107 src = pkgs.fetchurl { 108 url = "https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip"; 109 sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd"; 110 }; 111 # We need unzip to build this package 112 nativeBuildInputs = [ pkgs.unzip ]; 113 # Installing simply means copying all files to the output directory 114 installPhase = "mkdir -p $out; cp -R * $out/"; 115 }; 116 117 And then pass this theme to the themes list like this: 118 plugins = [ embedPdfViewerPlugin ]; 119 ''; 120 }; 121 122 themes = mkOption { 123 type = types.listOf types.path; 124 default = []; 125 description = '' 126 List of path(s) to respective theme(s) which are copied from the 'theme' directory. 127 <note><para>These themes need to be packaged before use, see example.</para></note> 128 ''; 129 example = '' 130 # Let's package the responsive theme 131 responsiveTheme = pkgs.stdenv.mkDerivation { 132 name = "responsive-theme"; 133 # Download the theme from the wordpress site 134 src = pkgs.fetchurl { 135 url = "https://downloads.wordpress.org/theme/responsive.3.14.zip"; 136 sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3"; 137 }; 138 # We need unzip to build this package 139 nativeBuildInputs = [ pkgs.unzip ]; 140 # Installing simply means copying all files to the output directory 141 installPhase = "mkdir -p $out; cp -R * $out/"; 142 }; 143 144 And then pass this theme to the themes list like this: 145 themes = [ responsiveTheme ]; 146 ''; 147 }; 148 149 database = { 150 host = mkOption { 151 type = types.str; 152 default = "localhost"; 153 description = "Database host address."; 154 }; 155 156 port = mkOption { 157 type = types.port; 158 default = 3306; 159 description = "Database host port."; 160 }; 161 162 name = mkOption { 163 type = types.str; 164 default = "wordpress"; 165 description = "Database name."; 166 }; 167 168 user = mkOption { 169 type = types.str; 170 default = "wordpress"; 171 description = "Database user."; 172 }; 173 174 passwordFile = mkOption { 175 type = types.nullOr types.path; 176 default = null; 177 example = "/run/keys/wordpress-dbpassword"; 178 description = '' 179 A file containing the password corresponding to 180 <option>database.user</option>. 181 ''; 182 }; 183 184 tablePrefix = mkOption { 185 type = types.str; 186 default = "wp_"; 187 description = '' 188 The $table_prefix is the value placed in the front of your database tables. 189 Change the value if you want to use something other than wp_ for your database 190 prefix. Typically this is changed if you are installing multiple WordPress blogs 191 in the same database. 192 193 See <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php#table_prefix'/>. 194 ''; 195 }; 196 197 socket = mkOption { 198 type = types.nullOr types.path; 199 default = null; 200 defaultText = "/run/mysqld/mysqld.sock"; 201 description = "Path to the unix socket file to use for authentication."; 202 }; 203 204 createLocally = mkOption { 205 type = types.bool; 206 default = true; 207 description = "Create the database and database user locally."; 208 }; 209 }; 210 211 virtualHost = mkOption { 212 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix); 213 example = literalExample '' 214 { 215 adminAddr = "webmaster@example.org"; 216 forceSSL = true; 217 enableACME = true; 218 } 219 ''; 220 description = '' 221 Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>. 222 ''; 223 }; 224 225 poolConfig = mkOption { 226 type = with types; attrsOf (oneOf [ str int bool ]); 227 default = { 228 "pm" = "dynamic"; 229 "pm.max_children" = 32; 230 "pm.start_servers" = 2; 231 "pm.min_spare_servers" = 2; 232 "pm.max_spare_servers" = 4; 233 "pm.max_requests" = 500; 234 }; 235 description = '' 236 Options for the WordPress PHP pool. See the documentation on <literal>php-fpm.conf</literal> 237 for details on configuration directives. 238 ''; 239 }; 240 241 extraConfig = mkOption { 242 type = types.lines; 243 default = ""; 244 description = '' 245 Any additional text to be appended to the wp-config.php 246 configuration file. This is a PHP script. For configuration 247 settings, see <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php'/>. 248 ''; 249 example = '' 250 define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds 251 ''; 252 }; 253 }; 254 255 config.virtualHost.hostName = mkDefault name; 256 }; 257in 258{ 259 # interface 260 options = { 261 services.wordpress = mkOption { 262 type = types.attrsOf (types.submodule siteOpts); 263 default = {}; 264 description = "Specification of one or more WordPress sites to serve via Apache."; 265 }; 266 }; 267 268 # implementation 269 config = mkIf (eachSite != {}) { 270 271 assertions = mapAttrsToList (hostName: cfg: 272 { assertion = cfg.database.createLocally -> cfg.database.user == user; 273 message = "services.wordpress.${hostName}.database.user must be ${user} if the database is to be automatically provisioned"; 274 } 275 ) eachSite; 276 277 services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) { 278 enable = true; 279 package = mkDefault pkgs.mariadb; 280 ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite; 281 ensureUsers = mapAttrsToList (hostName: cfg: 282 { name = cfg.database.user; 283 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 284 } 285 ) eachSite; 286 }; 287 288 services.phpfpm.pools = mapAttrs' (hostName: cfg: ( 289 nameValuePair "wordpress-${hostName}" { 290 inherit user group; 291 settings = { 292 "listen.owner" = config.services.httpd.user; 293 "listen.group" = config.services.httpd.group; 294 } // cfg.poolConfig; 295 } 296 )) eachSite; 297 298 services.httpd = { 299 enable = true; 300 extraModules = [ "proxy_fcgi" ]; 301 virtualHosts = mapAttrs (hostName: cfg: mkMerge [ cfg.virtualHost { 302 documentRoot = mkForce "${pkg hostName cfg}/share/wordpress"; 303 extraConfig = '' 304 <Directory "${pkg hostName cfg}/share/wordpress"> 305 <FilesMatch "\.php$"> 306 <If "-f %{REQUEST_FILENAME}"> 307 SetHandler "proxy:unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket}|fcgi://localhost/" 308 </If> 309 </FilesMatch> 310 311 # standard wordpress .htaccess contents 312 <IfModule mod_rewrite.c> 313 RewriteEngine On 314 RewriteBase / 315 RewriteRule ^index\.php$ - [L] 316 RewriteCond %{REQUEST_FILENAME} !-f 317 RewriteCond %{REQUEST_FILENAME} !-d 318 RewriteRule . /index.php [L] 319 </IfModule> 320 321 DirectoryIndex index.php 322 Require all granted 323 Options +FollowSymLinks 324 </Directory> 325 326 # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php 327 <Files wp-config.php> 328 Require all denied 329 </Files> 330 ''; 331 } ]) eachSite; 332 }; 333 334 systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [ 335 "d '${stateDir hostName}' 0750 ${user} ${group} - -" 336 "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -" 337 "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -" 338 ]) eachSite); 339 340 systemd.services = mkMerge [ 341 (mapAttrs' (hostName: cfg: ( 342 nameValuePair "wordpress-init-${hostName}" { 343 wantedBy = [ "multi-user.target" ]; 344 before = [ "phpfpm-wordpress-${hostName}.service" ]; 345 after = optional cfg.database.createLocally "mysql.service"; 346 script = secretsScript (stateDir hostName); 347 348 serviceConfig = { 349 Type = "oneshot"; 350 User = user; 351 Group = group; 352 }; 353 })) eachSite) 354 355 (optionalAttrs (any (v: v.database.createLocally) (attrValues eachSite)) { 356 httpd.after = [ "mysql.service" ]; 357 }) 358 ]; 359 360 users.users.${user} = { 361 group = group; 362 isSystemUser = true; 363 }; 364 365 }; 366}