From aa05aad4702995be7b75e8ac7c21ec3e686b10e9 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Sun, 2 Jun 2019 10:36:14 -0400 Subject: nixos/wordpress: create module to replace the httpd subservice --- nixos/modules/services/web-apps/wordpress.nix | 371 +++++++++++++++++++++ .../web-servers/apache-httpd/wordpress.nix | 285 ---------------- 2 files changed, 371 insertions(+), 285 deletions(-) create mode 100644 nixos/modules/services/web-apps/wordpress.nix delete mode 100644 nixos/modules/services/web-servers/apache-httpd/wordpress.nix (limited to 'nixos/modules/services') 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" '' + + ''; + + 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. + These plugins need to be packaged before use, see example. + ''; + 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. + These themes need to be packaged before use, see example. + ''; + 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 + . + ''; + }; + + 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 . + ''; + }; + + 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 . + ''; + }; + + 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 php-fpm.conf + 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 . + ''; + 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 = '' + + + + SetHandler "proxy:unix:/run/phpfpm/wordpress-${hostName}.sock|fcgi://localhost/" + + + + # standard wordpress .htaccess contents + + RewriteEngine On + RewriteBase / + RewriteRule ^index\.php$ - [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . /index.php [L] + + + DirectoryIndex index.php + Require all granted + Options +FollowSymLinks + + + # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php + + Require all denied + + ''; + } + ]) + ) 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 "> "${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; + + }; +} diff --git a/nixos/modules/services/web-servers/apache-httpd/wordpress.nix b/nixos/modules/services/web-servers/apache-httpd/wordpress.nix deleted file mode 100644 index 3dddda138fedb..0000000000000 --- a/nixos/modules/services/web-servers/apache-httpd/wordpress.nix +++ /dev/null @@ -1,285 +0,0 @@ -{ config, lib, pkgs, serverInfo, ... }: -# http://codex.wordpress.org/Hardening_WordPress - -with lib; - -let - # Our bare-bones wp-config.php file using the above settings - wordpressConfig = pkgs.writeText "wp-config.php" '' - - RewriteEngine On - RewriteBase / - RewriteRule ^index\.php$ - [L] - - # add a trailing slash to /wp-admin - RewriteRule ^wp-admin$ wp-admin/ [R=301,L] - - RewriteCond %{REQUEST_FILENAME} -f [OR] - RewriteCond %{REQUEST_FILENAME} -d - RewriteRule ^ - [L] - RewriteRule ^(wp-(content|admin|includes).*) $1 [L] - RewriteRule ^(.*\.php)$ $1 [L] - RewriteRule . index.php [L] - - - ${config.extraHtaccess} - ''; - - # WP translation can be found here: - # https://github.com/nixcloud/wordpress-translations - supportedLanguages = { - en_GB = { revision="d6c005372a5318fd758b710b77a800c86518be13"; sha256="0qbbsi87k47q4rgczxx541xz4z4f4fr49hw4lnaxkdsf5maz8p9p"; }; - de_DE = { revision="3c62955c27baaae98fd99feb35593d46562f4736"; sha256="1shndgd11dk836dakrjlg2arwv08vqx6j4xjh4jshvwmjab6ng6p"; }; - zh_ZN = { revision="12b9f811e8cae4b6ee41de343d35deb0a8fdda6d"; sha256="1339ggsxh0g6lab37jmfxicsax4h702rc3fsvv5azs7mcznvwh47"; }; - fr_FR = { revision="688c8b1543e3d38d9e8f57e0a6f2a2c3c8b588bd"; sha256="1j41iak0i6k7a4wzyav0yrllkdjjskvs45w53db8vfm8phq1n014"; }; - }; - - downloadLanguagePack = language: revision: sha256s: - pkgs.stdenv.mkDerivation rec { - name = "wp_${language}"; - src = pkgs.fetchFromGitHub { - owner = "nixcloud"; - repo = "wordpress-translations"; - rev = revision; - sha256 = sha256s; - }; - installPhase = "mkdir -p $out; cp -R * $out/"; - }; - - selectedLanguages = map (lang: downloadLanguagePack lang supportedLanguages.${lang}.revision supportedLanguages.${lang}.sha256) (config.languages); - - # The wordpress package itself - wordpressRoot = pkgs.stdenv.mkDerivation rec { - name = "wordpress"; - src = config.package; - installPhase = '' - mkdir -p $out - # copy all the wordpress files we downloaded - cp -R * $out/ - - # symlink the wordpress config - ln -s ${wordpressConfig} $out/wp-config.php - # symlink custom .htaccess - ln -s ${htaccess} $out/.htaccess - # symlink uploads directory - ln -s ${config.wordpressUploads} $out/wp-content/uploads - - # remove bundled plugins(s) coming with wordpress - rm -Rf $out/wp-content/plugins/* - # remove bundled themes(s) coming with wordpress - rm -Rf $out/wp-content/themes/* - - # copy additional theme(s) - ${concatMapStrings (theme: "cp -r ${theme} $out/wp-content/themes/${theme.name}\n") config.themes} - # copy additional plugin(s) - ${concatMapStrings (plugin: "cp -r ${plugin} $out/wp-content/plugins/${plugin.name}\n") (config.plugins) } - - # symlink additional translation(s) - mkdir -p $out/wp-content/languages - ${concatMapStrings (language: "ln -s ${language}/*.mo ${language}/*.po $out/wp-content/languages/\n") (selectedLanguages) } - ''; - }; - -in - -{ - - # And some httpd extraConfig to make things work nicely - extraConfig = '' - - DirectoryIndex index.php - Allow from * - Options FollowSymLinks - AllowOverride All - - ''; - - enablePHP = true; - - options = { - package = mkOption { - type = types.path; - default = pkgs.wordpress; - description = '' - Path to the wordpress sources. - Upgrading? We have a test! nix-build ./nixos/tests/wordpress.nix - ''; - }; - dbHost = mkOption { - default = "localhost"; - description = "The location of the database server."; - example = "localhost"; - }; - dbName = mkOption { - default = "wordpress"; - description = "Name of the database that holds the Wordpress data."; - example = "localhost"; - }; - dbUser = mkOption { - default = "wordpress"; - description = "The dbUser, read: the username, for the database."; - example = "wordpress"; - }; - dbPassword = mkOption { - default = "wordpress"; - description = '' - The mysql password to the respective dbUser. - - Warning: this password is stored in the world-readable Nix store. It's - recommended to use the $dbPasswordFile option since that gives you control over - the security of the password. $dbPasswordFile also takes precedence over $dbPassword. - ''; - example = "wordpress"; - }; - dbPasswordFile = mkOption { - type = types.str; - default = toString (pkgs.writeTextFile { - name = "wordpress-dbpassword"; - text = config.dbPassword; - }); - example = "/run/keys/wordpress-dbpassword"; - description = '' - Path to a file that contains the mysql password to the respective dbUser. - The file should be readable by the user: config.services.httpd.user. - - $dbPasswordFile takes precedence over the $dbPassword option. - - This defaults to a file in the world-readable Nix store that contains the value - of the $dbPassword option. It's recommended to override this with a path not in - the Nix store. Tip: use nixops key management: - - ''; - }; - tablePrefix = mkOption { - 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 . - ''; - }; - wordpressUploads = mkOption { - default = "/data/uploads"; - description = '' - This directory is used for uploads of pictures and must be accessible (read: owned) by the httpd running user. The directory passed here is automatically created and permissions are given to the httpd running user. - ''; - }; - plugins = mkOption { - default = []; - type = types.listOf types.path; - description = - '' - List of path(s) to respective plugin(s) which are symlinked from the 'plugins' directory. Note: These plugins need to be packaged before use, see example. - ''; - example = '' - # Wordpress plugin 'akismet' installation example - akismetPlugin = pkgs.stdenv.mkDerivation { - name = "akismet-plugin"; - # Download the theme from the wordpress site - src = pkgs.fetchurl { - url = https://downloads.wordpress.org/plugin/akismet.3.1.zip; - sha256 = "1i4k7qyzna08822ncaz5l00wwxkwcdg4j9h3z2g0ay23q640pclg"; - }; - # 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 = [ akismetPlugin ]; - ''; - }; - themes = mkOption { - default = []; - type = types.listOf types.path; - description = - '' - List of path(s) to respective theme(s) which are symlinked from the 'theme' directory. Note: These themes need to be packaged before use, see example. - ''; - 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 = http://wordpress.org/themes/download/responsive.1.9.7.6.zip; - sha256 = "06i26xlc5kdnx903b1gfvnysx49fb4kh4pixn89qii3a30fgd8r8"; - }; - # 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 ]; - ''; - }; - languages = mkOption { - default = []; - description = "Installs wordpress language packs based on the list, see wordpress.nix for possible translations."; - example = "[ \"en_GB\" \"de_DE\" ];"; - }; - extraConfig = mkOption { - type = types.lines; - default = ""; - example = - '' - define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds - ''; - description = '' - Any additional text to be appended to Wordpress's wp-config.php - configuration file. This is a PHP script. For configuration - settings, see . - ''; - }; - extraHtaccess = mkOption { - default = ""; - example = - '' - php_value upload_max_filesize 20M - php_value post_max_size 20M - ''; - description = '' - Any additional text to be appended to Wordpress's .htaccess file. - ''; - }; - }; - - documentRoot = wordpressRoot; - - # FIXME adding the user has to be done manually for the time being - startupScript = pkgs.writeScript "init-wordpress.sh" '' - #!/bin/sh - mkdir -p ${config.wordpressUploads} - chown ${serverInfo.serverConfig.user} ${config.wordpressUploads} - - # we should use systemd dependencies here - if [ ! -d ${serverInfo.fullConfig.services.mysql.dataDir}/${config.dbName} ]; then - echo "Need to create the database '${config.dbName}' and grant permissions to user named '${config.dbUser}'." - # Wait until MySQL is up - while [ ! -S /run/mysqld/mysqld.sock ]; do - sleep 1 - done - ${pkgs.mysql}/bin/mysql -e 'CREATE DATABASE ${config.dbName};' - ${pkgs.mysql}/bin/mysql -e "GRANT ALL ON ${config.dbName}.* TO ${config.dbUser}@localhost IDENTIFIED BY \"$(cat ${config.dbPasswordFile})\";" - else - echo "Good, no need to do anything database related." - fi - ''; -} -- cgit 1.4.1