diff --git a/composer.json b/composer.json index 273b70a..f50ecf0 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ }, "files": [ "auth-command.php", - "src/auth-utils.php" + "src/auth-utils.php", + "src/helper/hooks.php" ] }, "extra": { diff --git a/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php b/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php new file mode 100644 index 0000000..1b3740f --- /dev/null +++ b/migrations/container/20260924120000_auth-command_regenerate_site_auth_files.php @@ -0,0 +1,56 @@ +sites = Site::all(); + if ( $this->is_first_execution || ! $this->sites ) { + $this->skip_this_migration = true; + } + } + + /** + * Regenerate the htpasswd and ACL files of all sites. + * + * Older versions only wrote `htpasswd/` and `_acl`, leaving subdomains and alias domains unprotected, and kept files of sites without own entries. + * + * @throws EE\ExitException + */ + public function up() { + + if ( $this->skip_this_migration ) { + EE::debug( 'Skipping site auth files regeneration migration as it is not needed.' ); + + return; + } + + foreach ( $this->sites as $site ) { + try { + generate_site_auth_files( $site->site_url, $site ); + generate_site_whitelist( $site->site_url, $site ); + } catch ( \Throwable $e ) { + EE::warning( sprintf( 'Could not regenerate the auth files of %s: %s', $site->site_url, $e->getMessage() ) ); + } + } + + \EE\Site\Utils\reload_global_nginx_proxy(); + } + + /** + * Not reverted. The files need the nginx-proxy image of the same release: the previous image also applies `_wildcard.*` files to sibling sites. + */ + public function down() { + } +} diff --git a/src/Auth_Command.php b/src/Auth_Command.php index f297aa6..55588ad 100644 --- a/src/Auth_Command.php +++ b/src/Auth_Command.php @@ -17,9 +17,13 @@ use EE\Model\Auth; use EE\Model\Whitelist; use Symfony\Component\Filesystem\Filesystem; +use function EE\Auth\Utils\generate_site_auth_files; +use function EE\Auth\Utils\generate_site_whitelist; use function EE\Auth\Utils\verify_htpasswd_is_present; +use function EE\Auth\Utils\write_htpasswd_file; use function EE\Site\Utils\auto_site_name; use function EE\Site\Utils\get_site_info; +use function EE\Site\Utils\is_reserved_proxy_file_name; use function EE\Site\Utils\reload_global_nginx_proxy; class Auth_Command extends EE_Command { @@ -161,7 +165,7 @@ private function create_auth( array $assoc_args, bool $global, string $site_url if ( 'default' === $site_url ) { $this->generate_global_auth_files(); } else { - $this->generate_site_auth_files( $site_url ); + generate_site_auth_files( $site_url, $this->site_data ); } EE::log( 'Reloading global reverse proxy.' ); @@ -200,7 +204,7 @@ private function create_whitelist( string $site_url, string $ips ) { if ( 'default' === $site_url ) { $this->generate_global_whitelist(); } else { - $this->generate_site_whitelist( $site_url ); + generate_site_whitelist( $site_url, $this->site_data ); } reload_global_nginx_proxy(); @@ -255,24 +259,17 @@ private function generate_global_auth_files() { $global_admin_tools_auth = Auth::get_global_admin_tools_auth(); if ( ! empty( $global_admin_tools_auth ) ) { - EE::exec( sprintf( 'docker exec %s htpasswd -bc /etc/nginx/htpasswd/default_admin_tools %s %s', EE_PROXY_TYPE, $global_admin_tools_auth->username, $global_admin_tools_auth->password ) ); + write_htpasswd_file( 'default_admin_tools', $global_admin_tools_auth ); } else { - $this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default_admin_tools' ); - $this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default' ); $auths = Auth::get_global_auths(); if ( empty( $auths ) ) { + $this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default_admin_tools' ); + $this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default' ); $this->regen_admin_tools_auth(); - } else { - foreach ( $auths as $key => $auth ) { - $flags = 'b'; - - if ( 0 === $key ) { - $flags = 'bc'; - } - - EE::exec( sprintf( 'docker exec %s htpasswd -%s /etc/nginx/htpasswd/default %s %s', EE_PROXY_TYPE, $flags, $auth->username, $auth->password ) ); - } + } elseif ( write_htpasswd_file( 'default', $auths ) ) { + // Admin tools prefer default_admin_tools, so drop it only once `default` is written; on failure both files keep protecting. + $this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default_admin_tools' ); } $sites = array_unique( @@ -283,34 +280,11 @@ private function generate_global_auth_files() { ); foreach ( $sites as $site ) { - $this->generate_site_auth_files( $site ); - } - } - } - - /** - * Generates auth files for a site - * - * @param string $site_url URL of site - * - * @throws Exception - */ - private function generate_site_auth_files( string $site_url ) { - $site_auth_file = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/' . $site_url; - $this->fs->remove( $site_auth_file ); - - $auths = array_merge( - Auth::get_global_auths(), - Auth::where( 'site_url', $site_url ) - ); - - foreach ( $auths as $key => $auth ) { - $flags = 'b'; - - if ( $key === 0 ) { - $flags = 'bc'; + // The global files were handled above. + if ( ! is_reserved_proxy_file_name( $site ) ) { + generate_site_auth_files( $site, \EE\Model\Site::find( $site ) ?: null ); + } } - EE::exec( sprintf( 'docker exec %s htpasswd -%s /etc/nginx/htpasswd/%s %s %s', EE_PROXY_TYPE, $flags, $site_url, $auth->username, $auth->password ) ); } } @@ -320,7 +294,7 @@ private function generate_site_auth_files( string $site_url ) { * @throws Exception */ private function generate_global_whitelist() { - $this->generate_site_whitelist( 'default' ); + generate_site_whitelist( 'default' ); $sites = array_unique( array_column( @@ -333,54 +307,11 @@ private function generate_global_whitelist() { } foreach ( $sites as $site ) { - $this->generate_site_whitelist( $site ); + generate_site_whitelist( $site, \EE\Model\Site::find( $site ) ?: null ); } } - /** - * Generates site whitelist files - * - * @param string $site_url - * - * @throws Exception - */ - private function generate_site_whitelist( string $site_url ) { - $site_whitelist_file = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d/' . $site_url . '_acl'; - $this->fs->remove( $site_whitelist_file ); - - $whitelists = array_column( - 'default' === $site_url ? Whitelist::get_global_ips() : - array_merge( - Whitelist::get_global_ips(), - Whitelist::where( 'site_url', $site_url ) - ), - 'ip' - ); - - $this->put_ips_to_file( $site_whitelist_file, $whitelists ); - } - - /** - * Function to put list of ip's into a file. - * - * @param string $file Path of file to write ip's in. - * @param array $ips List of ip's. - */ - private function put_ips_to_file( string $file, array $ips ) { - - if ( empty( $ips ) ) { - return; - } - - $file_content = 'satisfy any;' . PHP_EOL; - foreach ( $ips as $ip ) { - $file_content .= "allow $ip;" . PHP_EOL; - } - $file_content .= 'deny all;'; - $this->fs->dumpFile( $file, $file_content ); - } - /** * Updates http authentication password for a site. * @@ -453,7 +384,7 @@ private function update_auth( array $assoc_args, string $site_url ) { if ( 'default' === $site_url ) { $this->generate_global_auth_files(); } else { - $this->generate_site_auth_files( $site_url ); + generate_site_auth_files( $site_url, $this->site_data ); } EE::log( 'Reloading global reverse proxy.' ); @@ -499,7 +430,7 @@ private function update_whitelist( string $site_url, string $ips ) { if ( 'default' === $site_url ) { $this->generate_global_whitelist(); } else { - $this->generate_site_whitelist( $site_url ); + generate_site_whitelist( $site_url, $this->site_data ); } reload_global_nginx_proxy(); @@ -594,7 +525,7 @@ public function delete( $args, $assoc_args ) { if ( 'default' === $site_url ) { $this->generate_global_auth_files(); } else { - $this->generate_site_auth_files( $site_url ); + generate_site_auth_files( $site_url, $this->site_data ); } if ( $user ) { @@ -646,7 +577,7 @@ public function delete( $args, $assoc_args ) { if ( 'default' === $site_url ) { $this->generate_global_whitelist(); } else { - $this->generate_site_whitelist( $site_url ); + generate_site_whitelist( $site_url, $this->site_data ); } reload_global_nginx_proxy(); diff --git a/src/auth-utils.php b/src/auth-utils.php index dde463e..420abc0 100644 --- a/src/auth-utils.php +++ b/src/auth-utils.php @@ -6,7 +6,11 @@ use EE; use EE\Model\Auth; use EE\Model\Option; +use EE\Model\Whitelist; +use Symfony\Component\Filesystem\Filesystem; use function EE\Service\Utils\ensure_global_network_initialized; +use function EE\Site\Utils\is_reserved_proxy_file_name; +use function EE\Site\Utils\is_valid_alias_domain; use function EE\Utils\get_config_value; /** @@ -38,7 +42,7 @@ function init_global_admin_tools_auth( $display_log = true ) { Auth::create( $auth_data ); - EE::exec( sprintf( 'docker exec %s htpasswd -bc /etc/nginx/htpasswd/default_admin_tools %s %s', EE_PROXY_TYPE, $auth_data['username'], $auth_data['password'] ) ); + write_htpasswd_file( 'default_admin_tools', [ (object) $auth_data ] ); if ( $display_log ) { EE::success( sprintf( 'Global admin-tools auth added. Use `ee auth list global` to view credentials.' ) ); @@ -62,3 +66,355 @@ function verify_htpasswd_is_present() { } EE::error( sprintf( 'Could not find apache2-utils installed in %s.', EE_PROXY_TYPE ) ); } + +/** + * Maps a domain to its htpasswd/ACL file name: `*.example.com` becomes `_wildcard.example.com`. + * + * @param string $domain Domain name. + * + * @return string + */ +function get_auth_domain( string $domain ): string { + + return 0 === strpos( $domain, '*.' ) ? '_wildcard.' . substr( $domain, 2 ) : $domain; +} + +/** + * Maps alias domains to their htpasswd/ACL file names, skipping the ones site-command would reject. + * + * @param array $aliases Alias domains. + * + * @return array + */ +function get_alias_auth_domains( array $aliases ): array { + + $domains = []; + foreach ( $aliases as $alias ) { + $alias = trim( (string) $alias ); + if ( is_valid_alias_domain( $alias ) ) { + $domains[] = get_auth_domain( $alias ); + } + } + + return $domains; +} + +/** + * Collects the htpasswd/ACL file names of a site: the site itself, `_wildcard.` for subdomain multisites, and its alias domains. + * + * @param string $site_url URL of site. + * @param \EE\Model\Site|null $site_data Site model. + * @param array $extra_aliases Alias domains not saved on the site yet, e.g. ones about to be added. + * + * @return array + */ +function get_site_auth_domains( string $site_url, $site_data, array $extra_aliases = [] ): array { + + $is_subdom = ! empty( $site_data->app_sub_type ) && 'subdom' === $site_data->app_sub_type; + $domains = [ $site_url ]; + + if ( $is_subdom ) { + $domains[] = '_wildcard.' . $site_url; + } + + $aliases = empty( $site_data->alias_domains ) ? [] : explode( ',', $site_data->alias_domains ); + $domains = array_merge( $domains, get_alias_auth_domains( array_merge( $aliases, $extra_aliases ) ) ); + + return array_values( array_unique( $domains ) ); +} + +/** + * Checks that a name refers to an entry directly inside a directory, not the directory itself or anything outside it. + * + * @param string $name File name. + * + * @return bool + */ +function is_proxy_file_name( string $name ): bool { + + return '' !== $name && '.' !== $name && '..' !== $name && false === strpbrk( $name, "/\\\0" ); +} + +/** + * Removes a regular file directly inside a proxy directory. + * + * @param string $dir Directory path. + * @param string $name File name. + * + * @return bool Whether the file was removed. + */ +function remove_proxy_file( string $dir, string $name ): bool { + + $file = $dir . '/' . $name; + + // An empty name would make Filesystem::remove() wipe the whole directory. + if ( ! is_proxy_file_name( $name ) || ! is_file( $file ) ) { + return false; + } + + ( new Filesystem() )->remove( $file ); + + return true; +} + +/** + * Copies a file inside a proxy directory to other names in the same directory. + * + * @param string $dir Directory path. + * @param string $source Source file name. + * @param array $targets Target file names. + */ +function copy_proxy_file( string $dir, string $source, array $targets ) { + + $fs = new Filesystem(); + $mode = fileperms( $dir . '/' . $source ) & 0777; + + foreach ( $targets as $target ) { + if ( ! is_proxy_file_name( $target ) || $target === $source ) { + continue; + } + // Built under a name no host matches, then renamed, so the proxy never reads a partial copy. + $tmp = '.' . $target . '.tmp'; + try { + $fs->copy( $dir . '/' . $source, $dir . '/' . $tmp, true ); + // Don't depend on the umask: nginx workers read these files. + $fs->chmod( $dir . '/' . $tmp, $mode ); + $fs->rename( $dir . '/' . $tmp, $dir . '/' . $target, true ); + } catch ( \Exception $e ) { + remove_proxy_file( $dir, $tmp ); + EE::warning( sprintf( 'Could not copy %s to %s, so it was left unchanged.', $source, $target ) ); + } + } +} + +/** + * Removes the htpasswd and ACL files of the given domains. + * + * @param array $domains File names as returned by get_site_auth_domains(). + * + * @return bool Whether any file was removed. + */ +function remove_auth_files( array $domains ): bool { + + $removed = false; + foreach ( $domains as $domain ) { + $domain = (string) $domain; + if ( '' === $domain || is_reserved_proxy_file_name( $domain ) ) { + continue; + } + $removed = remove_proxy_file( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd', $domain ) || $removed; + $removed = remove_proxy_file( EE_ROOT_DIR . '/services/nginx-proxy/vhost.d', $domain . '_acl' ) || $removed; + } + + return $removed; +} + +/** + * Generates auth files for a site. + * + * @param string $site_url URL of site. + * @param \EE\Model\Site|null $site_data Site model. + * @param array $extra_aliases Alias domains not saved on the site yet, e.g. ones about to be added. + * + * @throws \Exception + */ +function generate_site_auth_files( string $site_url, $site_data = null, array $extra_aliases = [] ) { + + $dir = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd'; + $domains = get_site_auth_domains( $site_url, $site_data, $extra_aliases ); + $site_auths = Auth::where( 'site_url', $site_url ); + + // Without site entries the proxy falls back to the global `default` file. + if ( empty( $site_auths ) ) { + foreach ( $domains as $domain ) { + remove_proxy_file( $dir, $domain ); + } + + return; + } + + $source = array_shift( $domains ); + + // If it can't be rewritten (e.g. the proxy is stopped during an upgrade), still spread the existing file to the other domains. + if ( write_htpasswd_file( $source, array_merge( Auth::get_global_auths(), $site_auths ) ) || is_file( $dir . '/' . $source ) ) { + copy_proxy_file( $dir, $source, $domains ); + } +} + +/** + * Writes a site's htpasswd and ACL files, including for alias domains that are about to be served. + * + * @param string $site_url URL of site. + * @param \EE\Model\Site|null $site_data Site model. + * @param array $aliases Alias domains not saved on the site yet. + * + * @throws \Exception + */ +function add_site_auth_files( string $site_url, $site_data, array $aliases ) { + + generate_site_auth_files( $site_url, $site_data, $aliases ); + generate_site_whitelist( $site_url, $site_data, $aliases ); +} + +/** + * Checks whether any of the given file names lacks the htpasswd or ACL file the site's own entries call for. + * + * @param string $site_url URL of site. + * @param array $names File names as returned by get_alias_auth_domains(). + * + * @return bool + */ +function site_auth_files_missing( string $site_url, array $names ): bool { + + $has_auths = ! empty( Auth::where( 'site_url', $site_url ) ); + $has_ips = Whitelist::has_ips( $site_url ); + + foreach ( $names as $name ) { + $auth_missing = $has_auths && ! is_file( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/' . $name ); + $acl_missing = $has_ips && ! is_file( EE_ROOT_DIR . '/services/nginx-proxy/vhost.d/' . $name . '_acl' ); + + if ( $auth_missing || $acl_missing ) { + return true; + } + } + + return false; +} + +/** + * (Re)creates an htpasswd file in the proxy container with the given auth entries. + * + * The file is replaced only once all entries are written; on failure the existing file is left unchanged. + * + * @param string $name File name inside the htpasswd directory. + * @param array $auths Auth models, or objects with `username` and `password`. + * + * @return bool Whether all entries were written. + */ +function write_htpasswd_file( string $name, array $auths ): bool { + + if ( empty( $auths ) ) { + return true; + } + + $dir = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd'; + // No host name starts with a dot, so the proxy never uses the file while it is being built. + $tmp = '.' . $name . '.tmp'; + $flags = 'bc'; + $written = true; + + foreach ( $auths as $auth ) { + // Keep the credentials out of ee.log. + $obfuscate = [ escapeshellarg( $auth->password ), escapeshellarg( $auth->username ) ]; + + if ( ! EE::exec( htpasswd_command( $flags, $tmp, $auth->username, $auth->password ), false, false, $obfuscate ) ) { + $written = false; + break; + } + $flags = 'b'; + } + + if ( $written ) { + try { + ( new Filesystem() )->rename( $dir . '/' . $tmp, $dir . '/' . $name, true ); + } catch ( \Exception $e ) { + $written = false; + } + } + + if ( ! $written ) { + remove_proxy_file( $dir, $tmp ); + EE::warning( sprintf( 'Could not write the htpasswd file %s, so it was left unchanged.', $name ) ); + } + + return $written; +} + +/** + * Builds the `htpasswd` command run in the proxy container, with shell-escaped arguments. + * + * @param string $flags htpasswd flags without the leading dash. + * @param string $name File name inside the htpasswd directory. + * @param string $username Username. + * @param string $password Password. + * + * @return string + */ +function htpasswd_command( string $flags, string $name, string $username, string $password ): string { + + // htpasswd reports `Adding password for user ` on STDERR, which EE::exec() would log; failures still show in the exit code. + return sprintf( + 'docker exec %s htpasswd -%s %s %s %s 2>/dev/null', + EE_PROXY_TYPE, + $flags, + escapeshellarg( '/etc/nginx/htpasswd/' . $name ), + escapeshellarg( $username ), + escapeshellarg( $password ) + ); +} + +/** + * Gets the IPs to whitelist on a site: global and site entries, or none when the site has no own entries. + * + * @param string $site_url URL of site, `default` for global. + * + * @return array + */ +function get_site_whitelist_ips( string $site_url ): array { + + $site_ips = Whitelist::where( 'site_url', $site_url ); + + if ( empty( $site_ips ) ) { + return []; + } + + return array_column( + 'default' === $site_url ? $site_ips : array_merge( Whitelist::get_global_ips(), $site_ips ), + 'ip' + ); +} + +/** + * Generates whitelist files for a site. + * + * @param string $site_url URL of site, `default` for global. + * @param \EE\Model\Site|null $site_data Site model. + * @param array $extra_aliases Alias domains not saved on the site yet, e.g. ones about to be added. + * + * @throws \Exception + */ +function generate_site_whitelist( string $site_url, $site_data = null, array $extra_aliases = [] ) { + + $dir = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d'; + $domains = get_site_auth_domains( $site_url, $site_data, $extra_aliases ); + $ips = get_site_whitelist_ips( $site_url ); + + foreach ( $domains as $domain ) { + // Without site entries the proxy falls back to `default_acl`. + if ( empty( $ips ) ) { + remove_proxy_file( $dir, $domain . '_acl' ); + } else { + put_ips_to_file( $dir . '/' . $domain . '_acl', $ips ); + } + } +} + +/** + * Function to put list of ip's into a file. + * + * @param string $file Path of file to write ip's in. + * @param array $ips List of ip's. + */ +function put_ips_to_file( string $file, array $ips ) { + + if ( empty( $ips ) ) { + return; + } + + $file_content = 'satisfy any;' . PHP_EOL; + foreach ( $ips as $ip ) { + $file_content .= "allow $ip;" . PHP_EOL; + } + $file_content .= 'deny all;'; + ( new Filesystem() )->dumpFile( $file, $file_content ); +} diff --git a/src/helper/hooks.php b/src/helper/hooks.php index fe494f5..4bcc2b6 100644 --- a/src/helper/hooks.php +++ b/src/helper/hooks.php @@ -7,44 +7,115 @@ use EE\Model\Auth; use EE\Model\Site; use EE\Model\Whitelist; -use Symfony\Component\Filesystem\Filesystem; +use function EE\Auth\Utils\add_site_auth_files; +use function EE\Auth\Utils\get_alias_auth_domains; +use function EE\Auth\Utils\get_site_auth_domains; +use function EE\Auth\Utils\remove_auth_files; +use function EE\Auth\Utils\site_auth_files_missing; /** - * Hook to cleanup auth entries and whitelisted ips if any. + * Hook to cleanup auth entries, whitelisted ips and their files if any. * * @param string $site_url The site to be cleaned up. */ function cleanup_auth_and_whitelist( $site_url ) { - if ( ! Site::find( $site_url ) ) { + $site = Site::find( $site_url ); + + if ( ! $site ) { + return; + } + + $rows = array_merge( Auth::where( [ 'site_url' => $site_url ] ), Whitelist::where( [ 'site_url' => $site_url ] ) ); + + foreach ( $rows as $row ) { + $row->delete(); + } + + // Files may exist without site entries (e.g. left by older versions), so always remove them. + $removed = remove_auth_files( get_site_auth_domains( $site_url, $site ) ); + + if ( $removed || ! empty( $rows ) ) { + \EE\Site\Utils\reload_global_nginx_proxy(); + } +} + +/** + * Hook to sync auth and whitelist files with the alias domains of a site after they change. + * + * @param string $site_url The site whose alias domains changed. + * @param array $added_domains Alias domains that were added. + * @param array $removed_domains Alias domains that were removed. + */ +function update_auth_on_alias_domains_change( $site_url, $added_domains = [], $removed_domains = [] ) { + + $site = Site::find( $site_url ); + + if ( ! $site ) { return; } - $fs = new Filesystem(); + $reload = false; + + // Keep files the site still uses, e.g. _wildcard. of a subdomain multisite. + $removed = array_diff( get_alias_auth_domains( (array) $removed_domains ), get_site_auth_domains( $site_url, $site ) ); + + if ( remove_auth_files( $removed ) ) { + $reload = true; + } + + // The added domains got their files before the update, so only rewrite them if some are missing, e.g. when that failed. + if ( site_auth_files_missing( $site_url, get_alias_auth_domains( (array) $added_domains ) ) ) { + add_site_auth_files( $site_url, $site, [] ); + $reload = true; + } - $auths = Auth::where( [ 'site_url' => $site_url ] ); + if ( $reload ) { + \EE\Site\Utils\reload_global_nginx_proxy(); + } +} + +/** + * Hook to write auth and whitelist files for alias domains before the proxy serves them, so they are never reachable unprotected. + * + * @param string $site_url The site whose alias domains change. + * @param array $domains_to_add Alias domains that are being added. + */ +function add_auth_before_alias_domains_update( $site_url, $domains_to_add = [] ) { - if ( ! empty( $auths ) ) { - foreach ( $auths as $auth ) { - $auth->delete(); - } + $site = Site::find( $site_url ); - $site_auth_file = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/' . $site_url; - $fs->remove( $site_auth_file ); + if ( ! $site || empty( $domains_to_add ) ) { + return; } - $whitelists = Whitelist::where( [ 'site_url' => $site_url ] ); + // No reload needed: docker-gen renders the new hosts with these files once the site's containers are recreated. + add_site_auth_files( $site_url, $site, (array) $domains_to_add ); +} - if ( ! empty( $whitelists ) ) { - foreach ( $whitelists as $whitelist ) { - $whitelist->delete(); - } +/** + * Hook to remove the files written for alias domains whose update failed. + * + * @param string $site_url The site whose alias domains update failed. + * @param array $domains_to_add Alias domains that were not added after all. + */ +function remove_auth_on_alias_domains_update_failure( $site_url, $domains_to_add = [] ) { - $site_whitelist_file = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d/' . $site_url . '_acl'; - $fs->remove( $site_whitelist_file ); + $site = Site::find( $site_url ); + + if ( ! $site ) { + return; } - \EE\Site\Utils\reload_global_nginx_proxy(); + // The site still has its old alias domains, so this keeps every file it uses. + $names = array_diff( get_alias_auth_domains( (array) $domains_to_add ), get_site_auth_domains( $site_url, $site ) ); + + if ( remove_auth_files( $names ) ) { + \EE\Site\Utils\reload_global_nginx_proxy(); + } } EE::add_hook( 'site_cleanup', 'cleanup_auth_and_whitelist' ); +EE::add_hook( 'site_alias_domains_before_update', 'add_auth_before_alias_domains_update' ); +EE::add_hook( 'site_alias_domains_updated', 'update_auth_on_alias_domains_change' ); +EE::add_hook( 'site_alias_domains_update_failed', 'remove_auth_on_alias_domains_update_failure' );