'', 'client_id' => '', 'client_secret' => '', // Stored encrypted. 'prompt' => 'select_account', // Security / matching. 'upn_fallback' => 1, 'bind_oid' => 1, 'allowed_domains' => '', 'remember_me' => 0, // Button appearance. 'button_text' => __( 'Sign in with Microsoft', 'm365-login' ), 'button_icon' => '', // Empty = bundled Microsoft logo. 'button_show_icon' => 1, 'button_bg' => '#2f2f2f', 'button_bg_hover' => '#1a1a1a', 'button_color' => '#ffffff', 'button_border' => '#2f2f2f', 'button_radius' => 4, 'button_position' => 'below', 'divider_text' => __( 'or', 'm365-login' ), ); } /** * Returns all settings merged with defaults. * * @return array */ public function all() { if ( null === $this->cache ) { $stored = get_option( M365_LOGIN_OPTION, array() ); $this->cache = wp_parse_args( is_array( $stored ) ? $stored : array(), $this->defaults() ); } return $this->cache; } /** * Returns a single setting. * * @param string $key Setting key. * @param mixed $default Fallback. * @return mixed */ public function get( $key, $default = null ) { $all = $this->all(); return array_key_exists( $key, $all ) ? $all[ $key ] : $default; } /** * Decrypted client secret. * * @return string */ public function client_secret() { $enc = (string) $this->get( 'client_secret', '' ); if ( '' === $enc ) { return ''; } $plain = M365_Login_Crypto::decrypt( $enc ); return is_string( $plain ) ? $plain : ''; } /** * Whether the plugin has everything it needs to start a login. * * @return bool */ public function is_configured() { return '' !== $this->get( 'tenant_id' ) && '' !== $this->get( 'client_id' ) && '' !== $this->client_secret(); } /** * Tenant segment used in Microsoft endpoints. * * @return string */ public function tenant() { $tenant = (string) $this->get( 'tenant_id', '' ); return '' === $tenant ? 'organizations' : $tenant; } /** * Redirect URI registered in Entra ID. * * @return string */ public function redirect_uri() { if ( $this->uses_pretty_callback() ) { $uri = home_url( '/m365-login/callback' ); } else { $uri = add_query_arg( 'm365-login', 'callback', home_url( '/' ) ); } /** * Filters the redirect URI registered in Entra ID. * * @param string $uri Redirect URI. */ return (string) apply_filters( 'm365_login_redirect_uri', $uri ); } /** * Whether the callback can use a path (requires rewrite rules) instead of a query argument. * * @return bool */ public function uses_pretty_callback() { return '' !== (string) get_option( 'permalink_structure', '' ); } /** * Allowed e-mail domains as an array (lowercase, no leading @). * * @return string[] */ public function allowed_domains() { $raw = (string) $this->get( 'allowed_domains', '' ); if ( '' === trim( $raw ) ) { return array(); } $parts = preg_split( '/[\s,;]+/', strtolower( $raw ) ); $out = array(); foreach ( $parts as $p ) { $p = ltrim( trim( $p ), '@' ); if ( '' !== $p ) { $out[] = $p; } } return array_values( array_unique( $out ) ); } /** * Sanitises settings coming from the admin form. * * @param array $input Raw input. * @return array */ public function sanitize( $input ) { $defaults = $this->defaults(); $current = $this->all(); $input = is_array( $input ) ? $input : array(); $out = $current; // Tenant: GUID or one of the well-known aliases. $tenant = isset( $input['tenant_id'] ) ? trim( sanitize_text_field( wp_unslash( $input['tenant_id'] ) ) ) : ''; $tenant = strtolower( $tenant ); if ( '' !== $tenant && ! self::is_valid_tenant( $tenant ) ) { add_settings_error( M365_LOGIN_OPTION, 'tenant_id', __( 'The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of "organizations", "common", "consumers".', 'm365-login' ) ); $tenant = $current['tenant_id']; } $out['tenant_id'] = $tenant; // Client ID: GUID. $client_id = isset( $input['client_id'] ) ? trim( sanitize_text_field( wp_unslash( $input['client_id'] ) ) ) : ''; if ( '' !== $client_id && ! self::is_guid( $client_id ) ) { add_settings_error( M365_LOGIN_OPTION, 'client_id', __( 'The application (client) ID must be a GUID.', 'm365-login' ) ); $client_id = $current['client_id']; } $out['client_id'] = strtolower( $client_id ); // Client secret: only replaced when a new value was entered. $secret_input = isset( $input['client_secret'] ) ? (string) wp_unslash( $input['client_secret'] ) : ''; $secret_input = trim( $secret_input ); if ( ! empty( $input['client_secret_clear'] ) ) { $out['client_secret'] = ''; } elseif ( '' !== $secret_input ) { if ( strlen( $secret_input ) > 512 || preg_match( '/[\x00-\x1F\x7F]/', $secret_input ) ) { add_settings_error( M365_LOGIN_OPTION, 'client_secret', __( 'The client secret contains invalid characters.', 'm365-login' ) ); } else { $enc = M365_Login_Crypto::encrypt( $secret_input ); if ( false === $enc ) { add_settings_error( M365_LOGIN_OPTION, 'client_secret', __( 'The client secret could not be encrypted. Is the OpenSSL extension available?', 'm365-login' ) ); } else { $out['client_secret'] = $enc; } } } $prompt = isset( $input['prompt'] ) ? sanitize_key( $input['prompt'] ) : ''; $out['prompt'] = in_array( $prompt, array( 'none', 'select_account', 'login' ), true ) ? $prompt : 'none'; $out['upn_fallback'] = empty( $input['upn_fallback'] ) ? 0 : 1; $out['bind_oid'] = empty( $input['bind_oid'] ) ? 0 : 1; $out['remember_me'] = empty( $input['remember_me'] ) ? 0 : 1; $domains = isset( $input['allowed_domains'] ) ? sanitize_textarea_field( wp_unslash( $input['allowed_domains'] ) ) : ''; $domains = preg_replace( '/[^a-z0-9.\-@,;\s]/i', '', $domains ); $out['allowed_domains'] = trim( (string) $domains ); // Button. $text = isset( $input['button_text'] ) ? sanitize_text_field( wp_unslash( $input['button_text'] ) ) : ''; $out['button_text'] = '' === trim( $text ) ? $defaults['button_text'] : mb_substr( $text, 0, 80 ); $icon = isset( $input['button_icon'] ) ? esc_url_raw( trim( wp_unslash( $input['button_icon'] ) ) ) : ''; $out['button_icon'] = self::is_safe_image_url( $icon ) ? $icon : ''; $out['button_show_icon'] = empty( $input['button_show_icon'] ) ? 0 : 1; foreach ( array( 'button_bg', 'button_bg_hover', 'button_color', 'button_border' ) as $color_key ) { $color = isset( $input[ $color_key ] ) ? sanitize_hex_color( trim( wp_unslash( $input[ $color_key ] ) ) ) : ''; $out[ $color_key ] = $color ? $color : $defaults[ $color_key ]; } $radius = isset( $input['button_radius'] ) ? absint( $input['button_radius'] ) : $defaults['button_radius']; $out['button_radius'] = min( 50, $radius ); $position = isset( $input['button_position'] ) ? sanitize_key( $input['button_position'] ) : 'below'; $out['button_position'] = in_array( $position, array( 'above', 'below' ), true ) ? $position : 'below'; $divider = isset( $input['divider_text'] ) ? sanitize_text_field( wp_unslash( $input['divider_text'] ) ) : ''; $out['divider_text'] = mb_substr( $divider, 0, 40 ); $this->cache = null; return $out; } /** * Checks a GUID. * * @param string $value Value. * @return bool */ public static function is_guid( $value ) { return (bool) preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value ); } /** * Checks a tenant identifier. * * @param string $value Value. * @return bool */ public static function is_valid_tenant( $value ) { return self::is_guid( $value ) || in_array( $value, array( 'organizations', 'common', 'consumers' ), true ); } /** * Only allows http(s) image URLs with a known image extension. * * @param string $url URL. * @return bool */ public static function is_safe_image_url( $url ) { if ( '' === $url ) { return false; } $parts = wp_parse_url( $url ); if ( empty( $parts['scheme'] ) || ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) ) { return false; } $path = isset( $parts['path'] ) ? strtolower( $parts['path'] ) : ''; return (bool) preg_match( '/\.(png|jpe?g|gif|svg|webp)$/', $path ); } }