is_valid_page( $page_id ) ? $page_id : 0; } /** * A valid maintenance page is a published page without password, other than * the posts page (on the posts page is_page() is false, which would cause a * redirect loop; a password would show the password form to visitors). */ private function is_valid_page( $page_id ) { if ( $page_id <= 0 || (int) get_option( 'page_for_posts' ) === $page_id ) { return false; } $page = get_post( $page_id ); return $page && 'page' === $page->post_type && 'publish' === $page->post_status && '' === $page->post_password; } /** * Roles that keep browsing the site while maintenance is active. * Until the setting is saved, default to the roles that can write content, * so subscribers and customers see the maintenance page. */ private function get_bypass_roles() { $roles = get_option( self::OPTION_ROLES, false ); if ( false === $roles ) { $roles = array(); foreach ( wp_roles()->role_objects as $role => $role_object ) { if ( $role_object->has_cap( 'edit_posts' ) ) { $roles[] = $role; } } } return (array) $roles; } /** * True when the current user can browse the site and the dashboard. * Administrators always can, so nobody can lock themselves out. */ private function user_can_bypass() { if ( ! is_user_logged_in() ) { return false; } $user = wp_get_current_user(); $can_bypass = current_user_can( 'manage_options' ) || (bool) array_intersect( (array) $user->roles, $this->get_bypass_roles() ); /** * Filters whether the current user can browse the site during maintenance. * * @param bool $can_bypass Whether the user can bypass maintenance. * @param WP_User $user The current user. */ return (bool) apply_filters( 'acwmmbp_user_can_bypass', $can_bypass, $user ); } /** * Pages that stay reachable during maintenance: the ones selected in the * settings plus the login, registration and lost password pages when a * plugin moves them to the front end (e.g. Paid Memberships Pro). */ private function get_allowed_page_ids() { $ids = (array) get_option( self::OPTION_ALLOWED, array() ); foreach ( $this->get_login_page_ids() as $id ) { $ids[] = $id; } /** * Filters the IDs of the pages that stay reachable during maintenance. * * @param int[] $ids Page IDs. */ $ids = apply_filters( 'acwmmbp_allowed_page_ids', $ids ); return array_values( array_unique( array_filter( array_map( 'absint', (array) $ids ) ) ) ); } /** * Front-end pages used as login, registration or lost password page. */ private function get_login_page_ids() { $ids = array(); foreach ( array( wp_login_url(), wp_registration_url(), wp_lostpassword_url() ) as $url ) { if ( false !== strpos( $url, 'wp-login.php' ) ) { continue; } $id = url_to_postid( $url ); if ( $id ) { $ids[] = $id; } } return array_unique( $ids ); } /** * True when the current request is the maintenance page shown to a user * who cannot bypass maintenance. */ private function is_maintenance_request() { if ( ! $this->is_enabled() || $this->user_can_bypass() ) { return false; } $page_id = $this->get_page_id(); return $page_id && is_page( $page_id ); } /** * Redirect visitors (and users whose role cannot bypass maintenance) to the * maintenance page. */ public function maybe_redirect() { if ( ! $this->is_enabled() || $this->user_can_bypass() ) { return; } // Serve robots.txt normally: search engines then see the 503 of every // page, while a 5xx robots.txt would stop the crawl of the whole site. if ( is_robots() || ( function_exists( 'is_favicon' ) && is_favicon() ) ) { return; } $page_id = $this->get_page_id(); // If we are already on the maintenance page, serve it with a 503 status // (temporarily unavailable) so search engines do not index it as // permanent content. if ( $page_id && is_page( $page_id ) ) { $this->send_503_headers(); return; } if ( is_page() && in_array( get_queried_object_id(), $this->get_allowed_page_ids(), true ) ) { return; } // The selected page is missing or no longer published: keep the site // closed with a generic message instead of redirecting to a dead URL. if ( ! $page_id ) { $this->send_503_headers(); wp_die( esc_html__( 'The site is undergoing maintenance. Please come back soon.', 'acw-maintenance-mode-by-page' ), esc_html__( 'Maintenance', 'acw-maintenance-mode-by-page' ), array( 'response' => 503 ) ); } nocache_headers(); wp_safe_redirect( get_permalink( $page_id ), 302 ); exit; } /** * Keep logged-in users whose role cannot bypass maintenance out of the * dashboard. AJAX and admin-post.php stay reachable for front-end features. */ public function maybe_block_admin() { if ( ! $this->is_enabled() || ! is_user_logged_in() || $this->user_can_bypass() || wp_doing_ajax() ) { return; } if ( isset( $GLOBALS['pagenow'] ) && 'admin-post.php' === $GLOBALS['pagenow'] ) { return; } $page_id = $this->get_page_id(); nocache_headers(); wp_safe_redirect( $page_id ? get_permalink( $page_id ) : home_url( '/' ), 302 ); exit; } /** * Answer XML sitemap requests (e.g. those of WordPress and Yoast SEO) with a * 503, so the list of the site URLs is not published during * maintenance. Runs on init because SEO plugins serve their sitemaps before * template_redirect. */ public function maybe_block_sitemap() { if ( ! $this->is_enabled() || ! $this->is_sitemap_request() || $this->user_can_bypass() ) { return; } $this->send_503_headers(); header( 'Content-Type: text/plain; charset=utf-8' ); echo esc_html__( 'The site is undergoing maintenance. Please come back soon.', 'acw-maintenance-mode-by-page' ); exit; } /** * True for an XML sitemap or its stylesheet, e.g. /sitemap_index.xml, * /page-sitemap.xml, /wp-sitemap-posts-page-1.xml or /main-sitemap.xsl. */ private function is_sitemap_request() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check of the requested URL. if ( isset( $_GET['sitemap'] ) || isset( $_GET['sitemap-stylesheet'] ) ) { return true; } $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); return (bool) preg_match( '#sitemap[^/]*\.(xml|xsl)(\.gz)?$#i', $path ); } /** * When the option is on, answer the REST API routes that publish the site * content (wp/v2 and oEmbed) with a 503 for requests that are not * authenticated. Signed-in users and application passwords keep working; * the routes of other plugins (e.g. forms) are not affected. */ public function maybe_block_rest( $result, $server, $request ) { if ( null !== $result || ! get_option( self::OPTION_REST ) || ! $this->is_enabled() || is_user_logged_in() ) { return $result; } /** * Filters the REST API namespaces closed to unauthenticated requests. * * @param string[] $namespaces Namespaces, e.g. "wp/v2". */ $namespaces = (array) apply_filters( 'acwmmbp_blocked_rest_namespaces', array( 'wp/v2', 'oembed/1.0' ) ); // WordPress matches routes case-insensitively. $route = strtolower( untrailingslashit( $request->get_route() ) ); foreach ( $namespaces as $namespace ) { $namespace = '/' . strtolower( trim( $namespace, '/' ) ); if ( $route === $namespace || 0 === strpos( $route, $namespace . '/' ) ) { $response = new WP_REST_Response( array( 'code' => 'acwmmbp_maintenance', 'message' => __( 'The site is undergoing maintenance. Please come back soon.', 'acw-maintenance-mode-by-page' ), 'data' => array( 'status' => 503 ), ), 503 ); $response->header( 'Retry-After', '3600' ); return $response; } } return $result; } /** * When the option is on, filter what plugins and snippets print through * wp_head(), wp_body_open() and wp_footer() on the maintenance page, * removing tracking codes and other added scripts. Styles stay. */ public function maybe_hide_scripts() { if ( ! get_option( self::OPTION_SCRIPTS ) || ! $this->is_maintenance_request() ) { return; } foreach ( array( 'wp_head', 'wp_body_open', 'wp_footer' ) as $hook ) { add_action( $hook, array( $this, 'start_scripts_buffer' ), PHP_INT_MIN ); add_action( $hook, array( $this, 'end_scripts_buffer' ), PHP_INT_MAX ); } } public function start_scripts_buffer() { ob_start(); } public function end_scripts_buffer() { echo $this->remove_added_scripts( (string) ob_get_clean() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Markup printed by WordPress hooks, only filtered here. } /** * Remove