'off', // Content. 'cs_headline' => __( "We're launching soon", 'sitehold' ), 'cs_message' => __( 'Something awesome is on its way. Stay tuned!', 'sitehold' ), 'mt_headline' => __( 'Under maintenance', 'sitehold' ), 'mt_message' => __( 'We are performing scheduled maintenance. We will be back online shortly. Thank you for your patience.', 'sitehold' ), 'contact_email' => '', 'show_login_link' => 1, // Countdown. 'countdown_enabled' => 0, 'launch_date' => '', 'auto_disable' => 0, // Design. 'logo_id' => 0, 'bg_image_id' => 0, 'bg_color' => '#0f172a', 'overlay' => 60, 'text_color' => '#ffffff', 'accent_color' => '#6366f1', // Social. 'social_facebook' => '', 'social_x' => '', 'social_instagram' => '', 'social_linkedin' => '', 'social_youtube' => '', // Access. 'bypass_roles' => array( 'administrator' ), 'bypass_key' => '', 'bypass_days' => 2, 'allowed_ips' => '', 'excluded_paths' => '', 'block_rest' => 0, // SEO & advanced. 'meta_title' => '', 'meta_description' => '', 'noindex' => 1, 'retry_after' => 1, ); } public static function init() { add_action( 'template_redirect', array( __CLASS__, 'maybe_show' ), 0 ); add_filter( 'rest_authentication_errors', array( __CLASS__, 'maybe_block_rest' ) ); } public static function activate() { if ( false === get_option( self::OPTION ) ) { $settings = self::defaults(); $settings['bypass_key'] = self::generate_key(); add_option( self::OPTION, $settings ); } } public static function generate_key() { return strtolower( wp_generate_password( 16, false, false ) ); } /** * Get all settings, or a single key. * * @param string|null $key Setting key. * @return mixed */ public static function get( $key = null ) { $settings = wp_parse_args( (array) get_option( self::OPTION, array() ), self::defaults() ); if ( null === $key ) { return $settings; } return isset( $settings[ $key ] ) ? $settings[ $key ] : null; } public static function set_mode( $mode ) { if ( ! in_array( $mode, self::MODES, true ) ) { return; } $settings = self::get(); $settings['mode'] = $mode; update_option( self::OPTION, $settings ); } public static function is_active() { return 'off' !== self::get( 'mode' ); } /** * Launch date as a Unix timestamp in the site timezone, or 0. * * @return int */ public static function launch_timestamp() { $date = self::get( 'launch_date' ); if ( ! $date ) { return 0; } try { $dt = new DateTime( $date, wp_timezone() ); return $dt->getTimestamp(); } catch ( Exception $e ) { return 0; } } /** * Turn the site live automatically once the countdown has passed. */ private static function maybe_auto_disable() { $s = self::get(); if ( 'off' === $s['mode'] || ! $s['countdown_enabled'] || ! $s['auto_disable'] ) { return; } $ts = self::launch_timestamp(); if ( $ts && time() >= $ts ) { self::set_mode( 'off' ); } } /** * Whether the current visitor is allowed to see the real site. * * @return bool */ public static function visitor_can_bypass() { $s = self::get(); if ( is_user_logged_in() ) { if ( current_user_can( 'manage_options' ) ) { return true; } $user = wp_get_current_user(); if ( array_intersect( (array) $user->roles, (array) $s['bypass_roles'] ) ) { return true; } } if ( $s['bypass_key'] && isset( $_COOKIE[ self::COOKIE ] ) ) { $cookie = sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE ] ) ); if ( hash_equals( self::cookie_value( $s['bypass_key'] ), $cookie ) ) { return true; } } $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; if ( $ip && in_array( $ip, self::lines( $s['allowed_ips'] ), true ) ) { return true; } return false; } private static function cookie_value( $key ) { return hash_hmac( 'sha256', $key, wp_salt( 'auth' ) ); } private static function lines( $text ) { return array_values( array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', (string) $text ) ) ) ); } /** * Whether the requested path matches an excluded path rule. * * @return bool */ private static function is_excluded_path() { $rules = self::lines( self::get( 'excluded_paths' ) ); if ( ! $rules ) { return false; } $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); // Make the path relative to the home URL (sub-directory installs). $home_path = untrailingslashit( (string) wp_parse_url( home_url(), PHP_URL_PATH ) ); if ( $home_path && 0 === strpos( $path, $home_path ) ) { $path = substr( $path, strlen( $home_path ) ); } $path = '/' . trim( $path, '/' ); foreach ( $rules as $rule ) { $rule = '/' . trim( wp_parse_url( $rule, PHP_URL_PATH ) ?? $rule, '/' ); if ( '*' === substr( $rule, -1 ) ) { if ( 0 === strpos( $path, rtrim( substr( $rule, 0, -1 ), '/' ) ) ) { return true; } } elseif ( $path === $rule ) { return true; } } return false; } /** * Handle the secret bypass link: ?sitehold_bypass=KEY. */ private static function maybe_handle_bypass_link() { if ( ! isset( $_GET['sitehold_bypass'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $given = sanitize_key( wp_unslash( $_GET['sitehold_bypass'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $key = self::get( 'bypass_key' ); if ( ! $key || ! hash_equals( $key, $given ) ) { return; } $days = max( 1, (int) self::get( 'bypass_days' ) ); setcookie( self::COOKIE, self::cookie_value( $key ), time() + $days * DAY_IN_SECONDS, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, is_ssl(), true ); wp_safe_redirect( remove_query_arg( 'sitehold_bypass' ) ); exit; } /** * Main front-end gate. */ public static function maybe_show() { if ( is_admin() || wp_doing_ajax() || wp_doing_cron() || is_robots() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { return; } // Admin preview: ?sitehold_preview=coming_soon|maintenance. if ( isset( $_GET['sitehold_preview'] ) && current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $mode = sanitize_key( wp_unslash( $_GET['sitehold_preview'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended self::render( 'maintenance' === $mode ? 'maintenance' : 'coming_soon' ); } self::maybe_auto_disable(); if ( ! self::is_active() ) { return; } self::maybe_handle_bypass_link(); if ( self::visitor_can_bypass() || self::is_excluded_path() ) { return; } self::render( self::get( 'mode' ) ); } /** * Output the holding page and stop. * * @param string $mode coming_soon|maintenance. */ public static function render( $mode ) { $s = self::get(); if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Standard constant read by caching plugins. } nocache_headers(); if ( 'maintenance' === $mode ) { status_header( 503 ); header( 'Retry-After: ' . max( 1, (int) $s['retry_after'] ) * HOUR_IN_SECONDS ); } else { status_header( 200 ); } if ( $s['noindex'] ) { header( 'X-Robots-Tag: noindex, nofollow', true ); } header( 'Content-Type: text/html; charset=' . get_bloginfo( 'charset' ) ); self::enqueue_assets(); $template = apply_filters( 'sitehold_template', SITEHOLD_DIR . 'templates/page.php', $mode ); include $template; exit; } /** * Register the holding page assets. The page is a standalone document, so the * template prints them with wp_print_styles() / wp_print_scripts(). */ private static function enqueue_assets() { $s = self::get(); foreach ( array( 'bg_color', 'text_color', 'accent_color' ) as $k ) { $color = sanitize_hex_color( $s[ $k ] ); $s[ $k ] = $color ? $color : self::defaults()[ $k ]; } $bg_image = $s['bg_image_id'] ? wp_get_attachment_image_url( $s['bg_image_id'], 'full' ) : ''; wp_enqueue_style( 'sitehold-front', SITEHOLD_URL . 'assets/front.css', array(), SITEHOLD_VERSION ); $vars = sprintf( ':root{--sh-bg:%1$s;--sh-bg-rgb:%2$s;--sh-text:%3$s;--sh-accent:%4$s;--sh-overlay:%5$s;%6$s}', $s['bg_color'], self::hex_to_rgb( $s['bg_color'] ), $s['text_color'], $s['accent_color'], $bg_image ? (string) ( (int) $s['overlay'] / 100 ) : '0', $bg_image ? '--sh-bg-image:url("' . esc_url( $bg_image ) . '");' : '' ); wp_add_inline_style( 'sitehold-front', $vars ); if ( $s['countdown_enabled'] && self::launch_timestamp() > time() ) { wp_enqueue_script( 'sitehold-front', SITEHOLD_URL . 'assets/front.js', array(), SITEHOLD_VERSION, true ); } } /** * Convert a sanitized hex colour to an "r,g,b" string. * * @param string $hex Hex colour. * @return string */ private static function hex_to_rgb( $hex ) { $hex = ltrim( (string) $hex, '#' ); if ( 3 === strlen( $hex ) ) { $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } return hexdec( substr( $hex, 0, 2 ) ) . ',' . hexdec( substr( $hex, 2, 2 ) ) . ',' . hexdec( substr( $hex, 4, 2 ) ); } /** * Optionally block the REST API for visitors while active. * * @param WP_Error|null|true $result Existing result. * @return WP_Error|null|true */ public static function maybe_block_rest( $result ) { if ( null !== $result || ! self::is_active() || ! self::get( 'block_rest' ) || self::visitor_can_bypass() ) { return $result; } return new WP_Error( 'sitehold_rest_disabled', __( 'The site is currently unavailable.', 'sitehold' ), array( 'status' => 503 ) ); } }