ID ) ) { $allcaps[ self::capability() ] = true; } return $allcaps; } /** * Sanitize a single logo item coming from the editor / AJAX / import. * * @param array $logo Raw logo data. * @return array Sanitized logo data. */ public static function sanitize_logo( $logo ) { $logo = is_array( $logo ) ? $logo : array(); $target = isset( $logo['target'] ) && '_blank' === $logo['target'] ? '_blank' : '_self'; return array( 'image_id' => isset( $logo['image_id'] ) ? absint( $logo['image_id'] ) : 0, 'url' => isset( $logo['url'] ) ? esc_url_raw( trim( (string) $logo['url'] ) ) : '', 'title' => isset( $logo['title'] ) ? sanitize_text_field( wp_unslash( $logo['title'] ) ) : '', 'alt' => isset( $logo['alt'] ) ? sanitize_text_field( wp_unslash( $logo['alt'] ) ) : '', 'target' => $target, 'class' => isset( $logo['class'] ) ? self::sanitize_html_classes( $logo['class'] ) : '', 'order' => isset( $logo['order'] ) ? absint( $logo['order'] ) : 0, ); } /** * Sanitize a full array of logos. * * @param array $logos Raw logos array. * @return array */ public static function sanitize_logos( $logos ) { if ( ! is_array( $logos ) ) { return array(); } $sanitized = array(); $order = 0; foreach ( $logos as $logo ) { $clean = self::sanitize_logo( $logo ); // A logo with no image attached is not usable; skip it. if ( empty( $clean['image_id'] ) ) { continue; } $clean['order'] = $order; $sanitized[] = $clean; ++$order; } return $sanitized; } /** * Sanitize a free-form list of CSS classes provided by an admin user. * * @param string $classes Raw class string. * @return string */ public static function sanitize_html_classes( $classes ) { $classes = sanitize_text_field( wp_unslash( (string) $classes ) ); $parts = preg_split( '/[\s,]+/', $classes, -1, PREG_SPLIT_NO_EMPTY ); $parts = array_map( 'sanitize_html_class', (array) $parts ); return implode( ' ', array_filter( $parts ) ); } /** * Sanitize the full slider settings array against a schema of defaults. * * Any key not present in defaults is discarded; any missing key falls * back to its default so the settings array is always complete. * * @param array $settings Raw settings. * @param array $defaults Default/schema settings. * @return array */ public static function sanitize_settings( $settings, $defaults ) { $settings = is_array( $settings ) ? $settings : array(); $clean = array(); foreach ( $defaults as $key => $default_value ) { $raw = array_key_exists( $key, $settings ) ? $settings[ $key ] : $default_value; if ( is_bool( $default_value ) ) { $clean[ $key ] = self::sanitize_bool( $raw ); } elseif ( is_int( $default_value ) ) { $clean[ $key ] = intval( $raw ); } elseif ( is_float( $default_value ) ) { $clean[ $key ] = floatval( $raw ); } elseif ( is_array( $default_value ) ) { $clean[ $key ] = self::sanitize_settings( $raw, $default_value ); } elseif ( self::looks_like_color( $key ) ) { $clean[ $key ] = self::sanitize_color( $raw ); } else { $clean[ $key ] = sanitize_text_field( wp_unslash( (string) $raw ) ); } } // Width is a bounded pixel quantity, never an arbitrary CSS expression. if ( isset( $clean['container_width'] ) ) { $clean['container_width'] = min( 10000, max( 0, absint( $clean['container_width'] ) ) ); } // Responsive breakpoints must descend from Desktop to Tablet to Mobile. // If an invalid/overlapping combination is submitted, use the schema // defaults so frontend behavior remains deterministic at every width. if ( isset( $defaults['breakpoint_desktop'], $defaults['breakpoint_tablet'], $defaults['breakpoint_mobile'] ) ) { $desktop = max( 1, absint( $clean['breakpoint_desktop'] ) ); $tablet = max( 1, absint( $clean['breakpoint_tablet'] ) ); $mobile = max( 1, absint( $clean['breakpoint_mobile'] ) ); if ( $desktop <= $tablet || $tablet <= $mobile ) { $desktop = max( 1, absint( $defaults['breakpoint_desktop'] ) ); $tablet = max( 1, absint( $defaults['breakpoint_tablet'] ) ); $mobile = max( 1, absint( $defaults['breakpoint_mobile'] ) ); } $clean['breakpoint_desktop'] = $desktop; $clean['breakpoint_tablet'] = $tablet; $clean['breakpoint_mobile'] = $mobile; } return $clean; } /** * Determine (heuristically) whether a settings key represents a color value. * * @param string $key Settings key. * @return bool */ protected static function looks_like_color( $key ) { return ( false !== strpos( $key, 'color' ) || false !== strpos( $key, 'background' ) ); } /** * Sanitize a color value (hex, rgb/rgba, or empty). * * @param string $value Raw color value. * @return string */ public static function sanitize_color( $value ) { $value = trim( (string) $value ); if ( '' === $value ) { return ''; } $hex = sanitize_hex_color( $value ); if ( $hex ) { return $hex; } // Allow rgb()/rgba() values, stripped of anything unsafe. if ( preg_match( '/^rgba?\(\s*[\d\.\s,]+\)$/i', $value ) ) { return preg_replace( '/[^0-9a-z.,()\s%]/i', '', $value ); } return ''; } /** * Sanitize a boolean-ish value coming from checkboxes, AJAX, or shortcode attrs. * * @param mixed $value Raw value. * @return bool */ public static function sanitize_bool( $value ) { if ( is_bool( $value ) ) { return $value; } return in_array( strtolower( (string) $value ), array( '1', 'true', 'yes', 'on' ), true ); } /** * Verify a nonce for a standard admin-post form submission and stop * execution with an error if verification fails. * * @param string $nonce_field Name of the $_POST field containing the nonce. * @param string $action Nonce action. * @return void */ public static function verify_admin_nonce_or_die( $nonce_field, $action ) { if ( ! isset( $_POST[ $nonce_field ] ) || ! check_admin_referer( $action, $nonce_field ) ) { wp_die( esc_html__( 'Security check failed. Please reload the page and try again.', 'developersd-logo-slider' ), esc_html__( 'DeveloperSD Logo Slider', 'developersd-logo-slider' ), array( 'response' => 403 ) ); } } /** * Verify an AJAX nonce and stop execution with a JSON error if invalid. * * @param string $nonce Nonce value. * @return void */ public static function verify_ajax_nonce_or_die( $nonce ) { if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION_AJAX ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed. Please reload the page and try again.', 'developersd-logo-slider' ) ), 403 ); } } } /** * Register the Multisite Super Admin capability grant. This is safe to add * unconditionally: on non-Multisite installs is_multisite() short-circuits * the callback to a no-op, and even on Multisite it only ever adds the one * capability this plugin requires, only for confirmed network Super Admins. */ add_filter( 'user_has_cap', array( 'DeveloperSD_LS_Security', 'grant_super_admin_cap' ), 10, 4 );