store_certificate() and remove_certificate() wrote the option with update_option(), which runs the registered sanitize() callback in the admin (including admin-ajax). sanitize() expects raw form input, so it restored the previous certificate fields and encrypted the stored client secret a second time: "Generate certificate" did not keep the new certificate and broke an existing client secret. Internal writes now bypass the form sanitiser. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
596 lines
18 KiB
PHP
596 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Settings storage and sanitisation.
|
|
*
|
|
* @package M365_Login
|
|
*/
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
/**
|
|
* Reads, sanitises and writes plugin settings.
|
|
*/
|
|
class M365_Login_Settings {
|
|
|
|
/**
|
|
* Cached settings.
|
|
*
|
|
* @var array|null
|
|
*/
|
|
private $cache = null;
|
|
|
|
/**
|
|
* Set while the plugin writes already sanitised values (skips the form sanitiser).
|
|
*
|
|
* @var bool
|
|
*/
|
|
private $raw_write = false;
|
|
|
|
/**
|
|
* Default settings.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function defaults() {
|
|
return array(
|
|
// Connection.
|
|
'tenant_id' => '',
|
|
'client_id' => '',
|
|
'client_secret' => '', // Stored encrypted.
|
|
'auth_method' => 'secret', // 'secret' or 'certificate'.
|
|
'cert_private_key' => '', // PEM, stored encrypted.
|
|
'cert_certificate' => '', // PEM (public).
|
|
'prompt' => 'select_account',
|
|
// Security / matching.
|
|
'upn_fallback' => 1,
|
|
'bind_oid' => 1,
|
|
'allowed_domains' => '',
|
|
'allowed_groups' => array(), // id => display name.
|
|
'remember_me' => 0,
|
|
// Button-only mode.
|
|
'button_only' => 0,
|
|
'fallback_key' => '',
|
|
// 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' ),
|
|
// Custom login pages.
|
|
'custom_login_url' => '',
|
|
'inject_form' => 1, // Add the button to wp_login_form() output.
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 : '';
|
|
}
|
|
|
|
/**
|
|
* Selected client authentication method.
|
|
*
|
|
* @return string 'secret' or 'certificate'.
|
|
*/
|
|
public function auth_method() {
|
|
return 'certificate' === $this->get( 'auth_method' ) ? 'certificate' : 'secret';
|
|
}
|
|
|
|
/**
|
|
* Decrypted certificate private key (PEM) or ''.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function certificate_key() {
|
|
$enc = (string) $this->get( 'cert_private_key', '' );
|
|
if ( '' === $enc ) {
|
|
return '';
|
|
}
|
|
$plain = M365_Login_Crypto::decrypt( $enc );
|
|
return is_string( $plain ) ? $plain : '';
|
|
}
|
|
|
|
/**
|
|
* Certificate PEM (public part) or ''.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function certificate_pem() {
|
|
return (string) $this->get( 'cert_certificate', '' );
|
|
}
|
|
|
|
/**
|
|
* Whether a usable certificate + key pair is stored.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function has_certificate() {
|
|
return '' !== $this->certificate_pem() && '' !== $this->certificate_key();
|
|
}
|
|
|
|
/**
|
|
* Parsed certificate metadata or null.
|
|
*
|
|
* @return array|null
|
|
*/
|
|
public function certificate_info() {
|
|
return $this->has_certificate() ? M365_Login_Certificate::info( $this->certificate_pem() ) : null;
|
|
}
|
|
|
|
/**
|
|
* Whether the plugin has everything it needs to start a login.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_configured() {
|
|
if ( '' === $this->get( 'tenant_id' ) || '' === $this->get( 'client_id' ) ) {
|
|
return false;
|
|
}
|
|
if ( 'certificate' === $this->auth_method() ) {
|
|
$info = $this->certificate_info();
|
|
return null !== $info && ( 0 === $info['not_after'] || $info['not_after'] > time() );
|
|
}
|
|
return '' !== $this->client_secret();
|
|
}
|
|
|
|
/**
|
|
* Client authentication parameters for the token endpoint (secret or signed assertion).
|
|
*
|
|
* @param string $token_endpoint Token endpoint URL (assertion audience).
|
|
* @return array|WP_Error
|
|
*/
|
|
public function client_auth_params( $token_endpoint ) {
|
|
if ( 'certificate' === $this->auth_method() ) {
|
|
$assertion = M365_Login_Certificate::assertion( $this->certificate_key(), $this->certificate_pem(), (string) $this->get( 'client_id' ), $token_endpoint );
|
|
if ( is_wp_error( $assertion ) ) {
|
|
return $assertion;
|
|
}
|
|
return array(
|
|
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
|
'client_assertion' => $assertion,
|
|
);
|
|
}
|
|
return array( 'client_secret' => $this->client_secret() );
|
|
}
|
|
|
|
/**
|
|
* Stores a validated key/certificate pair (key encrypted).
|
|
*
|
|
* @param array $pair array( 'private_key' => PEM, 'certificate' => PEM ).
|
|
* @return true|WP_Error
|
|
*/
|
|
public function store_certificate( $pair ) {
|
|
$enc = M365_Login_Crypto::encrypt( $pair['private_key'] );
|
|
if ( false === $enc ) {
|
|
return new WP_Error( 'encrypt', __( 'The private key could not be encrypted. Is the OpenSSL extension available?', 'm365-login' ) );
|
|
}
|
|
$all = $this->all();
|
|
$all['cert_private_key'] = $enc;
|
|
$all['cert_certificate'] = $pair['certificate'];
|
|
$this->write( $all );
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Removes the stored certificate and key.
|
|
*/
|
|
public function remove_certificate() {
|
|
$all = $this->all();
|
|
$all['cert_private_key'] = '';
|
|
$all['cert_certificate'] = '';
|
|
$this->write( $all );
|
|
}
|
|
|
|
/**
|
|
* Stores already sanitised settings.
|
|
*
|
|
* The option is registered with sanitize() as callback, which expects raw form input
|
|
* (it would, for example, encrypt the stored client secret a second time).
|
|
*
|
|
* @param array $all Complete settings.
|
|
*/
|
|
private function write( $all ) {
|
|
$this->raw_write = true;
|
|
update_option( M365_LOGIN_OPTION, $all );
|
|
$this->raw_write = false;
|
|
$this->cache = null;
|
|
}
|
|
|
|
/**
|
|
* Tenant segment used in Microsoft endpoints.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function tenant() {
|
|
$tenant = (string) $this->get( 'tenant_id', '' );
|
|
return '' === $tenant ? 'organizations' : $tenant;
|
|
}
|
|
|
|
/**
|
|
* Whether sign-ins from more than one tenant are accepted (no tenant GUID pinned).
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_multi_tenant() {
|
|
return ! self::is_guid( $this->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 ) );
|
|
}
|
|
|
|
/**
|
|
* Allowed Entra group IDs (lowercase GUIDs) mapped to display names.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function allowed_groups() {
|
|
$raw = $this->get( 'allowed_groups', array() );
|
|
$out = array();
|
|
if ( is_array( $raw ) ) {
|
|
foreach ( $raw as $id => $name ) {
|
|
$id = strtolower( (string) $id );
|
|
if ( self::is_guid( $id ) ) {
|
|
$out[ $id ] = (string) $name;
|
|
}
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Whether the password form is hidden and password sign-in blocked.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function button_only() {
|
|
if ( defined( 'M365_LOGIN_DISABLE_BUTTON_ONLY' ) && M365_LOGIN_DISABLE_BUTTON_ONLY ) {
|
|
return false;
|
|
}
|
|
return $this->is_configured() && (bool) $this->get( 'button_only' ) && '' !== $this->fallback_key();
|
|
}
|
|
|
|
/**
|
|
* Secret key that re-enables the password form.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function fallback_key() {
|
|
$key = (string) $this->get( 'fallback_key', '' );
|
|
return preg_match( '/^[A-Za-z0-9]{16,64}$/', $key ) ? $key : '';
|
|
}
|
|
|
|
/**
|
|
* URL that shows the password form again when button-only mode is active.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function fallback_url() {
|
|
$key = $this->fallback_key();
|
|
return '' === $key ? '' : add_query_arg( 'm365_fallback', $key, $this->login_page_url() );
|
|
}
|
|
|
|
/**
|
|
* URL of a custom login page (same site), or empty string.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function custom_login_url() {
|
|
$url = (string) $this->get( 'custom_login_url', '' );
|
|
if ( '' === $url ) {
|
|
return '';
|
|
}
|
|
$validated = wp_validate_redirect( $url, '' );
|
|
return is_string( $validated ) ? $validated : '';
|
|
}
|
|
|
|
/**
|
|
* Where users land after a failed Microsoft sign-in (custom page or wp-login.php).
|
|
*
|
|
* @return string
|
|
*/
|
|
public function login_page_url() {
|
|
$custom = $this->custom_login_url();
|
|
return '' !== $custom ? $custom : wp_login_url();
|
|
}
|
|
|
|
/**
|
|
* Generates a new fallback key.
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function generate_fallback_key() {
|
|
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
|
|
$key = '';
|
|
for ( $i = 0; $i < 24; $i++ ) {
|
|
$key .= $alphabet[ random_int( 0, strlen( $alphabet ) - 1 ) ];
|
|
}
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Sanitises settings coming from the admin form.
|
|
*
|
|
* @param array $input Raw input.
|
|
* @return array
|
|
*/
|
|
public function sanitize( $input ) {
|
|
if ( $this->raw_write ) {
|
|
return $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;
|
|
}
|
|
}
|
|
}
|
|
|
|
$method = isset( $input['auth_method'] ) ? sanitize_key( $input['auth_method'] ) : 'secret';
|
|
$out['auth_method'] = 'certificate' === $method ? 'certificate' : 'secret';
|
|
|
|
// Certificate: keep the stored pair unless a new one is pasted or removal is requested.
|
|
$out['cert_private_key'] = $current['cert_private_key'];
|
|
$out['cert_certificate'] = $current['cert_certificate'];
|
|
$pasted_key = isset( $input['cert_key_pem'] ) ? trim( (string) wp_unslash( $input['cert_key_pem'] ) ) : '';
|
|
$pasted_cert = isset( $input['cert_cert_pem'] ) ? trim( (string) wp_unslash( $input['cert_cert_pem'] ) ) : '';
|
|
if ( ! empty( $input['cert_remove'] ) ) {
|
|
$out['cert_private_key'] = '';
|
|
$out['cert_certificate'] = '';
|
|
} elseif ( '' !== $pasted_key || '' !== $pasted_cert ) {
|
|
if ( '' === $pasted_key || '' === $pasted_cert ) {
|
|
add_settings_error( M365_LOGIN_OPTION, 'certificate', __( 'Please paste both the private key and the certificate.', 'm365-login' ) );
|
|
} elseif ( strlen( $pasted_key ) > 20000 || strlen( $pasted_cert ) > 20000 ) {
|
|
add_settings_error( M365_LOGIN_OPTION, 'certificate', __( 'The pasted key or certificate is too large.', 'm365-login' ) );
|
|
} else {
|
|
$pair = M365_Login_Certificate::from_pem( $pasted_key, $pasted_cert );
|
|
if ( is_wp_error( $pair ) ) {
|
|
add_settings_error( M365_LOGIN_OPTION, 'certificate', $pair->get_error_message() );
|
|
} else {
|
|
$enc = M365_Login_Crypto::encrypt( $pair['private_key'] );
|
|
if ( false === $enc ) {
|
|
add_settings_error( M365_LOGIN_OPTION, 'certificate', __( 'The private key could not be encrypted. Is the OpenSSL extension available?', 'm365-login' ) );
|
|
} else {
|
|
$out['cert_private_key'] = $enc;
|
|
$out['cert_certificate'] = $pair['certificate'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( 'certificate' === $out['auth_method'] && '' === $out['cert_certificate'] ) {
|
|
add_settings_error( M365_LOGIN_OPTION, 'auth_method', __( 'Certificate authentication is selected but no certificate is stored yet. Generate one or paste your own; the Microsoft button stays hidden until then.', 'm365-login' ), 'warning' );
|
|
}
|
|
|
|
$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 );
|
|
|
|
// Allowed groups: GUID => name.
|
|
$groups = array();
|
|
if ( ! empty( $input['allowed_groups'] ) && is_array( $input['allowed_groups'] ) ) {
|
|
foreach ( $input['allowed_groups'] as $id => $name ) {
|
|
$id = strtolower( trim( sanitize_text_field( wp_unslash( (string) $id ) ) ) );
|
|
if ( ! self::is_guid( $id ) ) {
|
|
continue;
|
|
}
|
|
$name = sanitize_text_field( wp_unslash( (string) $name ) );
|
|
$groups[ $id ] = '' === $name ? $id : mb_substr( $name, 0, 120 );
|
|
if ( count( $groups ) >= 100 ) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$out['allowed_groups'] = $groups;
|
|
|
|
// Button-only mode + fallback key.
|
|
$out['button_only'] = empty( $input['button_only'] ) ? 0 : 1;
|
|
$key = (string) $current['fallback_key'];
|
|
if ( ! empty( $input['fallback_regenerate'] ) || ! preg_match( '/^[A-Za-z0-9]{16,64}$/', $key ) ) {
|
|
$key = self::generate_fallback_key();
|
|
}
|
|
$out['fallback_key'] = $key;
|
|
|
|
// Custom login page (must be on this site).
|
|
$custom = isset( $input['custom_login_url'] ) ? esc_url_raw( trim( wp_unslash( $input['custom_login_url'] ) ) ) : '';
|
|
if ( '' !== $custom ) {
|
|
if ( 0 === strpos( $custom, '/' ) ) {
|
|
$custom = home_url( $custom );
|
|
}
|
|
if ( '' === wp_validate_redirect( $custom, '' ) ) {
|
|
add_settings_error( M365_LOGIN_OPTION, 'custom_login_url', __( 'The custom login page must be a URL on this site.', 'm365-login' ) );
|
|
$custom = $current['custom_login_url'];
|
|
}
|
|
}
|
|
$out['custom_login_url'] = $custom;
|
|
$out['inject_form'] = empty( $input['inject_form'] ) ? 0 : 1;
|
|
|
|
// 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 );
|
|
}
|
|
}
|