From 94d3c3c65c57d46f7561e5d718537c8b52aad3ef Mon Sep 17 00:00:00 2001 From: toastal Date: Mon, 18 Mar 2024 14:08:46 +0700 Subject: [PATCH 1/6] movim: add bin script helper --- pkgs/by-name/mo/movim/package.nix | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkgs/by-name/mo/movim/package.nix b/pkgs/by-name/mo/movim/package.nix index 600af837b107..0fb4851aa109 100644 --- a/pkgs/by-name/mo/movim/package.nix +++ b/pkgs/by-name/mo/movim/package.nix @@ -1,5 +1,6 @@ { lib , fetchFromGitHub +, dash , php , phpCfg ? null , withPgsql ? true # “strongly recommended” according to docs @@ -34,10 +35,24 @@ php.buildComposerProject (finalAttrs: { vendorHash = "sha256-PBoJbVuF0Qy7nNlL4yx446ivlZpPYNIai78yC0wWkCM="; + postInstall = '' + mkdir -p $out/bin + echo "#!${lib.getExe dash}" > $out/bin/movim + echo "${lib.getExe finalAttrs.php} $out/share/php/${finalAttrs.pname}/daemon.php \"\$@\"" >> $out/bin/movim + chmod +x $out/bin/movim + + mkdir -p $out/share/{bash-completion/completion,fish/vendor_completions.d,zsh/site-functions} + $out/bin/movim completion bash | sed "s/daemon.php/movim/g" > $out/share/bash-completion/completion/movim.bash + $out/bin/movim completion fish | sed "s/daemon.php/movim/g" > $out/share/fish/vendor_completions.d/movim.fish + $out/bin/movim completion zsh | sed "s/daemon.php/movim/g" > $out/share/zsh/site-functions/_movim + chmod +x $out/share/{bash-completion/completion/movim.bash,fish/vendor_completions.d/movim.fish,zsh/site-functions/_movim} + ''; + meta = { description = "a federated blogging & chat platform that acts as a web front end for the XMPP protocol"; homepage = "https://movim.eu"; license = lib.licenses.agpl3Plus; maintainers = with lib.maintainers; [ toastal ]; + mainProgram = "movim"; }; }) From c10316a4f6337a4ef58902784fae1ecc37c8daf4 Mon Sep 17 00:00:00 2001 From: toastal Date: Fri, 22 Mar 2024 23:48:53 +0700 Subject: [PATCH 2/6] movim: ImageMagick fix --- pkgs/by-name/mo/movim/package.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkgs/by-name/mo/movim/package.nix b/pkgs/by-name/mo/movim/package.nix index 0fb4851aa109..658e15916b7e 100644 --- a/pkgs/by-name/mo/movim/package.nix +++ b/pkgs/by-name/mo/movim/package.nix @@ -35,6 +35,14 @@ php.buildComposerProject (finalAttrs: { vendorHash = "sha256-PBoJbVuF0Qy7nNlL4yx446ivlZpPYNIai78yC0wWkCM="; + postPatch = '' + # BUGFIX: Imagick API Changes for 7.x+ + # See additionally: https://github.com/movim/movim/pull/1122 + substituteInPlace src/Movim/Image.php \ + --replace-fail "Imagick::ALPHACHANNEL_REMOVE" "Imagick::ALPHACHANNEL_OFF" \ + --replace-fail "Imagick::ALPHACHANNEL_ACTIVATE" "Imagick::ALPHACHANNEL_ON" + ''; + postInstall = '' mkdir -p $out/bin echo "#!${lib.getExe dash}" > $out/bin/movim From 47918937b8b017764161116c0346a9f09f3f83ab Mon Sep 17 00:00:00 2001 From: toastal Date: Fri, 22 Mar 2024 23:15:47 +0700 Subject: [PATCH 3/6] movim: update to latest (includes NixOS patches) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version is ‘weird’ to deal with the composer script --- pkgs/by-name/mo/movim/package.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkgs/by-name/mo/movim/package.nix b/pkgs/by-name/mo/movim/package.nix index 658e15916b7e..0225dbc98018 100644 --- a/pkgs/by-name/mo/movim/package.nix +++ b/pkgs/by-name/mo/movim/package.nix @@ -9,13 +9,13 @@ php.buildComposerProject (finalAttrs: { pname = "movim"; - version = "0.23"; + version = "0.23.0.20240328"; src = fetchFromGitHub { owner = "movim"; repo = "movim"; - rev = "v${finalAttrs.version}"; - hash = "sha256-9MBe2IRYxvUuCc5m7ajvIlBU7YVm4A3RABlOOIjpKoM="; + rev = "c3a43cd7e3a1a3a6efd595470e6a85b2ec578cba"; + hash = "sha256-x0C4w3SRP3NMOhGSZOQALk6PNWUre4MvFW5cESr8Wvk="; }; php = php.buildEnv ({ @@ -33,7 +33,7 @@ php.buildComposerProject (finalAttrs: { # pinned commonmark composerStrictValidation = false; - vendorHash = "sha256-PBoJbVuF0Qy7nNlL4yx446ivlZpPYNIai78yC0wWkCM="; + vendorHash = "sha256-RFIi1I+gcagRgkDpgQeR1oGJeBGA7z9q3DCfW+ZDr2Y="; postPatch = '' # BUGFIX: Imagick API Changes for 7.x+ From fcc7c53e9c833a9ee40b790c62bcbc0543170d50 Mon Sep 17 00:00:00 2001 From: toastal Date: Fri, 22 Mar 2024 23:49:50 +0700 Subject: [PATCH 4/6] nixos/movim: add service module --- nixos/modules/module-list.nix | 1 + nixos/modules/services/web-apps/movim.nix | 602 ++++++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/web-apps/movim/default.nix | 8 + nixos/tests/web-apps/movim/standard.nix | 102 ++++ pkgs/by-name/mo/movim/package.nix | 20 + 6 files changed, 734 insertions(+) create mode 100644 nixos/modules/services/web-apps/movim.nix create mode 100644 nixos/tests/web-apps/movim/default.nix create mode 100644 nixos/tests/web-apps/movim/standard.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 9fc036f9213a..db0f0f0870e7 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1363,6 +1363,7 @@ ./services/web-apps/miniflux.nix ./services/web-apps/monica.nix ./services/web-apps/moodle.nix + ./services/web-apps/movim.nix ./services/web-apps/netbox.nix ./services/web-apps/nextcloud.nix ./services/web-apps/nextcloud-notify_push.nix diff --git a/nixos/modules/services/web-apps/movim.nix b/nixos/modules/services/web-apps/movim.nix new file mode 100644 index 000000000000..c9314e28e949 --- /dev/null +++ b/nixos/modules/services/web-apps/movim.nix @@ -0,0 +1,602 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) + filterAttrsRecursive + generators + literalExpression + mkDefault + mkIf + mkOption + mkEnableOption + mkPackageOption + mkMerge + pipe + types + ; + + cfg = config.services.movim; + + defaultPHPCfg = { + "output_buffering" = 0; + "error_reporting" = "E_ALL & ~E_DEPRECATED & ~E_STRICT"; + "opcache.enable_cli" = 1; + "opcache.interned_strings_buffer" = 8; + "opcache.max_accelerated_files" = 6144; + "opcache.memory_consumption" = 128; + "opcache.revalidate_freq" = 2; + "opcache.fast_shutdown" = 1; + }; + + phpCfg = generators.toKeyValue + { mkKeyValue = generators.mkKeyValueDefault { } " = "; } + (defaultPHPCfg // cfg.phpCfg); + + podConfigFlags = + let + bevalue = a: lib.escapeShellArg (generators.mkValueStringDefault { } a); + in + lib.concatStringsSep " " + (lib.attrsets.foldlAttrs + (acc: k: v: acc ++ lib.optional (v != null) "--${k}=${bevalue v}") + [ ] + cfg.podConfig); + + package = + let + p = cfg.package.override { + inherit phpCfg; + withPgsql = cfg.database.type == "pgsql"; + withMysql = cfg.database.type == "mysql"; + }; + in + p.overrideAttrs (finalAttrs: prevAttrs: + let + appDir = "$out/share/php/${finalAttrs.pname}"; + + stateDirectories = '' + # Symlinking in our state directories + rm -rf $out/.env $out/cache ${appDir}/public/cache + ln -s ${cfg.dataDir}/.env ${appDir}/.env + ln -s ${cfg.dataDir}/public/cache ${appDir}/public/cache + ln -s ${cfg.logDir} ${appDir}/log + ln -s ${cfg.runtimeDir}/cache ${appDir}/cache + ''; + + exposeComposer = '' + # Expose PHP Composer for scripts + mkdir -p $out/bin + echo "#!${lib.getExe pkgs.dash}" > $out/bin/movim-composer + echo "${finalAttrs.php.packages.composer}/bin/composer --working-dir="${appDir}" \"\$@\"" >> $out/bin/movim-composer + chmod +x $out/bin/movim-composer + ''; + + podConfigInputDisableReplace = lib.optionalString (podConfigFlags != "") + (lib.concatStringsSep "\n" + (lib.attrsets.foldlAttrs + (acc: k: v: + acc ++ lib.optional (v != null) + # Disable all Admin panel options that were set in the + # `cfg.podConfig` to prevent confusing situtions where the + # values are rewritten on server reboot + '' + substituteInPlace ${appDir}/app/widgets/AdminMain/adminmain.tpl \ + --replace-warn 'name="${k}"' 'name="${k}" disabled' + '') + [ ] + cfg.podConfig)); + in + { + postInstall = lib.concatStringsSep "\n\n" [ + prevAttrs.postInstall + stateDirectories + exposeComposer + podConfigInputDisableReplace + ]; + }); + + configFile = pipe cfg.settings [ + (filterAttrsRecursive (_: v: v != null)) + (generators.toKeyValue { }) + (pkgs.writeText "movim-env") + ]; + + pool = "movim"; + fpm = config.services.phpfpm.pools.${pool}; + phpExecutionUnit = "phpfpm-${pool}"; + + dbService = { + "postgresql" = "postgresql.service"; + "mysql" = "mysql.service"; + }.${cfg.database.type}; +in +{ + options.services = { + movim = { + enable = mkEnableOption "a Movim instance"; + package = mkPackageOption pkgs "movim" { }; + phpPackage = mkPackageOption pkgs "php" { }; + + phpCfg = mkOption { + type = with types; attrsOf (oneOf [ int str bool ]); + defaultText = literalExpression (generators.toPretty { } defaultPHPCfg); + default = { }; + description = "Extra PHP INI options such as `memory_limit`, `max_execution_time`, etc."; + }; + + user = mkOption { + type = types.nonEmptyStr; + default = "movim"; + description = "User running Movim service"; + }; + + group = mkOption { + type = types.nonEmptyStr; + default = "movim"; + description = "Group running Movim service"; + }; + + dataDir = mkOption { + type = types.nonEmptyStr; + default = "/var/lib/movim"; + description = "State directory of the `movim` user which holds the application’s state & data."; + }; + + logDir = mkOption { + type = types.nonEmptyStr; + default = "/var/log/movim"; + description = "Log directory of the `movim` user which holds the application’s logs."; + }; + + runtimeDir = mkOption { + type = types.nonEmptyStr; + default = "/run/movim"; + description = "Runtime directory of the `movim` user which holds the application’s caches & temporary files."; + }; + + domain = mkOption { + type = types.nonEmptyStr; + description = "Fully-qualified domain name (FQDN) for the Movim instance."; + }; + + port = mkOption { + type = types.port; + default = 8080; + description = "Movim daemon port."; + }; + + debug = mkOption { + type = types.bool; + default = false; + description = "Debugging logs."; + }; + + verbose = mkOption { + type = types.bool; + default = false; + description = "Verbose logs."; + }; + + podConfig = mkOption { + type = types.submodule { + options = { + info = mkOption { + type = with types; nullOr str; + default = null; + description = "Content of the info box on the login page"; + }; + + description = mkOption { + type = with types; nullOr str; + default = null; + description = "General description of the instance"; + }; + + timezone = mkOption { + type = with types; nullOr str; + default = null; + description = "The server timezone"; + }; + + restrictsuggestions = mkOption { + type = with types; nullOr bool; + default = null; + description = "Only suggest chatrooms, Communities and other contents that are available on the user XMPP server and related services"; + }; + + chatonly = mkOption { + type = with types; nullOr bool; + default = null; + description = "Disable all the social feature (Communities, Blog…) and keep only the chat ones"; + }; + + disableregistration = mkOption { + type = with types; nullOr bool; + default = null; + description = "Remove the XMPP registration flow and buttons from the interface"; + }; + + loglevel = mkOption { + type = with types; nullOr (ints.between 0 3); + default = null; + description = "The server loglevel"; + }; + + locale = mkOption { + type = with types; nullOr str; + default = null; + description = "The server main locale"; + }; + + xmppdomain = mkOption { + type = with types; nullOr str; + default = null; + description = "The default XMPP server domain"; + }; + + xmppdescription = mkOption { + type = with types; nullOr str; + default = null; + description = "The default XMPP server description"; + }; + + xmppwhitelist = mkOption { + type = with types; nullOr str; + default = null; + description = "The allowlisted XMPP servers"; + }; + }; + }; + default = { }; + description = '' + Pod configuration (values from `php daemon.php config --help`). + Note that these values will now be disabled in the admin panel. + ''; + }; + + settings = mkOption { + type = with types; attrsOf (nullOr (oneOf [ int str bool ])); + default = { }; + description = ".env settings for Movim. Secrets should use `secretFile` option instead. `null`s will be culled."; + }; + + secretFile = mkOption { + type = with types; nullOr path; + default = null; + description = "The secret file to be sourced for the .env settings."; + }; + + database = { + type = mkOption { + type = types.enum [ "mysql" "postgresql" ]; + example = "mysql"; + default = "postgresql"; + description = "Database engine to use."; + }; + + name = mkOption { + type = types.str; + default = "movim"; + description = "Database name."; + }; + + user = mkOption { + type = types.str; + default = "movim"; + description = "Database username."; + }; + + createLocally = mkOption { + type = types.bool; + default = true; + description = "local database using UNIX socket authentication"; + }; + }; + + nginx = mkOption { + type = with types; nullOr (submodule + (import ../web-servers/nginx/vhost-options.nix { + inherit config lib; + })); + default = null; + example = lib.literalExpression /* nginx */ '' + { + serverAliases = [ + "pics.''${config.networking.domain}" + ]; + enableACME = true; + forceHttps = true; + } + ''; + description = '' + With this option, you can customize an nginx virtual host which already has sensible defaults for Movim. + Set to `{ }` if you do not need any customization to the virtual host. + If enabled, then by default, the {option}`serverName` is `''${domain}`, + If this is set to null (the default), no nginx virtualHost will be configured. + ''; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [ int str bool ]); + default = { }; + description = "Options for Movim’s PHP-FPM pool."; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + + users = { + users = { + movim = mkIf (cfg.user == "movim") { + isSystemUser = true; + group = cfg.group; + }; + "${config.services.nginx.user}".extraGroups = [ cfg.group ]; + }; + groups = { + ${cfg.group} = { }; + }; + }; + + services = { + movim = { + settings = mkMerge [ + { + DAEMON_URL = "//${cfg.domain}"; + DAEMON_PORT = cfg.port; + DAEMON_INTERFACE = "127.0.0.1"; + DAEMON_DEBUG = cfg.debug; + DAEMON_VERBOSE = cfg.verbose; + } + (mkIf cfg.database.createLocally { + DB_DRIVER = { + "postgresql" = "pgsql"; + "mysql" = "mysql"; + }.${cfg.database.type}; + DB_HOST = "localhost"; + DB_PORT = config.services.${cfg.database.type}.settings.port; + DB_DATABASE = cfg.database.name; + DB_USERNAME = cfg.database.user; + DB_PASSWORD = ""; + }) + ]; + + poolConfig = lib.mapAttrs' (n: v: lib.nameValuePair n (lib.mkDefault v)) { + "pm" = "dynamic"; + "php_admin_value[error_log]" = "stderr"; + "php_admin_flag[log_errors]" = true; + "catch_workers_output" = true; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 8; + "pm.max_requests" = 500; + }; + }; + + nginx = mkIf (cfg.nginx != null) { + enable = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedBrotliSettings = true; + recommendedProxySettings = true; + # TODO: recommended cache options already in Nginx⁇ + appendHttpConfig = /* nginx */ '' + fastcgi_cache_path /tmp/nginx_cache levels=1:2 keys_zone=nginx_cache:100m inactive=60m; + fastcgi_cache_key "$scheme$request_method$host$request_uri"; + ''; + virtualHosts."${cfg.domain}" = mkMerge [ + cfg.nginx + { + root = lib.mkForce "${package}/share/php/movim/public"; + locations = { + "/favicon.ico" = { + priority = 100; + extraConfig = /* nginx */ '' + access_log off; + log_not_found off; + ''; + }; + "/robots.txt" = { + priority = 100; + extraConfig = /* nginx */ '' + access_log off; + log_not_found off; + ''; + }; + "~ /\\.(?!well-known).*" = { + priority = 210; + extraConfig = /* nginx */ '' + deny all; + ''; + }; + # Ask nginx to cache every URL starting with "/picture" + "/picture" = { + priority = 400; + tryFiles = "$uri $uri/ /index.php$is_args$args"; + extraConfig = /* nginx */ '' + set $no_cache 0; # Enable cache only there + ''; + }; + "/" = { + priority = 490; + tryFiles = "$uri $uri/ /index.php$is_args$args"; + extraConfig = /* nginx */ '' + # https://github.com/movim/movim/issues/314 + add_header Content-Security-Policy "default-src 'self'; img-src 'self' aesgcm: https:; media-src 'self' aesgcm: https:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"; + set $no_cache 1; + ''; + }; + "~ \\.php$" = { + priority = 500; + tryFiles = "$uri =404"; + extraConfig = /* nginx */ '' + include ${config.services.nginx.package}/conf/fastcgi.conf; + add_header X-Cache $upstream_cache_status; + fastcgi_ignore_headers "Cache-Control" "Expires" "Set-Cookie"; + fastcgi_cache nginx_cache; + fastcgi_cache_valid any 7d; + fastcgi_cache_bypass $no_cache; + fastcgi_no_cache $no_cache; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_index index.php; + fastcgi_pass unix:${fpm.socket}; + ''; + }; + "/ws/" = { + priority = 900; + proxyPass = "http://${cfg.settings.DAEMON_INTERFACE}:${builtins.toString cfg.port}/"; + proxyWebsockets = true; + recommendedProxySettings = true; + extraConfig = /* nginx */ '' + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + ''; + }; + }; + extraConfig = /* ngnix */ '' + index index.php; + ''; + } + ]; + }; + + mysql = mkIf (cfg.database.createLocally && cfg.database.type == "mysql") { + enable = mkDefault true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [{ + name = cfg.user; + ensureDBOwnership = true; + }]; + }; + + postgresql = mkIf (cfg.database.createLocally && cfg.database.type == "postgresql") { + enable = mkDefault true; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [{ + name = cfg.user; + ensureDBOwnership = true; + }]; + authentication = '' + host ${cfg.database.name} ${cfg.database.user} localhost trust + ''; + }; + + phpfpm.pools.${pool} = + let + socketOwner = + if (cfg.nginx != null) + then config.services.nginx.user + else cfg.user; + in + { + phpPackage = package.php; + user = cfg.user; + group = cfg.group; + + phpOptions = '' + error_log = 'stderr' + log_errors = on + ''; + + settings = { + "listen.owner" = socketOwner; + "listen.group" = cfg.group; + "listen.mode" = "0660"; + "catch_workers_output" = true; + } // cfg.poolConfig; + }; + }; + + systemd = { + services.movim-data-setup = { + description = "Movim setup: .env file, databases init, cache reload"; + wantedBy = [ "multi-user.target" ]; + requiredBy = [ "${phpExecutionUnit}.service" ]; + before = [ "${phpExecutionUnit}.service" ]; + after = lib.optional cfg.database.createLocally dbService; + requires = lib.optional cfg.database.createLocally dbService; + + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + UMask = "077"; + } // lib.optionalAttrs (cfg.secretFile != null) { + LoadCredential = "env-secrets:${cfg.secretFile}"; + }; + + script = '' + # Env vars + rm -f ${cfg.dataDir}/.env + cp --no-preserve=all ${configFile} ${cfg.dataDir}/.env + echo -e '\n' >> ${cfg.dataDir}/.env + if [[ -f "$CREDENTIALS_DIRECTORY/env-secrets" ]]; then + cat "$CREDENTIALS_DIRECTORY/env-secrets" >> ${cfg.dataDir}/.env + echo -e '\n' >> ${cfg.dataDir}/.env + fi + + # Caches, logs + mkdir -p ${cfg.dataDir}/public/cache ${cfg.logDir} ${cfg.runtimeDir}/cache + chmod -R ug+rw ${cfg.dataDir}/public/cache + chmod -R ug+rw ${cfg.logDir} + chmod -R ug+rwx ${cfg.runtimeDir}/cache + + # Migrations + MOVIM_VERSION="${package.version}" + if [[ ! -f "${cfg.dataDir}/.migration-version" ]] || [[ "$MOVIM_VERSION" != "$(<${cfg.dataDir}/.migration-version)" ]]; then + ${package}/bin/movim-composer movim:migrate && echo $MOVIM_VERSION > ${cfg.dataDir}/.migration-version + fi + '' + + lib.optionalString (podConfigFlags != "") ( + let + flags = lib.concatStringsSep " " + ([ "--no-interaction" ] + ++ lib.optional cfg.debug "-vvv" + ++ lib.optional (!cfg.debug && cfg.verbose) "-v"); + in + '' + ${lib.getExe package} config ${podConfigFlags} + '' + ); + }; + + services.movim = { + description = "Movim daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "movim-data-setup.service" ]; + requires = [ "movim-data-setup.service" ] + ++ lib.optional cfg.database.createLocally dbService; + environment = { + PUBLIC_URL = "//${cfg.domain}"; + WS_PORT = builtins.toString cfg.port; + }; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + WorkingDirectory = "${package}/share/php/movim"; + ExecStart = "${lib.getExe package} start"; + }; + }; + + services.${phpExecutionUnit} = { + after = [ "movim-data-setup.service" ]; + requires = [ "movim-data-setup.service" ] + ++ lib.optional cfg.database.createLocally dbService; + }; + + tmpfiles.settings."10-movim" = with cfg; { + "${dataDir}".d = { inherit user group; mode = "0710"; }; + "${dataDir}/public".d = { inherit user group; mode = "0750"; }; + "${dataDir}/public/cache".d = { inherit user group; mode = "0750"; }; + "${runtimeDir}".d = { inherit user group; mode = "0700"; }; + "${runtimeDir}/cache".d = { inherit user group; mode = "0700"; }; + "${logDir}".d = { inherit user group; mode = "0700"; }; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 7d120d6bc09e..909eea38b35e 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -558,6 +558,7 @@ in { morty = handleTest ./morty.nix {}; mosquitto = handleTest ./mosquitto.nix {}; moosefs = handleTest ./moosefs.nix {}; + movim = discoverTests (import ./web-apps/movim { inherit handleTestOn; }); mpd = handleTest ./mpd.nix {}; mpv = handleTest ./mpv.nix {}; mtp = handleTest ./mtp.nix {}; diff --git a/nixos/tests/web-apps/movim/default.nix b/nixos/tests/web-apps/movim/default.nix new file mode 100644 index 000000000000..5d6314e2b41b --- /dev/null +++ b/nixos/tests/web-apps/movim/default.nix @@ -0,0 +1,8 @@ +{ system ? builtins.currentSystem, handleTestOn }: + +let + supportedSystems = [ "x86_64-linux" "i686-linux" ]; +in +{ + standard = handleTestOn supportedSystems ./standard.nix { inherit system; }; +} diff --git a/nixos/tests/web-apps/movim/standard.nix b/nixos/tests/web-apps/movim/standard.nix new file mode 100644 index 000000000000..470d81d8f722 --- /dev/null +++ b/nixos/tests/web-apps/movim/standard.nix @@ -0,0 +1,102 @@ +import ../../make-test-python.nix ({ lib, pkgs, ... }: + +let + movim = { + domain = "movim.local"; + info = "No ToS in tests"; + description = "NixOS testing server"; + }; + xmpp = { + domain = "xmpp.local"; + admin = rec { + JID = "${username}@${xmpp.domain}"; + username = "romeo"; + password = "juliet"; + }; + }; +in +{ + name = "movim-standard"; + + meta = { + maintainers = with pkgs.lib.maintainers; [ toastal ]; + }; + + nodes = { + server = { pkgs, ... }: { + services.movim = { + inherit (movim) domain; + enable = true; + verbose = true; + podConfig = { + inherit (movim) description info; + xmppdomain = xmpp.domain; + }; + nginx = { }; + }; + + services.prosody = { + enable = true; + xmppComplianceSuite = false; + disco_items = [ + { url = "upload.${xmpp.domain}"; description = "File Uploads"; } + ]; + virtualHosts."${xmpp.domain}" = { + inherit (xmpp) domain; + enabled = true; + extraConfig = '' + Component "pubsub.${xmpp.domain}" "pubsub" + pubsub_max_items = 10000 + expose_publisher = true + + Component "upload.${xmpp.domain}" "http_file_share" + http_external_url = "http://upload.${xmpp.domain}" + http_file_share_expires_after = 300 * 24 * 60 * 60 + http_file_share_size_limit = 1024 * 1024 * 1024 + http_file_share_daily_quota = 4 * 1024 * 1024 * 1024 + ''; + }; + extraConfig = '' + pep_max_items = 10000 + + http_paths = { + file_share = "/"; + } + ''; + }; + + networking.extraHosts = '' + 127.0.0.1 ${movim.domain} + 127.0.0.1 ${xmpp.domain} + ''; + }; + }; + + testScript = /* python */ '' + server.wait_for_unit("phpfpm-movim.service") + server.wait_for_unit("nginx.service") + server.wait_for_open_port(80) + + server.wait_for_unit("prosody.service") + server.succeed('prosodyctl status | grep "Prosody is running"') + server.succeed("prosodyctl register ${xmpp.admin.username} ${xmpp.domain} ${xmpp.admin.password}") + + server.wait_for_unit("movim.service") + + # Test unauthenticated + server.fail("curl -L --fail-with-body --max-redirs 0 http://${movim.domain}/chat") + + # Test basic Websocket + server.succeed("echo \"\" | ${lib.getExe pkgs.websocat} 'ws://${movim.domain}/ws/?path=login&offset=0' --origin 'http://${movim.domain}'") + + # Test login + create cookiejar + login_html = server.succeed("curl --fail-with-body -c /tmp/cookies http://${movim.domain}/login") + assert "${movim.description}" in login_html + assert "${movim.info}" in login_html + + # Test authentication POST + server.succeed("curl --fail-with-body -b /tmp/cookies -X POST --data-urlencode 'username=${xmpp.admin.JID}' --data-urlencode 'password=${xmpp.admin.password}' http://${movim.domain}/login") + + server.succeed("curl -L --fail-with-body --max-redirs 1 -b /tmp/cookies http://${movim.domain}/chat") + ''; +}) diff --git a/pkgs/by-name/mo/movim/package.nix b/pkgs/by-name/mo/movim/package.nix index 0225dbc98018..04695835710f 100644 --- a/pkgs/by-name/mo/movim/package.nix +++ b/pkgs/by-name/mo/movim/package.nix @@ -1,10 +1,12 @@ { lib +, fetchpatch , fetchFromGitHub , dash , php , phpCfg ? null , withPgsql ? true # “strongly recommended” according to docs , withMysql ? false +, nixosTests }: php.buildComposerProject (finalAttrs: { @@ -36,6 +38,20 @@ php.buildComposerProject (finalAttrs: { vendorHash = "sha256-RFIi1I+gcagRgkDpgQeR1oGJeBGA7z9q3DCfW+ZDr2Y="; postPatch = '' + # Our modules are already wrapped, removes missing *.so warnings; + # replacing `$configuration` with actually-used flags. + substituteInPlace src/Movim/Daemon/Session.php \ + --replace-fail "exec php ' . \$configuration " "exec php -dopcache.enable=1 -dopcache.enable_cli=1 ' " + + # Point to PHP + PHP INI in the Nix store + substituteInPlace src/Movim/{Console/DaemonCommand.php,Daemon/Session.php} \ + --replace-fail "exec php " "exec ${lib.getExe finalAttrs.php} " + substituteInPlace src/Movim/Console/DaemonCommand.php \ + --replace-fail "php vendor/bin/phinx migrate" \ + "${lib.getBin finalAttrs.php} vendor/bin/phinx migrate" \ + --replace-fail "php daemon.php setAdmin {jid}" \ + "${finalAttrs.meta.mainProgram} setAdmin {jid}" + # BUGFIX: Imagick API Changes for 7.x+ # See additionally: https://github.com/movim/movim/pull/1122 substituteInPlace src/Movim/Image.php \ @@ -56,6 +72,10 @@ php.buildComposerProject (finalAttrs: { chmod +x $out/share/{bash-completion/completion/movim.bash,fish/vendor_completions.d/movim.fish,zsh/site-functions/_movim} ''; + passthru = { + tests = { inherit (nixosTests) movim; }; + }; + meta = { description = "a federated blogging & chat platform that acts as a web front end for the XMPP protocol"; homepage = "https://movim.eu"; From 64b110589cf76e4eaa6ff1c79b162418cccae3a4 Mon Sep 17 00:00:00 2001 From: toastal Date: Mon, 18 Mar 2024 15:49:41 +0700 Subject: [PATCH 5/6] movim: minifyStaticFiles prop --- nixos/modules/services/web-apps/movim.nix | 57 ++++++++++++++++-- pkgs/by-name/mo/movim/package.nix | 70 +++++++++++++++++++++++ 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/nixos/modules/services/web-apps/movim.nix b/nixos/modules/services/web-apps/movim.nix index c9314e28e949..d7f6ad5bb5ef 100644 --- a/nixos/modules/services/web-apps/movim.nix +++ b/nixos/modules/services/web-apps/movim.nix @@ -44,11 +44,17 @@ let package = let - p = cfg.package.override { - inherit phpCfg; - withPgsql = cfg.database.type == "pgsql"; - withMysql = cfg.database.type == "mysql"; - }; + p = cfg.package.override + ({ + inherit phpCfg; + withPgsql = cfg.database.type == "pgsql"; + withMysql = cfg.database.type == "mysql"; + inherit (cfg) minifyStaticFiles; + } // lib.optionalAttrs (lib.isAttrs cfg.minifyStaticFiles) (with cfg.minifyStaticFiles; { + esbuild = esbuild.package; + lightningcss = lightningcss.package; + scour = scour.package; + })); in p.overrideAttrs (finalAttrs: prevAttrs: let @@ -177,6 +183,47 @@ in description = "Verbose logs."; }; + minifyStaticFiles = mkOption { + type = with types; either bool (submodule { + options = { + script = mkOption { + type = types.submodule { + options = { + enable = mkEnableOption "Script minification"; + package = mkPackageOption pkgs "esbuild" { }; + target = mkOption { + type = with types; nullOr nonEmptyStr; + default = null; + }; + }; + }; + }; + style = mkOption { + type = types.submodule { + options = { + enable = mkEnableOption "Script minification"; + package = mkPackageOption pkgs "lightningcss" { }; + target = mkOption { + type = with types; nullOr nonEmptyStr; + default = null; + }; + }; + }; + }; + svg = mkOption { + type = types.submodule { + options = { + enable = mkEnableOption "SVG minification"; + package = mkPackageOption pkgs "scour" { }; + }; + }; + }; + }; + }); + default = true; + description = "Do minification on public static files"; + }; + podConfig = mkOption { type = types.submodule { options = { diff --git a/pkgs/by-name/mo/movim/package.nix b/pkgs/by-name/mo/movim/package.nix index 04695835710f..6380baf3d46f 100644 --- a/pkgs/by-name/mo/movim/package.nix +++ b/pkgs/by-name/mo/movim/package.nix @@ -6,9 +6,37 @@ , phpCfg ? null , withPgsql ? true # “strongly recommended” according to docs , withMysql ? false +, minifyStaticFiles ? false # default files are often not minified +, parallel +, esbuild +, lightningcss +, scour , nixosTests }: +let + defaultMinifyOpts = { + script = { + enable = false; + target = "es2021"; + }; + style = { + enable = false; + browserslist = "defaults, Firefox ESR, last 20 Firefox major versions, last 20 Chrome major versions, last 3 Safari major versions, last 1 KaiOS version, and supports css-variables"; + }; + svg = { + enable = false; + }; + }; + + minify = lib.recursiveUpdate defaultMinifyOpts + (if lib.isBool minifyStaticFiles && minifyStaticFiles then + { script.enable = true; style.enable = true; svg.enable = true; } + else if lib.isAttrs minifyStaticFiles then + lib.filterAttrsRecursive (_: v: v != null) minifyStaticFiles + else + { }); +in php.buildComposerProject (finalAttrs: { pname = "movim"; version = "0.23.0.20240328"; @@ -31,6 +59,12 @@ php.buildComposerProject (finalAttrs: { extraConfig = phpCfg; }); + nativeBuildInputs = + lib.optional (lib.any (x: x.enable) (lib.attrValues minify)) parallel + ++ lib.optional minify.script.enable esbuild + ++ lib.optional minify.style.enable lightningcss + ++ lib.optional minify.svg.enable scour; + # no listed license # pinned commonmark composerStrictValidation = false; @@ -59,6 +93,42 @@ php.buildComposerProject (finalAttrs: { --replace-fail "Imagick::ALPHACHANNEL_ACTIVATE" "Imagick::ALPHACHANNEL_ON" ''; + preBuild = lib.optionalString minify.script.enable '' + find ./public -type f -iname "*.js" \ + | parallel ${lib.escapeShellArgs [ + "--will-cite" + "-j $NIX_BUILD_CORES" + '' + tmp="$(mktemp)" + esbuild {} --minify --target=${lib.escapeShellArg minify.script.target} --outfile=$tmp + [[ "$(stat -c %s $tmp)" -lt "$(stat -c %s {})" ]] && mv $tmp {} + '' + ]} + '' + lib.optionalString minify.style.enable '' + export BROWSERLIST=${lib.escapeShellArg minify.style.browserslist} + find ./public -type f -iname "*.css" \ + | parallel ${lib.escapeShellArgs [ + "--will-cite" + "-j $NIX_BUILD_CORES" + '' + tmp="$(mktemp)" + lightningcss {} --minify --browserslist --output-file=$tmp + [[ "$(stat -c %s $tmp)" -lt "$(stat -c %s {})" ]] && mv $tmp {} + '' + ]} + '' + lib.optionalString minify.svg.enable '' + find ./public -type f -iname "*.svg" -a -not -path "*/emojis/*" \ + | parallel ${lib.escapeShellArgs [ + "--will-cite" + "-j $NIX_BUILD_CORES" + '' + tmp="$(mktemp)" + scour -i {} -o $tmp --disable-style-to-xml --enable-comment-stripping --enable-viewboxing --indent=tab + [[ "$(stat -c %s $tmp)" -lt "$(stat -c %s {})" ]] && mv $tmp {} + '' + ]} + ''; + postInstall = '' mkdir -p $out/bin echo "#!${lib.getExe dash}" > $out/bin/movim From 0ba23300dec970d3226841a0002e1dd0a4320430 Mon Sep 17 00:00:00 2001 From: toastal Date: Mon, 18 Mar 2024 15:50:57 +0700 Subject: [PATCH 6/6] nixos/movim: precompress static files --- nixos/modules/services/web-apps/movim.nix | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/nixos/modules/services/web-apps/movim.nix b/nixos/modules/services/web-apps/movim.nix index d7f6ad5bb5ef..bb88a185b461 100644 --- a/nixos/modules/services/web-apps/movim.nix +++ b/nixos/modules/services/web-apps/movim.nix @@ -91,6 +91,37 @@ let '') [ ] cfg.podConfig)); + + precompressStaticFilesJobs = + let + inherit (cfg.precompressStaticFiles) brotli gzip; + + findTextFileNames = lib.concatStringsSep " -o " + (builtins.map (n: ''-iname "*.${n}"'') + [ "css" "ini" "js" "json" "manifest" "mjs" "svg" "webmanifest" ]); + in + lib.concatStringsSep "\n" [ + (lib.optionalString brotli.enable '' + echo -n "Precompressing static files with Brotli …" + find ${appDir}/public -type f ${findTextFileNames} \ + | ${lib.getExe pkgs.parallel} ${lib.escapeShellArgs [ + "--will-cite" + "-j $NIX_BUILD_CORES" + "${lib.getExe brotli.package} --keep --quality=${builtins.toString brotli.compressionLevel} --output={}.br {}" + ]} + echo " done." + '') + (lib.optionalString gzip.enable '' + echo -n "Precompressing static files with Gzip …" + find ${appDir}/public -type f ${findTextFileNames} \ + | ${lib.getExe pkgs.parallel} ${lib.escapeShellArgs [ + "--will-cite" + "-j $NIX_BUILD_CORES" + "${lib.getExe gzip.package} -c -${builtins.toString gzip.compressionLevel} {} > {}.gz" + ]} + echo " done." + '') + ]; in { postInstall = lib.concatStringsSep "\n\n" [ @@ -98,6 +129,7 @@ let stateDirectories exposeComposer podConfigInputDisableReplace + precompressStaticFilesJobs ]; }); @@ -224,6 +256,36 @@ in description = "Do minification on public static files"; }; + precompressStaticFiles = mkOption { + type = with types; submodule { + options = { + brotli = { + enable = mkEnableOption "Brotli precompression"; + package = mkPackageOption pkgs "brotli" { }; + compressionLevel = mkOption { + type = types.ints.between 0 11; + default = 11; + description = "Brotli compression level"; + }; + }; + gzip = { + enable = mkEnableOption "Gzip precompression"; + package = mkPackageOption pkgs "gzip" { }; + compressionLevel = mkOption { + type = types.ints.between 1 9; + default = 9; + description = "Gzip compression level"; + }; + }; + }; + }; + default = { + brotli.enable = true; + gzip.enable = false; + }; + description = "Aggressively precompress static files"; + }; + podConfig = mkOption { type = types.submodule { options = {