$max ) { return false; } $whole = intdiv( $bits, 8 ); $rem = $bits % 8; if ( $whole > 0 && 0 !== substr_compare( $ip_bin, $subnet_bin, 0, $whole ) ) { return false; } if ( 0 === $rem ) { return true; } $mask = chr( ( 0xff << ( 8 - $rem ) ) & 0xff ); return ( ord( $ip_bin[ $whole ] ) & ord( $mask ) ) === ( ord( $subnet_bin[ $whole ] ) & ord( $mask ) ); } /** * Whether an IP matches any entry in a list of IPs/CIDRs. * * @param string $ip Candidate IP. * @param string[] $list IPs/CIDRs. * @return bool */ function equasign_ip_in_list( $ip, array $list ) { foreach ( $list as $entry ) { if ( equasign_ip_in_cidr( $ip, $entry ) ) { return true; } } return false; } /** * Sanitizes a trusted-proxy list (textarea): keeps only syntactically valid IP or * CIDR entries, one per line; invalid lines are dropped. Never trusts content beyond * shape — the list only declares WHICH direct peers may have their forwarding header * believed. * * @param string $raw Raw textarea value. * @return string Newline-joined valid entries (deduplicated). */ function equasign_sanitize_proxy_list( $raw ) { $out = array(); foreach ( preg_split( '/[\r\n,]+/', (string) $raw ) as $line ) { $line = trim( (string) $line ); if ( '' === $line ) { continue; } if ( false === strpos( $line, '/' ) ) { if ( filter_var( $line, FILTER_VALIDATE_IP ) ) { $out[] = $line; } continue; } list( $subnet, $bits ) = explode( '/', $line, 2 ); if ( filter_var( $subnet, FILTER_VALIDATE_IP ) && ctype_digit( (string) $bits ) ) { $max = ( false !== strpos( $subnet, ':' ) ) ? 128 : 32; if ( (int) $bits >= 0 && (int) $bits <= $max ) { $out[] = $subnet . '/' . (int) $bits; } } } return implode( "\n", array_values( array_unique( $out ) ) ); } /** * Caller IP address. By default (no trusted proxy declared) this is REMOTE_ADDR * only — request headers are falsifiable and are NEVER trusted blindly. When the * operator declares trusted reverse proxies (admin setting) AND the direct peer is * one of them, the real client IP is taken from the forwarded chain: X-Forwarded-For * is walked right-to-left, skipping declared proxies, and the first non-proxy valid * address is returned; X-Real-IP is the fallback. This lets the proof record the * signer's real public IP behind a reverse proxy / load balancer / CDN, without ever * trusting an unverified hop. * * @return string IP or empty string. */ function equasign_client_ip() { $remote = isset( $_SERVER['REMOTE_ADDR'] ) ? trim( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) : ''; $remote = filter_var( $remote, FILTER_VALIDATE_IP ) ? $remote : ''; $trusted = equasign_trusted_proxies(); if ( '' === $remote || ! $trusted || ! equasign_ip_in_list( $remote, $trusted ) ) { return $remote; // Default posture: direct peer, no header trust. } // Direct peer is a declared trusted proxy → consult the forwarded chain. $forwarded = ''; if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $hops = array_reverse( array_map( 'trim', explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ); foreach ( $hops as $hop ) { // XFF entries may be "ip", "ip:port" or "[v6]:port"; normalize lightly. $hop = preg_replace( '/^\[(.+)\](:\d+)?$/', '$1', $hop ); if ( ! filter_var( $hop, FILTER_VALIDATE_IP ) && false !== strrpos( $hop, ':' ) && substr_count( $hop, ':' ) === 1 ) { $hop = substr( $hop, 0, strrpos( $hop, ':' ) ); // strip :port (IPv4). } if ( filter_var( $hop, FILTER_VALIDATE_IP ) && ! equasign_ip_in_list( $hop, $trusted ) ) { $forwarded = $hop; break; } } } if ( '' === $forwarded && ! empty( $_SERVER['HTTP_X_REAL_IP'] ) ) { $real = trim( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ) ); if ( filter_var( $real, FILTER_VALIDATE_IP ) ) { $forwarded = $real; } } return '' !== $forwarded ? $forwarded : $remote; } /** * Caller User-Agent, length-bounded (VARCHAR(255) column). * * @return string */ function equasign_user_agent() { $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $ua = is_string( $ua ) ? trim( $ua ) : ''; return mb_substr( sanitize_text_field( $ua ), 0, 255 ); } /** * Format a UTC DATETIME for admin display, according to the chosen timezone * (WP local equivalent or UTC; storage always stays UTC). * * @param string|null $datetime_utc UTC DATETIME. * @return string Displayable date, or "—" if empty. */ function equasign_display_date( $datetime_utc ) { if ( empty( $datetime_utc ) ) { return '—'; } $ts = strtotime( $datetime_utc . ' UTC' ); if ( false === $ts ) { return '—'; } $format = get_option( 'date_format', 'Y-m-d' ) . ' ' . get_option( 'time_format', 'H:i' ); if ( 'utc' === get_option( 'equasign_display_timezone', 'wp' ) ) { return gmdate( $format, $ts ) . ' UTC'; } // WordPress local timezone. return function_exists( 'wp_date' ) ? wp_date( $format, $ts ) : gmdate( $format, $ts ); } /** * Maps an internal REST error CODE to a clean, user-facing label. Internal slugs * must NEVER be shown in the UI — they stay available on the API/logs for * diagnosis. Unknown codes fall back to the server-provided sentence * (already human-readable), then to $default. Translatable; the admin locale * applies with an English fallback. * * @param string $code Internal error code. * @param string $message Server-provided message (may be empty). * @param string $default Ultimate fallback. * @return string */ function equasign_error_label( $code, $message = '', $default = '' ) { $map = array( 'license_unavailable' => __( 'The licensing service is temporarily unavailable. Please try again in a moment.', 'equasign' ), 'document_too_large' => __( 'A document exceeds the maximum allowed size.', 'equasign' ), 'vault_unavailable' => __( 'The document could not be saved. Please try again.', 'equasign' ), 'invalid_key' => __( 'Authorization failed — check your access.', 'equasign' ), // NOTE: `invalid_payload` is deliberately NOT mapped — its server messages are // already clean, specific validation sentences (e.g. "A document is not a // readable PDF.") with no internal slug, and are far more useful than a generic // label. They fall through to the message branch below. ); if ( isset( $map[ (string) $code ] ) ) { return $map[ (string) $code ]; } if ( '' !== (string) $message ) { return (string) $message; // Server sentence — human-readable, never a slug. } return '' !== (string) $default ? (string) $default : __( 'The operation could not be completed.', 'equasign' ); } /** * Reserved notice hook. This build never limits signature volume, so there is nothing * to announce; it always returns ''. Kept so its call sites need no change. * * @return string Always '' in this build. */ function equasign_quota_notice_html() { // No volume limit in this build, so there is nothing to announce. Kept as a no-op so // its call sites need no change; the discreet "Upgrade to Pro" bar // (EquaSign_Admin::pro_bar) is the single upsell surface. return ''; } /** * Discreet logging (only when WP_DEBUG). Never leaks a secret: callers are * responsible for not passing plaintext OTP/key. * * @param string $message Message. * @return void */ function equasign_log( $message ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( '[EquaSign] ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions } } /** * Maps a base language code (as resolved for a signer journey — 'fr', 'es', …) to * the full WordPress locale of the natively shipped set ('fr_FR', …). Returns '' * for English or an unknown code, so callers keep the current (admin) locale. * * @param string $lang Base language code. * @return string WordPress locale, or '' when none applies. */ function equasign_locale_for_lang( $lang ) { $map = array( 'fr' => 'fr_FR', 'es' => 'es_ES', 'de' => 'de_DE', 'pt' => 'pt_PT' ); $lang = strtolower( (string) $lang ); $lang = preg_replace( '/[^a-z].*$/', '', $lang ); return isset( $map[ $lang ] ) ? $map[ $lang ] : ''; } /** * Maps a creation-form invitation choice to the REST create payload fields. The * "default" choice returns an EMPTY array, leaving send_mail out so the * REST layer applies the document type's default channel; the explicit choices set * both booleans (so they override the type default). * * @param string $choice One of 'default' | 'email' | 'none'. * @return array Fields to merge into the create payload. */ function equasign_invite_fields( $choice ) { switch ( $choice ) { case 'email': return array( 'send_mail' => true ); case 'none': return array( 'send_mail' => false ); default: return array(); // 'default' → apply the type default (omit the fields). } } if ( ! function_exists( 'equasign_pro_page_url' ) ) { /** * The public "how to install EquaSign Pro" page URL, linked from the Free build's * "Upgrade to Pro" surfaces. Base is the filterable EQUASIGN_PRO_URL_BASE constant * (default https://equasuite.equadev.fr); the final URL is also filterable via * 'equasign_pro_url'. The plugin only LINKS to this page (new tab) — it downloads and * installs nothing. Common to both builds (only the Free surfaces consume it). * * @return string */ function equasign_pro_page_url() { $base = defined( 'EQUASIGN_PRO_URL_BASE' ) ? EQUASIGN_PRO_URL_BASE : 'https://equasuite.equadev.fr'; $url = rtrim( (string) $base, '/' ) . '/pro/how-to-install?plugin=equasign'; return (string) apply_filters( 'equasign_pro_url', $url ); } }