base_prefix, $wpdb->prefix ) ) as $prefix ) { if ( preg_match( '/^' . preg_quote( $prefix, '/' ) . '(?:[0-9]+_)?(?:capabilities|user_level)$/D', $name ) ) { return true; } } return false; } public static function targets() { return apply_filters( 'customfields_group_targets', array( 'post' => __( 'Posts and pages', 'customfields-com' ), 'user' => __( 'User profiles', 'customfields-com' ), 'term' => __( 'Taxonomy terms', 'customfields-com' ) ) ); } public static function decode( $json ) { if ( ! is_string( $json ) || strlen( $json ) > self::MAX_BYTES ) { return new \WP_Error( 'schema_size', __( 'Schema JSON must be at most 1 MB.', 'customfields-com' ) ); } $data = json_decode( $json, true, 24 ); if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) { return new \WP_Error( 'schema_json', __( 'The schema is not valid JSON.', 'customfields-com' ) ); } if ( ( $data['format'] ?? '' ) !== 'customfields' || ( $data['schema_version'] ?? null ) !== self::VERSION || ! isset( $data['groups'] ) ) { return new \WP_Error( 'schema_version', __( 'Expected a CustomFields.com export with schema_version 1. Other formats need an explicit importer.', 'customfields-com' ) ); } return self::validate_groups( $data['groups'] ); } public static function encode( $groups ) { return wp_json_encode( array( 'format' => 'customfields', 'schema_version' => self::VERSION, 'groups' => array_values( $groups ) ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } public static function validate_groups( $groups ) { if ( ! is_array( $groups ) || count( $groups ) > self::MAX_GROUPS ) { return new \WP_Error( 'schema_groups', __( 'Provide an array with at most 100 field groups.', 'customfields-com' ) ); } $clean = array(); $keys = array(); $names = array(); foreach ( $groups as $raw ) { $group = self::validate_group( $raw ); if ( is_wp_error( $group ) ) { return $group; } if ( isset( $keys[ $group['key'] ] ) ) { return new \WP_Error( 'duplicate_group', __( 'Every group must have a unique key.', 'customfields-com' ) ); } $keys[ $group['key'] ] = true; foreach ( $group['fields'] as $field ) { $scope = $group['target'] . ':' . $field['name']; if ( isset( $names[ $scope ] ) ) { return new \WP_Error( 'duplicate_field', sprintf( /* translators: %s: Field name. */ __( 'Field name "%s" appears in more than one group for the same target. Use a unique name to prevent metadata collisions.', 'customfields-com' ), $field['name'] ) ); } $names[ $scope ] = true; } $clean[] = $group; } return apply_filters( 'customfields_validate_groups', $clean, $groups ); } public static function validate_group( $raw ) { if ( ! is_array( $raw ) ) { return new \WP_Error( 'schema_group', __( 'Each group must be an object.', 'customfields-com' ) ); } $key = $raw['key'] ?? 'group_' . wp_generate_uuid4(); $title = $raw['title'] ?? ''; $target = $raw['target'] ?? 'post'; if ( ! is_string( $key ) || ! preg_match( '/^group_[a-zA-Z0-9_-]{1,80}$/D', $key ) ) { return new \WP_Error( 'group_key', __( 'Group keys must start with group_ and contain only letters, numbers, underscores or dashes.', 'customfields-com' ) ); } if ( ! is_string( $title ) || '' === trim( $title ) || strlen( $title ) > 200 ) { return new \WP_Error( 'group_title', __( 'Give every group a title of at most 200 characters.', 'customfields-com' ) ); } if ( ! is_string( $target ) || ! array_key_exists( $target, self::targets() ) ) { return new \WP_Error( 'group_target', __( 'This group target is unavailable. Activate its extension before importing or editing it.', 'customfields-com' ) ); } $post_types = $raw['post_types'] ?? array(); if ( ! is_array( $post_types ) || count( $post_types ) > 100 ) { return new \WP_Error( 'group_location', __( 'Post types must be an array.', 'customfields-com' ) ); } foreach ( $post_types as $post_type ) { if ( ! is_string( $post_type ) || ! preg_match( '/^[a-z0-9_-]{1,20}$/D', $post_type ) || in_array( $post_type, array( 'revision', 'attachment', 'nav_menu_item', 'custom_css', 'customize_changeset', 'wp_block', 'wp_template', 'wp_template_part', 'wp_navigation', 'wp_font_family', 'wp_font_face' ), true ) ) { return new \WP_Error( 'group_location', __( 'Use valid editable post-type names. Attachments and WordPress internal types are not supported.', 'customfields-com' ) ); } } if ( 'post' === $target && ! count( $post_types ) ) { return new \WP_Error( 'group_location', __( 'Choose at least one post type for this group.', 'customfields-com' ) ); } $taxonomies = self::slug_list( $raw['taxonomies'] ?? array(), 32 ); if ( is_wp_error( $taxonomies ) || ( 'term' === $target && ! $taxonomies ) ) { return new \WP_Error( 'group_taxonomies', __( 'Choose at least one valid taxonomy for a term group.', 'customfields-com' ) ); } $post_ids = $raw['post_ids'] ?? array(); if ( is_string( $post_ids ) ) { $post_ids = '' === trim( $post_ids ) ? array() : preg_split( '/[\s,]+/', trim( $post_ids ) ); } if ( ! is_array( $post_ids ) || count( $post_ids ) > 100 ) { return new \WP_Error( 'group_post_ids', __( 'Provide at most 100 post IDs.', 'customfields-com' ) ); } foreach ( $post_ids as $id ) { if ( ! is_scalar( $id ) || ! preg_match( '/^[1-9][0-9]*$/D', (string) $id ) || (float) $id > PHP_INT_MAX ) { return new \WP_Error( 'group_post_ids', __( 'Post IDs must be positive integers.', 'customfields-com' ) ); } } $templates = $raw['page_templates'] ?? array(); if ( is_string( $templates ) ) { $templates = '' === trim( $templates ) ? array() : preg_split( '/\r\n|\r|\n/', trim( $templates ) ); } if ( ! is_array( $templates ) || count( $templates ) > 100 ) { return new \WP_Error( 'group_templates', __( 'Provide at most 100 page template filenames.', 'customfields-com' ) ); } foreach ( $templates as $template ) { if ( ! is_string( $template ) || ! preg_match( '/^[a-zA-Z0-9_\/.-]{1,200}$/D', $template ) || false !== strpos( $template, '..' ) ) { return new \WP_Error( 'group_templates', __( 'Use relative page template filenames, or default.', 'customfields-com' ) ); } } $fields = $raw['fields'] ?? array(); if ( ! is_array( $fields ) || count( $fields ) > self::MAX_FIELDS ) { return new \WP_Error( 'schema_fields', __( 'A group can contain at most 200 fields.', 'customfields-com' ) ); } $clean_fields = array(); $names = array(); $keys = array(); foreach ( $fields as $field ) { $clean = self::validate_field( $field ); if ( is_wp_error( $clean ) ) { return $clean; } if ( 'user' === $target && self::reserved_user_name( $clean['name'] ) ) { return new \WP_Error( 'reserved_user_field', __( 'WordPress roles, sessions and account credentials cannot be configured as profile fields.', 'customfields-com' ) ); } if ( isset( $names[ $clean['name'] ] ) || isset( $keys[ $clean['key'] ] ) ) { return new \WP_Error( 'duplicate_field', __( 'Names and keys must be unique within a group.', 'customfields-com' ) ); } $names[ $clean['name'] ] = true; $keys[ $clean['key'] ] = true; $clean_fields[] = $clean; } $group = array( 'key' => $key, 'title' => sanitize_text_field( $title ), 'target' => $target, 'post_types' => 'post' === $target ? array_values( array_unique( $post_types ) ) : array(), 'fields' => $clean_fields ); $group['taxonomies'] = 'term' === $target ? $taxonomies : array(); $group['post_ids'] = 'post' === $target ? array_values( array_unique( array_map( 'intval', $post_ids ) ) ) : array(); $group['page_templates'] = 'post' === $target ? array_values( array_unique( $templates ) ) : array(); $group = apply_filters( 'customfields_validate_group_schema', $group, $raw ); if ( is_wp_error( $group ) ) { return $group; } $owned = apply_filters( 'customfields_supported_group_settings', array( 'complete' ), $group, $raw ); foreach ( $raw as $setting => $value ) { if ( ! array_key_exists( $setting, $group ) && ! in_array( $setting, $owned, true ) && ! in_array( $value, array( '', null, false, 0, '0', array() ), true ) ) { return new \WP_Error( 'unsupported_group_setting', sprintf( /* translators: %s: Schema setting name. */ __( 'This group uses an unavailable setting: %s. Its schema has not been changed.', 'customfields-com' ), (string) $setting ) ); } } return $group; } public static function validate_field( $raw, $depth = 0 ) { if ( ! is_array( $raw ) || $depth > 4 ) { return new \WP_Error( 'schema_field', __( 'A field must be an object within the supported nesting depth.', 'customfields-com' ) ); } $key = $raw['key'] ?? 'field_' . wp_generate_uuid4(); $name = $raw['name'] ?? ''; $label = $raw['label'] ?? ''; $type = $raw['type'] ?? 'text'; if ( ! is_string( $key ) || ! preg_match( '/^field_[a-zA-Z0-9_-]{1,80}$/D', $key ) ) { return new \WP_Error( 'field_key', __( 'Field keys must start with field_ and contain only letters, numbers, underscores or dashes.', 'customfields-com' ) ); } if ( ! is_string( $name ) || ! preg_match( '/^[a-z][a-z0-9_]{0,63}$/D', $name ) ) { return new \WP_Error( 'field_name', __( 'Field names must start with a lowercase letter and use only lowercase letters, numbers and underscores (maximum 64 characters). Protected WordPress metadata names are not allowed.', 'customfields-com' ) ); } if ( ! is_string( $label ) || '' === trim( $label ) || strlen( $label ) > 200 ) { return new \WP_Error( 'field_label', sprintf( /* translators: %s: Field name. */ __( 'Field "%s" needs a label of at most 200 characters.', 'customfields-com' ), $name ) ); } if ( ! is_string( $type ) || ! array_key_exists( $type, Fields::types() ) ) { return new \WP_Error( 'field_type', sprintf( /* translators: %s: Field name. */ __( 'Field "%s" uses an unavailable field type. Activate its extension before editing or importing this schema.', 'customfields-com' ), $name ) ); } $instructions = $raw['instructions'] ?? ''; if ( ! is_string( $instructions ) || strlen( $instructions ) > 2000 ) { return new \WP_Error( 'field_instructions', __( 'Field instructions must be text of at most 2,000 characters.', 'customfields-com' ) ); } $required = $raw['required'] ?? false; if ( ! in_array( $required, array( true, false, 0, 1, '0', '1', '' ), true ) ) { return new \WP_Error( 'field_required', __( 'Required must be a boolean.', 'customfields-com' ) ); } $choices = self::choices( $raw['choices'] ?? array() ); if ( is_wp_error( $choices ) ) { return $choices; } if ( in_array( $type, array( 'select', 'radio', 'multiselect', 'checkboxes' ), true ) && ! $choices ) { return new \WP_Error( 'field_choices', sprintf( /* translators: %s: Field name. */ __( 'Select field "%s" needs at least one choice.', 'customfields-com' ), $name ) ); } $field = array( 'key' => $key, 'name' => $name, 'label' => sanitize_text_field( $label ), 'type' => $type, 'required' => (bool) $required, 'instructions' => sanitize_textarea_field( $instructions ), 'choices' => $choices, 'default' => $raw['default'] ?? '' ); foreach ( array( 'min', 'max', 'step' ) as $setting ) { $value = $raw[$setting] ?? ( 'step' === $setting ? 'any' : '' ); if ( '' !== $value && ! ( 'step' === $setting && 'any' === $value ) && ( ! is_scalar( $value ) || ! is_numeric( $value ) || ! is_finite( (float) $value ) || ( 'step' === $setting && (float) $value <= 0 ) ) ) { return new \WP_Error( 'field_range', __( 'Numeric limits must be finite, and step must be positive or any.', 'customfields-com' ) ); } if ( 'number' === $type ) { $field[$setting] = '' === $value || 'any' === $value ? $value : (float) $value; } elseif ( isset( $raw[$setting] ) && ! in_array( $raw[$setting], array( '', 'any' ), true ) ) { return new \WP_Error( 'field_range_type', __( 'Numeric constraints apply only to number fields.', 'customfields-com' ) ); } } if ( 'number' === $type && '' !== $field['min'] && '' !== $field['max'] && $field['min'] > $field['max'] ) { return new \WP_Error( 'field_range', __( 'Minimum cannot exceed maximum.', 'customfields-com' ) ); } foreach ( array( 'minlength', 'maxlength' ) as $setting ) { $value = $raw[$setting] ?? ''; if ( '' !== $value && ( ! is_scalar( $value ) || ! preg_match( '/^[0-9]{1,6}$/D', (string) $value ) || (int) $value > 100000 ) ) { return new \WP_Error( 'field_length', __( 'Length limits must be integers from 0 to 100000.', 'customfields-com' ) ); } if ( in_array( $type, array( 'text', 'textarea', 'wysiwyg', 'email', 'url' ), true ) ) { $field[$setting] = '' === $value ? '' : (int) $value; } elseif ( ! empty( $value ) ) { return new \WP_Error( 'field_length_type', __( 'Length limits apply only to text fields.', 'customfields-com' ) ); } } if ( ! empty( $field['maxlength'] ) && ! empty( $field['minlength'] ) && $field['minlength'] > $field['maxlength'] ) { return new \WP_Error( 'field_length', __( 'Minimum length cannot exceed maximum length.', 'customfields-com' ) ); } if ( 'post' === $type ) { $field['post_types'] = self::slug_list( $raw['post_types'] ?? array( 'post' ), 20 ); if ( is_wp_error( $field['post_types'] ) || ! $field['post_types'] || array_intersect( $field['post_types'], array( 'revision', 'attachment', 'nav_menu_item', 'customize_changeset', 'custom_css', 'wp_block', 'wp_template', 'wp_template_part', 'wp_navigation' ) ) ) { return new \WP_Error( 'field_post_types', __( 'Select valid editable content post types for this reference.', 'customfields-com' ) ); } } if ( 'taxonomy' === $type ) { $taxonomy = $raw['taxonomy'] ?? 'category'; if ( ! is_string( $taxonomy ) || ! preg_match( '/^[a-z0-9_-]{1,32}$/D', $taxonomy ) ) { return new \WP_Error( 'field_taxonomy', __( 'Use a valid taxonomy name.', 'customfields-com' ) ); } $field['taxonomy'] = $taxonomy; } if ( in_array( $type, array( 'multiselect', 'checkboxes' ), true ) && is_string( $field['default'] ) && '' !== $field['default'] ) { // A visual schema form presents one selected choice value per line. $field['default'] = preg_split( '/\r\n|\r|\n/', trim( $field['default'] ) ); } $field = apply_filters( 'customfields_validate_field_schema', $field, $raw, $depth ); if ( is_wp_error( $field ) ) { return $field; } // Fail closed if an inactive extension would silently lose its schema settings. $owned_settings = apply_filters( 'customfields_supported_field_settings', array(), $field, $raw ); foreach ( $raw as $setting => $value ) { if ( ! array_key_exists( $setting, $field ) && ! ( 'step' === $setting && 'any' === $value ) && ! in_array( $setting, $owned_settings, true ) && ! in_array( $value, array( '', null, false, 0, '0', array() ), true ) ) { return new \WP_Error( 'unsupported_field_setting', sprintf( /* translators: 1: Field name; 2: schema setting name. */ __( 'Field "%1$s" uses the unavailable schema setting "%2$s". Activate its extension before editing or importing this group.', 'customfields-com' ), $name, (string) $setting ) ); } } $default_field = $field; $default_field['required'] = false; $default_field['_validating_default'] = true; $default = Fields::sanitize( $field['default'], $default_field ); if ( is_wp_error( $default ) ) { return new \WP_Error( 'field_default', sprintf( /* translators: 1: Field name; 2: validation error. */ __( 'Invalid default for "%1$s": %2$s', 'customfields-com' ), $name, $default->get_error_message() ) ); } $field['default'] = $default; return $field; } private static function slug_list( $raw, $length ) { if ( is_string( $raw ) ) { $raw = '' === trim( $raw ) ? array() : preg_split( '/[\s,]+/', trim( $raw ) ); } if ( ! is_array( $raw ) || count( $raw ) > 100 ) { return new \WP_Error( 'schema_slugs', __( 'Expected at most 100 names.', 'customfields-com' ) ); } foreach ( $raw as $name ) { if ( ! is_string( $name ) || ! preg_match( '/^[a-z0-9_-]{1,' . (int) $length . '}$/D', $name ) ) { return new \WP_Error( 'schema_slugs', __( 'Use lowercase registered names.', 'customfields-com' ) ); } } return array_values( array_unique( $raw ) ); } public static function choices( $raw ) { if ( is_string( $raw ) ) { if ( strlen( $raw ) > 40000 ) { return new \WP_Error( 'choices_size', __( 'Choices are too large.', 'customfields-com' ) ); } $choices = array(); foreach ( preg_split( '/\r\n|\r|\n/', $raw ) as $line ) { if ( '' === trim( $line ) ) { continue; } $pair = explode( ':', $line, 2 ); $key = trim( $pair[0] ); if ( array_key_exists( $key, $choices ) ) { return new \WP_Error( 'duplicate_choice', __( 'Choice values must be unique.', 'customfields-com' ) ); } $choices[ $key ] = isset( $pair[1] ) ? trim( $pair[1] ) : $key; } $raw = $choices; } if ( ! is_array( $raw ) || count( $raw ) > 200 ) { return new \WP_Error( 'choices_array', __( 'Provide at most 200 choices.', 'customfields-com' ) ); } $clean = array(); foreach ( $raw as $key => $label ) { $key = (string) $key; if ( '' === $key || strlen( $key ) > 128 || sanitize_text_field( $key ) !== $key || ! is_scalar( $label ) || strlen( (string) $label ) > 200 ) { return new \WP_Error( 'choice_value', __( 'Choice values must be plain text, with a value of at most 128 characters and label of at most 200.', 'customfields-com' ) ); } $clean[ $key ] = sanitize_text_field( (string) $label ); } return $clean; } }