diff options
author | Aaron Andersen <aaron@fosslib.net> | 2019-06-02 10:36:14 -0400 |
---|---|---|
committer | Aaron Andersen <aaron@fosslib.net> | 2019-07-03 11:47:33 -0400 |
commit | aa05aad4702995be7b75e8ac7c21ec3e686b10e9 (patch) | |
tree | 88a7f078fd252a3a3c7673acef38dcc2285eab79 /nixos/modules/services/web-apps/wordpress.nix | |
parent | e1857b78081a65ccf496c9d9e22ec401db526485 (diff) |
nixos/wordpress: create module to replace the httpd subservice
Diffstat (limited to 'nixos/modules/services/web-apps/wordpress.nix')
-rw-r--r-- | nixos/modules/services/web-apps/wordpress.nix | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix new file mode 100644 index 0000000000000..624b0089a0376 --- /dev/null +++ b/nixos/modules/services/web-apps/wordpress.nix @@ -0,0 +1,371 @@ +{ config, pkgs, lib, ... }: + +let + inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types; + inherit (lib) any attrValues concatMapStringsSep flatten literalExample; + inherit (lib) mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString; + + eachSite = config.services.wordpress; + user = "wordpress"; + group = config.services.httpd.group; + stateDir = hostName: "/var/lib/wordpress/${hostName}"; + + pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec { + pname = "wordpress-${hostName}"; + version = src.version; + src = cfg.package; + + installPhase = '' + mkdir -p $out + cp -r * $out/ + + # symlink the wordpress config + ln -s ${wpConfig hostName cfg} $out/share/wordpress/wp-config.php + # symlink uploads directory + ln -s ${cfg.uploadsDir} $out/share/wordpress/wp-content/uploads + + # https://github.com/NixOS/nixpkgs/pull/53399 + # + # Symlinking works for most plugins and themes, but Avada, for instance, fails to + # understand the symlink, causing its file path stripping to fail. This results in + # requests that look like: https://example.com/wp-content//nix/store/...plugin/path/some-file.js + # Since hard linking directories is not allowed, copying is the next best thing. + + # copy additional plugin(s) and theme(s) + ${concatMapStringsSep "\n" (theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${theme.name}") cfg.themes} + ${concatMapStringsSep "\n" (plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${plugin.name}") cfg.plugins} + ''; + }; + + wpConfig = hostName: cfg: pkgs.writeText "wp-config-${hostName}.php" '' + <?php + define('DB_NAME', '${cfg.database.name}'); + define('DB_HOST', '${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}'); + define('DB_USER', '${cfg.database.user}'); + ${optionalString (cfg.database.passwordFile != null) "define('DB_PASSWORD', file_get_contents('${cfg.database.passwordFile}'));"} + define('DB_CHARSET', 'utf8'); + $table_prefix = '${cfg.database.tablePrefix}'; + + require_once('${stateDir hostName}/secret-keys.php'); + + # wordpress is installed onto a read-only file system + define('DISALLOW_FILE_EDIT', true); + define('AUTOMATIC_UPDATER_DISABLED', true); + + ${cfg.extraConfig} + + if ( !defined('ABSPATH') ) + define('ABSPATH', dirname(__FILE__) . '/'); + + require_once(ABSPATH . 'wp-settings.php'); + ?> + ''; + + siteOpts = { lib, name, ... }: + { + options = { + package = mkOption { + type = types.package; + default = pkgs.wordpress; + description = "Which WordPress package to use."; + }; + + uploadsDir = mkOption { + type = types.path; + default = "/var/lib/wordpress/${name}/uploads"; + description = '' + This directory is used for uploads of pictures. The directory passed here is automatically + created and permissions adjusted as required. + ''; + }; + + plugins = mkOption { + type = types.listOf types.path; + default = []; + description = '' + List of path(s) to respective plugin(s) which are copied from the 'plugins' directory. + <note><para>These plugins need to be packaged before use, see example.</para></note> + ''; + example = '' + # Wordpress plugin 'embed-pdf-viewer' installation example + embedPdfViewerPlugin = pkgs.stdenv.mkDerivation { + name = "embed-pdf-viewer-plugin"; + # Download the theme from the wordpress site + src = pkgs.fetchurl { + url = https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip; + sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd"; + }; + # We need unzip to build this package + buildInputs = [ pkgs.unzip ]; + # Installing simply means copying all files to the output directory + installPhase = "mkdir -p $out; cp -R * $out/"; + }; + + And then pass this theme to the themes list like this: + plugins = [ embedPdfViewerPlugin ]; + ''; + }; + + themes = mkOption { + type = types.listOf types.path; + default = []; + description = '' + List of path(s) to respective theme(s) which are copied from the 'theme' directory. + <note><para>These themes need to be packaged before use, see example.</para></note> + ''; + example = '' + # For shits and giggles, let's package the responsive theme + responsiveTheme = pkgs.stdenv.mkDerivation { + name = "responsive-theme"; + # Download the theme from the wordpress site + src = pkgs.fetchurl { + url = https://downloads.wordpress.org/theme/responsive.3.14.zip; + sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3"; + }; + # We need unzip to build this package + buildInputs = [ pkgs.unzip ]; + # Installing simply means copying all files to the output directory + installPhase = "mkdir -p $out; cp -R * $out/"; + }; + + And then pass this theme to the themes list like this: + themes = [ responsiveTheme ]; + ''; + }; + + database = rec { + 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 = "wordpress"; + description = "Database name."; + }; + + user = mkOption { + type = types.str; + default = "wordpress"; + description = "Database user."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/wordpress-dbpassword"; + description = '' + A file containing the password corresponding to + <option>database.user</option>. + ''; + }; + + tablePrefix = mkOption { + type = types.str; + default = "wp_"; + 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 wp_ for your database + prefix. Typically this is changed if you are installing multiple WordPress blogs + in the same database. + + See <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php#table_prefix'/>. + ''; + }; + + socket = mkOption { + type = types.nullOr types.path; + default = null; + defaultText = "/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 ({ + options = import ../web-servers/apache-httpd/per-server-options.nix { + inherit lib; + forMainServer = false; + }; + }); + example = literalExample '' + { + enableSSL = true; + adminAddr = "webmaster@example.org"; + sslServerCert = "/var/lib/acme/wordpress.example.org/full.pem"; + sslServerKey = "/var/lib/acme/wordpress.example.org/key.pem"; + } + ''; + description = '' + Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>. + ''; + }; + + poolConfig = mkOption { + type = types.lines; + 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 WordPress PHP pool. See the documentation on <literal>php-fpm.conf</literal> + for details on configuration directives. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Any additional text to be appended to the wp-config.php + configuration file. This is a PHP script. For configuration + settings, see <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php'/>. + ''; + example = '' + define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds + ''; + }; + }; + + config.virtualHost.hostName = mkDefault name; + }; +in +{ + # interface + options = { + services.wordpress = mkOption { + type = types.attrsOf (types.submodule siteOpts); + default = {}; + description = "Specification of one or more WordPress sites to serve via Apache."; + }; + }; + + # implementation + config = mkIf (eachSite != {}) { + + assertions = mapAttrsToList (hostName: cfg: + { assertion = cfg.database.createLocally -> cfg.database.user == user; + message = "services.wordpress.${hostName}.database.user must be ${user} if the database is to be automatically provisioned"; + } + ) eachSite; + + services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) { + enable = true; + package = mkDefault pkgs.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 "wordpress-${hostName}" { + listen = "/run/phpfpm/wordpress-${hostName}.sock"; + extraConfig = '' + listen.owner = ${config.services.httpd.user} + listen.group = ${config.services.httpd.group} + user = ${user} + group = ${group} + + ${cfg.poolConfig} + ''; + } + )) eachSite; + + services.httpd = { + enable = true; + extraModules = [ "proxy_fcgi" ]; + virtualHosts = mapAttrsToList (hostName: cfg: + (mkMerge [ + cfg.virtualHost { + documentRoot = mkForce "${pkg hostName cfg}/share/wordpress"; + extraConfig = '' + <Directory "${pkg hostName cfg}/share/wordpress"> + <FilesMatch "\.php$"> + <If "-f %{REQUEST_FILENAME}"> + SetHandler "proxy:unix:/run/phpfpm/wordpress-${hostName}.sock|fcgi://localhost/" + </If> + </FilesMatch> + + # standard wordpress .htaccess contents + <IfModule mod_rewrite.c> + RewriteEngine On + RewriteBase / + RewriteRule ^index\.php$ - [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . /index.php [L] + </IfModule> + + DirectoryIndex index.php + Require all granted + Options +FollowSymLinks + </Directory> + + # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php + <Files wp-config.php> + Require all denied + </Files> + ''; + } + ]) + ) eachSite; + }; + + systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [ + "d '${stateDir hostName}' 0750 ${user} ${group} - -" + "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -" + "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -" + ]) eachSite); + + systemd.services = mkMerge [ + (mapAttrs' (hostName: cfg: ( + nameValuePair "wordpress-init-${hostName}" { + wantedBy = [ "multi-user.target" ]; + before = [ "phpfpm-wordpress-${hostName}.service" ]; + after = optional cfg.database.createLocally "mysql.service"; + script = '' + if ! test -e "${stateDir hostName}/secret-keys.php"; then + echo "<?php" >> "${stateDir hostName}/secret-keys.php" + ${pkgs.curl}/bin/curl -s https://api.wordpress.org/secret-key/1.1/salt/ >> "${stateDir hostName}/secret-keys.php" + echo "?>" >> "${stateDir hostName}/secret-keys.php" + chmod 440 "${stateDir hostName}/secret-keys.php" + fi + ''; + + serviceConfig = { + Type = "oneshot"; + User = user; + Group = group; + }; + })) eachSite) + + (optionalAttrs (any (v: v.database.createLocally) (attrValues eachSite)) { + httpd.after = [ "mysql.service" ]; + }) + ]; + + users.users.${user}.group = group; + + }; +} |