$rule ) { if ( isset( $rule['id'] ) && $rule['id'] === $rule_id ) { // Preserve original metadata. $validated['id'] = $rule_id; $validated['created_at'] = isset( $rule['created_at'] ) ? $rule['created_at'] : current_time( 'mysql' ); $validated['updated_at'] = current_time( 'mysql' ); $rules[ $index ] = $validated; $found = true; break; } } if ( ! $found ) { return new WP_Error( 'not_found', __( 'Rule not found.', 'gatewell' ) ); } update_option( self::OPTION_KEY, $rules ); return $validated; } /** * Delete a rule. * * @since 1.2.0 * @param string $rule_id The rule ID. * @return bool|WP_Error True on success, WP_Error on failure. */ public static function delete( $rule_id ) { $rules = self::get_all(); $found = false; foreach ( $rules as $index => $rule ) { if ( isset( $rule['id'] ) && $rule['id'] === $rule_id ) { unset( $rules[ $index ] ); $found = true; break; } } if ( ! $found ) { return new WP_Error( 'not_found', __( 'Rule not found.', 'gatewell' ) ); } // Re-index array. $rules = array_values( $rules ); update_option( self::OPTION_KEY, $rules ); return true; } /** * Toggle a rule's enabled state. * * @since 1.2.0 * @param string $rule_id The rule ID. * @return bool|WP_Error New enabled state, or WP_Error on failure. */ public static function toggle( $rule_id ) { $rules = self::get_all(); $new_state = null; foreach ( $rules as $index => $rule ) { if ( isset( $rule['id'] ) && $rule['id'] === $rule_id ) { $rules[ $index ]['enabled'] = ! $rule['enabled']; $new_state = $rules[ $index ]['enabled']; break; } } if ( null === $new_state ) { return new WP_Error( 'not_found', __( 'Rule not found.', 'gatewell' ) ); } update_option( self::OPTION_KEY, $rules ); return $new_state; } /** * Get the count of all rules. * * @since 1.2.0 * @return int Number of rules. */ public static function count() { return count( self::get_all() ); } /** * Validate rule data. * * Sanitizes and validates all rule fields. Returns a clean rule array * or WP_Error if validation fails. * * @since 1.2.0 * @param array $data Raw rule data. * @return array|WP_Error Validated rule data or WP_Error. */ public static function validate_rule( $data ) { $errors = array(); // Rule name (required). if ( empty( $data['name'] ) ) { $errors[] = __( 'Rule name is required.', 'gatewell' ); } $name = isset( $data['name'] ) ? sanitize_text_field( $data['name'] ) : ''; if ( strlen( $name ) > 100 ) { $errors[] = __( 'Rule name must be 100 characters or fewer.', 'gatewell' ); } // Targets (at least one required). $targets = self::validate_targets( isset( $data['targets'] ) ? $data['targets'] : array() ); if ( is_wp_error( $targets ) ) { $errors[] = $targets->get_error_message(); } // Constraints. $constraints = self::validate_constraints( isset( $data['constraints'] ) ? $data['constraints'] : array() ); if ( is_wp_error( $constraints ) ) { $errors[] = $constraints->get_error_message(); } // Return errors if any. if ( ! empty( $errors ) ) { return new WP_Error( 'validation_failed', implode( ' ', $errors ) ); } // Build validated rule. $rule = array( 'name' => $name, 'enabled' => isset( $data['enabled'] ) ? (bool) $data['enabled'] : true, 'priority' => self::validate_priority( isset( $data['priority'] ) ? $data['priority'] : 50 ), 'targets' => $targets, 'constraints' => $constraints, ); return $rule; } /** * Validate rule targets. * * @since 1.2.0 * @param array $targets Raw targets data. * @return array|WP_Error Validated targets or WP_Error. */ private static function validate_targets( $targets ) { if ( ! is_array( $targets ) ) { return new WP_Error( 'invalid_targets', __( 'Targets must be an array.', 'gatewell' ) ); } $validated = array(); // Post type target. if ( ! empty( $targets['post_type'] ) ) { $validated['post_type'] = sanitize_key( $targets['post_type'] ); } // Upload context target. if ( ! empty( $targets['context'] ) ) { $context = sanitize_key( $targets['context'] ); if ( in_array( $context, self::VALID_CONTEXTS, true ) ) { $validated['context'] = $context; } } // User role target. if ( ! empty( $targets['user_role'] ) ) { $validated['user_role'] = sanitize_key( $targets['user_role'] ); } // Must have at least one target. if ( empty( $validated ) ) { return new WP_Error( 'no_targets', __( 'At least one target is required (post type, upload context, or user role).', 'gatewell' ) ); } return $validated; } /** * Validate rule constraints. * * @since 1.2.0 * @param array $data Raw constraints data. * @return array|WP_Error Validated constraints or WP_Error. */ private static function validate_constraints( $data ) { if ( ! is_array( $data ) ) { return new WP_Error( 'invalid_constraints', __( 'Constraints must be an array.', 'gatewell' ) ); } $constraints = array(); // Size type. $size_type = isset( $data['size_type'] ) ? sanitize_key( $data['size_type'] ) : 'none'; if ( ! in_array( $size_type, self::VALID_SIZE_TYPES, true ) ) { $size_type = 'none'; } $constraints['size_type'] = $size_type; // Dimension constraints. if ( 'exact' === $size_type ) { $constraints['exact_width'] = self::validate_dimension( isset( $data['exact_width'] ) ? $data['exact_width'] : 0 ); $constraints['exact_height'] = self::validate_dimension( isset( $data['exact_height'] ) ? $data['exact_height'] : 0 ); if ( $constraints['exact_width'] <= 0 || $constraints['exact_height'] <= 0 ) { return new WP_Error( 'invalid_dimensions', __( 'Exact dimensions must be greater than zero.', 'gatewell' ) ); } } elseif ( 'min' === $size_type ) { $constraints['min_width'] = self::validate_dimension( isset( $data['min_width'] ) ? $data['min_width'] : 0 ); $constraints['min_height'] = self::validate_dimension( isset( $data['min_height'] ) ? $data['min_height'] : 0 ); } elseif ( 'max' === $size_type ) { $constraints['max_width'] = self::validate_dimension( isset( $data['max_width'] ) ? $data['max_width'] : GATEWELL_DEFAULT_MAX_WIDTH ); $constraints['max_height'] = self::validate_dimension( isset( $data['max_height'] ) ? $data['max_height'] : GATEWELL_DEFAULT_MAX_HEIGHT ); } elseif ( 'range' === $size_type ) { $constraints['min_width'] = self::validate_dimension( isset( $data['min_width'] ) ? $data['min_width'] : 0 ); $constraints['min_height'] = self::validate_dimension( isset( $data['min_height'] ) ? $data['min_height'] : 0 ); $constraints['max_width'] = self::validate_dimension( isset( $data['max_width'] ) ? $data['max_width'] : GATEWELL_DEFAULT_MAX_WIDTH ); $constraints['max_height'] = self::validate_dimension( isset( $data['max_height'] ) ? $data['max_height'] : GATEWELL_DEFAULT_MAX_HEIGHT ); // Min must not exceed max. if ( $constraints['min_width'] > $constraints['max_width'] ) { return new WP_Error( 'invalid_range', __( 'Minimum width cannot exceed maximum width.', 'gatewell' ) ); } if ( $constraints['min_height'] > $constraints['max_height'] ) { return new WP_Error( 'invalid_range', __( 'Minimum height cannot exceed maximum height.', 'gatewell' ) ); } } // Aspect ratio. if ( ! empty( $data['aspect_ratio'] ) ) { $ratio = sanitize_text_field( $data['aspect_ratio'] ); if ( preg_match( '/^\d+:\d+$/', $ratio ) ) { $constraints['aspect_ratio'] = $ratio; } } $constraints['lock_aspect_ratio'] = ! empty( $data['lock_aspect_ratio'] ); // Allowed formats. if ( ! empty( $data['formats'] ) && is_array( $data['formats'] ) ) { $formats = array_filter( $data['formats'], function ( $format ) { return in_array( $format, GATEWELL_Rule_Storage::VALID_FORMATS, true ); } ); if ( ! empty( $formats ) ) { $constraints['formats'] = array_values( $formats ); } } // Compression quality. if ( isset( $data['compression_quality'] ) ) { $quality = (int) $data['compression_quality']; $constraints['compression_quality'] = max( 1, min( 100, $quality ) ); } // Required fields. $constraints['alt_text_required'] = ! empty( $data['alt_text_required'] ); $constraints['title_required'] = ! empty( $data['title_required'] ); $constraints['caption_required'] = ! empty( $data['caption_required'] ); return $constraints; } /** * Validate a dimension value. * * @since 1.2.0 * @param mixed $value Raw dimension value. * @return int Validated dimension (0 or positive integer). */ private static function validate_dimension( $value ) { $value = (int) $value; return max( 0, min( 10000, $value ) ); } /** * Validate priority value. * * @since 1.2.0 * @param mixed $value Raw priority value. * @return int Validated priority (1-100). */ private static function validate_priority( $value ) { $value = (int) $value; return max( 1, min( 100, $value ) ); } /** * Get the default rule structure. * * Useful for pre-filling the rule builder form. * * @since 1.2.0 * @return array Default rule structure. */ public static function get_default_rule() { return array( 'name' => '', 'enabled' => true, 'priority' => 50, 'targets' => array( 'post_type' => '', 'context' => '', 'user_role' => '', ), 'constraints' => array( 'size_type' => 'none', 'exact_width' => 0, 'exact_height' => 0, 'min_width' => 0, 'min_height' => 0, 'max_width' => GATEWELL_DEFAULT_MAX_WIDTH, 'max_height' => GATEWELL_DEFAULT_MAX_HEIGHT, 'aspect_ratio' => '', 'lock_aspect_ratio' => false, 'formats' => array(), 'compression_quality' => GATEWELL_DEFAULT_COMPRESSION_QUALITY, 'alt_text_required' => false, 'title_required' => false, 'caption_required' => false, ), ); } /** * Get available post types for the rule builder. * * Returns public post types, excluding internal WordPress types. * * @since 1.2.0 * @return array Associative array of post_type => label. */ public static function get_available_post_types() { $post_types = get_post_types( array( 'public' => true ), 'objects' ); $excluded = array( 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation' ); $available = array(); foreach ( $post_types as $slug => $type ) { if ( ! in_array( $slug, $excluded, true ) ) { $available[ $slug ] = $type->labels->singular_name; } } return $available; } /** * Get available user roles for the rule builder. * * @since 1.2.0 * @return array Associative array of role_slug => role_name. */ public static function get_available_roles() { $wp_roles = wp_roles(); $available = array(); foreach ( $wp_roles->roles as $slug => $role ) { $available[ $slug ] = $role['name']; } return $available; } /** * Delete all rules. * * @since 1.2.0 * @return bool True on success. */ public static function delete_all() { return delete_option( self::OPTION_KEY ); } }