get_id();
$fields = array();
$acf_names = array();
foreach ( self::acf_fields( $product_id, $acf_names ) as $field ) {
$fields[] = $field;
}
foreach ( self::tab_fields( $product_id ) as $field ) {
$fields[] = $field;
}
foreach ( self::attribute_fields( $product ) as $field ) {
$fields[] = $field;
}
foreach ( self::taxonomy_fields( $product_id ) as $field ) {
$fields[] = $field;
}
foreach ( self::meta_fields( $product_id, $acf_names ) as $field ) {
$fields[] = $field;
}
/**
* Add, change or remove the custom fields sent for a product.
*
* Each entry: namespace, key, value (string), type, source, and
* optionally label, text and writable. Private-looking keys and fields
* the merchant left out are removed after this filter runs.
*
* @param array[] $fields Collected fields.
* @param \WC_Product $product Product.
*/
$fields = (array) apply_filters( 'agencoen_custom_fields', $fields, $product );
$excluded_set = array_flip( $excluded );
$out = array();
$bytes = 0;
foreach ( $fields as $field ) {
if ( ! is_array( $field ) || empty( $field['namespace'] ) || empty( $field['key'] ) || empty( $field['type'] ) ) {
continue;
}
$source = isset( $field['source'] ) ? (string) $field['source'] : 'wp_meta';
if ( isset( $excluded_set[ self::field_id( $source, (string) $field['namespace'], (string) $field['key'] ) ] ) ) {
continue;
}
$label = isset( $field['label'] ) ? (string) $field['label'] : '';
if ( self::is_private_key( (string) $field['key'], $label ) ) {
continue;
}
$value = isset( $field['value'] ) ? self::clip( (string) $field['value'] ) : '';
$text = isset( $field['text'] ) ? self::clip( (string) $field['text'] ) : '';
if ( '' === trim( $value ) && '' === trim( $text ) ) {
continue;
}
$entry = array(
'namespace' => (string) $field['namespace'],
'key' => (string) $field['key'],
'value' => $value,
'type' => (string) $field['type'],
'source' => $source,
);
if ( '' !== $label ) {
$entry['label'] = $label;
}
if ( '' !== $text ) {
$entry['text'] = $text;
}
if ( ! empty( $field['writable'] ) ) {
$entry['writable'] = true;
}
if ( ! empty( $field['choices'] ) && is_array( $field['choices'] ) ) {
$entry['choices'] = array_slice( array_values( array_filter( array_map( 'strval', array_filter( $field['choices'], 'is_scalar' ) ), 'strlen' ) ), 0, self::MAX_CHOICES );
}
$size = strlen( (string) wp_json_encode( $entry ) );
if ( $bytes + $size > self::MAX_BYTES ) {
break;
}
$bytes += $size;
$out[] = $entry;
if ( count( $out ) >= self::MAX_FIELDS ) {
break;
}
}
return $out;
}
/**
* Cut a value to MAX_VALUE characters.
*
* @param string $value Value.
* @return string
*/
private static function clip( $value ) {
return function_exists( 'mb_substr' ) ? mb_substr( $value, 0, self::MAX_VALUE ) : substr( $value, 0, self::MAX_VALUE );
}
/* --------------------------------------------------------------------- */
/* Rewrites: write back and undo (4.24.0) */
/* --------------------------------------------------------------------- */
/** Sources a rewrite may write back to. Attributes, tabs and taxonomies are read-only. */
const WRITABLE_SOURCES = array( 'acf', 'wp_meta', 'wc_core' );
/**
* The value of a field as the sync reads it, so its hash matches the one
* the suggestion was written from.
*
* @param int $product_id Product id.
* @param string $source Source.
* @param string $key Key.
* @param \WC_Product $product Loaded product, for the short description.
* @return string|null Null when the field cannot be read.
*/
public static function current_value( $product_id, $source, $key, $product = null ) {
if ( 'wc_core' === $source ) {
if ( 'short_description' !== $key || ! $product ) {
return null;
}
return trim( (string) $product->get_short_description() );
}
if ( 'acf' === $source && function_exists( 'get_field' ) ) {
$value = get_field( $key, $product_id, false );
return is_scalar( $value ) ? (string) $value : ( null === $value ? '' : null );
}
$value = get_post_meta( $product_id, $key, true );
return is_scalar( $value ) ? trim( (string) $value ) : ( '' === $value ? '' : null );
}
/**
* A rewrite, cleaned for the field's format.
*
* @param string $format plain | multiline | html.
* @param string $value Suggested value.
* @return string
*/
public static function clean_for_format( $format, $value ) {
switch ( $format ) {
case 'plain':
return sanitize_text_field( $value );
case 'multiline':
return sanitize_textarea_field( $value );
case 'html':
return wp_kses_post( $value );
default:
return '';
}
}
/**
* A field's value as lines a merchant can read on Review: HTML block ends
* and
become line breaks, list items become bullets, other tags go,
* common entities are decoded, and each line is trimmed.
*
* Mirrors customFieldDisplayText in the platform's
* src/lib/custom-fields/rules.ts, which the web and Shopify Review screens
* use; tests/fixtures/custom-field-display.json pins both to the same text.
* The result is text, not markup: escape it when it is drawn.
*
* @param mixed $value Stored or suggested value.
* @return string
*/
public static function display_text( $value ) {
if ( ! is_string( $value ) ) {
return '';
}
$text = (string) preg_replace( '#
#i', "\n", $value );
$text = (string) preg_replace( '#(p|div|li|h[1-6])>#i', "\n", $text );
$text = (string) preg_replace( '#
]*>#i', "\u{2022} ", $text );
$text = (string) preg_replace( '#<[^>]+>#', '', $text );
$text = str_replace( array( ' ', '<', '>', '"', ''', ''' ), array( ' ', '<', '>', '"', "'", "'" ), $text );
// Last, so "<" reads "<" rather than "<".
$text = str_replace( '&', '&', $text );
$lines = array_map(
function ( $line ) {
return trim( (string) preg_replace( '/[ \t]+/', ' ', $line ) );
},
explode( "\n", $text )
);
return trim( (string) preg_replace( "/\n{3,}/", "\n\n", implode( "\n", $lines ) ) );
}
/**
* Write approved rewrites of a product's custom fields.
*
* Each field is written only if it still holds the value the suggestion was
* written from (`beforeHash`), so an edit the merchant made since is never
* overwritten. Private-looking and read-only fields are refused whatever
* the payload says.
*
* @param int $product_id Product id.
* @param array $fields Wire entries { ref, source, namespace, key, type, format, value, beforeHash,
* fill?, choice?, acfKey? }. A fill writes a field the product
* does not have yet; a choice is a label from the field's list.
* @return array { written: int, skipped: string[] } Skipped are merchant-facing reasons.
*/
public static function write_rewrites( $product_id, array $fields ) {
$product_id = (int) $product_id;
$product = wc_get_product( $product_id );
$ledger = get_post_meta( $product_id, Meta::CUSTOM_FIELD_WRITES, true );
$ledger = is_array( $ledger ) ? $ledger : array();
$written = 0;
$skipped = array();
$save = false;
foreach ( $fields as $field ) {
if ( ! is_array( $field ) ) {
continue;
}
$source = isset( $field['source'] ) ? (string) $field['source'] : '';
$key = isset( $field['key'] ) ? (string) $field['key'] : '';
$label = isset( $field['label'] ) && '' !== (string) $field['label'] ? (string) $field['label'] : $key;
if ( ! in_array( $source, self::WRITABLE_SOURCES, true ) || '' === $key || self::is_private_key( $key ) ) {
$skipped[] = $label . ': ' . __( 'Rezolve Ai is not allowed to edit this field.', 'agentic-commerce-engine' );
continue;
}
if ( 'wp_meta' === $source && ( is_protected_meta( $key, 'post' ) || self::skipped_meta_key( $key ) ) ) {
$skipped[] = $label . ': ' . __( 'Rezolve Ai is not allowed to edit this field.', 'agentic-commerce-engine' );
continue;
}
$value = self::clean_for_format( isset( $field['format'] ) ? (string) $field['format'] : '', isset( $field['value'] ) ? (string) $field['value'] : '' );
if ( '' === trim( $value ) ) {
$skipped[] = $label . ': ' . __( 'This suggestion has nothing left to publish. Generate it again.', 'agentic-commerce-engine' );
continue;
}
$current = self::current_value( $product_id, $source, $key, $product );
if ( null === $current ) {
$skipped[] = $label . ': ' . __( 'This kind of field cannot be rewritten.', 'agentic-commerce-engine' );
continue;
}
$before_hash = isset( $field['beforeHash'] ) ? (string) $field['beforeHash'] : '';
if ( '' !== $before_hash && ! hash_equals( $before_hash, hash( 'sha256', $current ) ) ) {
$skipped[] = $label . ': ' . __( 'This field changed in WordPress after the suggestion was written. Generate it again.', 'agentic-commerce-engine' );
continue;
}
// A fill writes a field the product did not have (4.28.0), and a
// choice is a value picked from the field's own list.
$fill = ! empty( $field['fill'] );
$choice = ! empty( $field['choice'] );
if ( $fill && '' !== trim( (string) $current ) ) {
$skipped[] = $label . ': ' . __( 'This field has a value now. Generate it again.', 'agentic-commerce-engine' );
continue;
}
if ( $choice && 'acf' !== $source ) {
$skipped[] = $label . ': ' . __( 'Rezolve Ai is not allowed to edit this field.', 'agentic-commerce-engine' );
continue;
}
// ACF's own field key when the caller knows it (a content destination
// chosen from the field group, 4.25.0), or looked up for a fill: by
// name alone ACF cannot save a value on a product that has none yet.
$acf_key = isset( $field['acfKey'] ) && is_string( $field['acfKey'] ) && preg_match( '/^field_[A-Za-z0-9]{1,64}$/', $field['acfKey'] ) ? $field['acfKey'] : '';
$stored = $value;
if ( 'acf' === $source && ( $fill || $choice ) ) {
$object = function_exists( 'acf_get_field' ) ? acf_get_field( '' !== $acf_key ? $acf_key : $key ) : false;
if ( ! is_array( $object ) || empty( $object['key'] ) ) {
$skipped[] = $label . ': ' . __( 'This field is not in your ACF field groups any more.', 'agentic-commerce-engine' );
continue;
}
$acf_key = (string) $object['key'];
$type = isset( $object['type'] ) ? (string) $object['type'] : '';
if ( $choice ) {
$picked = self::single_choice_labels( $object ) ? self::choice_value( $object, $value ) : null;
if ( null === $picked ) {
$skipped[] = $label . ': ' . __( 'This value is not one of the field\'s choices any more. Generate it again.', 'agentic-commerce-engine' );
continue;
}
$stored = $picked;
} elseif ( ! in_array( $type, self::WRITABLE_ACF_TYPES, true ) ) {
$skipped[] = $label . ': ' . __( 'This kind of field cannot be rewritten.', 'agentic-commerce-engine' );
continue;
}
}
if ( 'wc_core' === $source ) {
$product->set_short_description( $stored );
$save = true;
} elseif ( 'acf' === $source && function_exists( 'update_field' ) ) {
update_field( '' !== $acf_key ? $acf_key : $key, $stored, $product_id );
} else {
update_post_meta( $product_id, $key, wp_slash( $stored ) );
}
$id = self::field_id( $source, isset( $field['namespace'] ) ? (string) $field['namespace'] : '', $key );
$ledger[ $id ] = array(
'source' => $source,
'key' => $key,
// The EARLIEST previous value wins: it is the merchant's original.
'previous' => isset( $ledger[ $id ]['previous'] ) ? $ledger[ $id ]['previous'] : $current,
'written' => $stored,
);
++$written;
}
if ( $save && $product ) {
$product->save();
}
if ( $written > 0 ) {
update_post_meta( $product_id, Meta::CUSTOM_FIELD_WRITES, $ledger );
}
return array(
'written' => $written,
'skipped' => $skipped,
);
}
/**
* Whether a product has custom-field writes to undo.
*
* @param int $product_id Product id.
* @return bool
*/
public static function has_writes( $product_id ) {
$ledger = get_post_meta( (int) $product_id, Meta::CUSTOM_FIELD_WRITES, true );
return is_array( $ledger ) && ! empty( $ledger );
}
/**
* Undo custom-field rewrites: restore each field that still holds exactly
* what was written, leave any field the merchant changed since, then clear
* the ledger.
*
* @param int $product_id Product id.
* @param \WC_Product $product Loaded product; saved by the caller when given.
* @return int Fields restored.
*/
public static function undo( $product_id, $product = null ) {
$product_id = (int) $product_id;
$ledger = get_post_meta( $product_id, Meta::CUSTOM_FIELD_WRITES, true );
if ( ! is_array( $ledger ) || empty( $ledger ) ) {
return 0;
}
$own_product = null === $product;
if ( $own_product ) {
$product = wc_get_product( $product_id );
}
$restored = 0;
$save = false;
foreach ( $ledger as $entry ) {
if ( ! is_array( $entry ) || empty( $entry['key'] ) || empty( $entry['source'] ) ) {
continue;
}
$current = self::current_value( $product_id, (string) $entry['source'], (string) $entry['key'], $product );
if ( null === $current || (string) $current !== trim( (string) $entry['written'] ) ) {
continue;
}
$previous = isset( $entry['previous'] ) ? (string) $entry['previous'] : '';
if ( 'wc_core' === $entry['source'] ) {
$product->set_short_description( $previous );
$save = true;
} elseif ( 'acf' === $entry['source'] && '' === $previous && function_exists( 'delete_field' ) ) {
// A field Rezolve Ai filled goes back to having no value at all.
delete_field( (string) $entry['key'], $product_id );
} elseif ( 'acf' === $entry['source'] && function_exists( 'update_field' ) ) {
update_field( (string) $entry['key'], $previous, $product_id );
} elseif ( '' === $previous ) {
delete_post_meta( $product_id, (string) $entry['key'] );
} else {
update_post_meta( $product_id, (string) $entry['key'], wp_slash( $previous ) );
}
++$restored;
}
if ( $save && $own_product && $product ) {
$product->save();
}
delete_post_meta( $product_id, Meta::CUSTOM_FIELD_WRITES );
return $restored;
}
/* --------------------------------------------------------------------- */
/* ACF */
/* --------------------------------------------------------------------- */
/**
* ACF fields, when ACF is active. Values are read raw and resolved here, so
* a select sends its choice label and a relationship its post titles.
*
* @param int $product_id Product id.
* @param string[] $acf_names Filled with the meta keys ACF owns.
* @return array[]
*/
private static function acf_fields( $product_id, array &$acf_names ) {
if ( ! function_exists( 'get_field_objects' ) ) {
return array();
}
$objects = get_field_objects( $product_id, false );
if ( ! is_array( $objects ) ) {
return array();
}
$out = array();
foreach ( $objects as $object ) {
if ( ! is_array( $object ) || empty( $object['name'] ) ) {
continue;
}
$acf_names[] = (string) $object['name'];
$type = isset( $object['type'] ) ? (string) $object['type'] : 'text';
if ( in_array( $type, self::SKIP_ACF_TYPES, true ) ) {
continue;
}
$text = self::acf_text( $object );
if ( '' === $text ) {
continue;
}
$writable = in_array( $type, self::WRITABLE_ACF_TYPES, true ) && is_string( $object['value'] );
$entry = array(
'namespace' => 'acf',
'key' => (string) $object['name'],
'value' => $writable ? (string) $object['value'] : $text,
'type' => 'acf:' . $type,
'source' => 'acf',
'label' => isset( $object['label'] ) ? wp_strip_all_tags( (string) $object['label'] ) : '',
'writable' => $writable,
);
// A single-choice field sends its choices (the labels it shows), so
// a product without a value can be filled with one of them. Its value
// stays the label it always sent.
$choices = self::single_choice_labels( $object );
if ( $choices ) {
$entry['choices'] = $choices;
$entry['writable'] = true;
}
$out[] = $entry;
}
return $out;
}
/**
* The choice labels of a single-choice ACF field, or none.
*
* @param array $object ACF field object (or field settings).
* @return string[]
*/
public static function single_choice_labels( array $object ) {
$type = isset( $object['type'] ) ? (string) $object['type'] : '';
if ( ! in_array( $type, self::SINGLE_CHOICE_ACF_TYPES, true ) || ! empty( $object['multiple'] ) ) {
return array();
}
$choices = isset( $object['choices'] ) && is_array( $object['choices'] ) ? $object['choices'] : array();
$labels = array();
foreach ( $choices as $value => $label ) {
// A choice with no label shows its value, as ACF does.
$text = trim( wp_strip_all_tags( is_scalar( $label ) ? (string) $label : '' ) );
if ( '' === $text ) {
$text = trim( (string) $value );
}
if ( '' !== $text ) {
$labels[] = function_exists( 'mb_substr' ) ? mb_substr( $text, 0, 256 ) : substr( $text, 0, 256 );
}
}
return array_slice( array_values( array_unique( $labels ) ), 0, self::MAX_CHOICES );
}
/**
* The stored value of a choice, from the label (or value) the platform
* picked. Null when it is not one of the field's choices.
*
* @param array $object ACF field settings.
* @param string $picked Picked label.
* @return string|null
*/
public static function choice_value( array $object, $picked ) {
$choices = isset( $object['choices'] ) && is_array( $object['choices'] ) ? $object['choices'] : array();
$picked = trim( (string) $picked );
foreach ( $choices as $value => $label ) {
if ( is_scalar( $label ) && trim( wp_strip_all_tags( (string) $label ) ) === $picked ) {
return (string) $value;
}
}
return array_key_exists( $picked, $choices ) ? $picked : null;
}
/**
* Readable text of one ACF field object (raw value).
*
* @param array $object ACF field object.
* @param int $depth Nesting depth; groups and repeaters flatten one level.
* @return string
*/
private static function acf_text( array $object, $depth = 0 ) {
$type = isset( $object['type'] ) ? (string) $object['type'] : 'text';
$value = isset( $object['value'] ) ? $object['value'] : null;
if ( null === $value || '' === $value || array() === $value ) {
return '';
}
switch ( $type ) {
case 'select':
case 'checkbox':
case 'radio':
case 'button_group':
$choices = isset( $object['choices'] ) && is_array( $object['choices'] ) ? $object['choices'] : array();
$labels = array();
foreach ( (array) $value as $choice ) {
if ( is_scalar( $choice ) ) {
$labels[] = isset( $choices[ $choice ] ) ? (string) $choices[ $choice ] : (string) $choice;
}
}
return implode( ', ', array_filter( $labels, 'strlen' ) );
case 'true_false':
return $value ? __( 'Yes', 'agentic-commerce-engine' ) : __( 'No', 'agentic-commerce-engine' );
case 'date_picker':
$date = \DateTime::createFromFormat( 'Ymd', (string) $value );
return $date ? $date->format( 'Y-m-d' ) : (string) $value;
case 'relationship':
case 'post_object':
case 'page_link':
$titles = array();
foreach ( (array) $value as $post_id ) {
if ( is_numeric( $post_id ) ) {
$title = get_the_title( (int) $post_id );
if ( '' !== $title ) {
$titles[] = $title;
}
}
}
return implode( ', ', $titles );
case 'taxonomy':
$names = array();
foreach ( (array) $value as $term_id ) {
$term = is_numeric( $term_id ) ? get_term( (int) $term_id ) : null;
if ( $term && ! is_wp_error( $term ) ) {
$names[] = $term->name;
}
}
return implode( ', ', $names );
case 'group':
case 'repeater':
case 'flexible_content':
if ( $depth > 0 || ! is_array( $value ) ) {
return '';
}
$sub_fields = isset( $object['sub_fields'] ) && is_array( $object['sub_fields'] ) ? $object['sub_fields'] : array();
$by_key = array();
foreach ( $sub_fields as $sub ) {
if ( isset( $sub['key'] ) ) {
$by_key[ $sub['key'] ] = $sub;
}
}
$rows = 'group' === $type ? array( $value ) : $value;
$out = array();
foreach ( $rows as $row ) {
if ( ! is_array( $row ) ) {
continue;
}
$parts = array();
foreach ( $row as $sub_key => $sub_value ) {
$sub = isset( $by_key[ $sub_key ] ) ? $by_key[ $sub_key ] : null;
if ( ! $sub || in_array( (string) $sub['type'], self::SKIP_ACF_TYPES, true ) ) {
continue;
}
$sub['value'] = $sub_value;
$text = self::acf_text( $sub, $depth + 1 );
if ( '' !== $text && ! self::is_private_key( (string) $sub['name'], (string) $sub['label'] ) ) {
$parts[] = wp_strip_all_tags( (string) $sub['label'] ) . ': ' . $text;
}
}
if ( $parts ) {
$out[] = implode( '; ', $parts );
}
}
return implode( "\n", $out );
default:
if ( is_array( $value ) || is_object( $value ) ) {
return '';
}
return trim( (string) $value );
}
}
/* --------------------------------------------------------------------- */
/* Tabs, attributes, taxonomies */
/* --------------------------------------------------------------------- */
/**
* Custom product tabs from "Custom Product Tabs for WooCommerce" (YIKES).
*
* @param int $product_id Product id.
* @return array[]
*/
private static function tab_fields( $product_id ) {
$tabs = get_post_meta( $product_id, 'yikes_woo_products_tabs', true );
if ( ! is_array( $tabs ) ) {
return array();
}
$out = array();
foreach ( $tabs as $tab ) {
if ( ! is_array( $tab ) || empty( $tab['content'] ) ) {
continue;
}
$title = isset( $tab['title'] ) ? (string) $tab['title'] : '';
$key = ! empty( $tab['id'] ) ? (string) $tab['id'] : sanitize_title( $title );
if ( '' === $key ) {
continue;
}
$out[] = array(
'namespace' => 'tab',
'key' => $key,
'value' => (string) $tab['content'],
'type' => 'wc:tab_html',
'source' => 'wc_tab',
'label' => $title,
);
}
return $out;
}
/**
* Product-level attributes that are not variation axes (those already ride
* on the variations), global and custom alike: "Material: Merino wool".
*
* @param \WC_Product $product Product.
* @return array[]
*/
private static function attribute_fields( $product ) {
$out = array();
$is_variable = $product->is_type( 'variable' );
foreach ( (array) $product->get_attributes() as $attribute ) {
if ( ! $attribute instanceof \WC_Product_Attribute ) {
continue;
}
if ( $is_variable && $attribute->get_variation() ) {
continue;
}
$name = $attribute->get_name();
$values = $attribute->is_taxonomy()
? wc_get_product_terms( $product->get_id(), $name, array( 'fields' => 'names' ) )
: $attribute->get_options();
$values = array_filter( array_map( 'strval', (array) $values ), 'strlen' );
if ( ! $values ) {
continue;
}
$out[] = array(
'namespace' => 'attribute',
'key' => sanitize_title( $name ),
'value' => implode( ', ', $values ),
'type' => 'wc:attribute',
'source' => 'wc_attribute',
'label' => wc_attribute_label( $name, $product ),
);
}
return $out;
}
/**
* Custom product taxonomies (WooCommerce's own Brands among them).
*
* @param int $product_id Product id.
* @return array[]
*/
private static function taxonomy_fields( $product_id ) {
$out = array();
foreach ( get_object_taxonomies( 'product', 'objects' ) as $taxonomy ) {
$name = $taxonomy->name;
if ( in_array( $name, self::SKIP_TAXONOMIES, true ) || 0 === strpos( $name, 'pa_' ) ) {
continue;
}
if ( empty( $taxonomy->public ) && empty( $taxonomy->show_ui ) ) {
continue;
}
$terms = wp_get_post_terms( $product_id, $name, array( 'fields' => 'names' ) );
if ( is_wp_error( $terms ) || empty( $terms ) ) {
continue;
}
$label = 'product_brand' === $name
? __( 'Brand', 'agentic-commerce-engine' )
: ( ! empty( $taxonomy->labels->singular_name ) ? $taxonomy->labels->singular_name : $taxonomy->label );
$out[] = array(
'namespace' => 'taxonomy',
'key' => $name,
'value' => implode( ', ', array_map( 'strval', $terms ) ),
'type' => 'wc:taxonomy',
'source' => 'wc_taxonomy',
'label' => (string) $label,
);
}
return $out;
}
/* --------------------------------------------------------------------- */
/* Post meta */
/* --------------------------------------------------------------------- */
/**
* Public custom fields (post meta) that are not protected, not owned by
* ACF and not known plugin configuration.
*
* @param int $product_id Product id.
* @param string[] $acf_names Meta keys ACF owns.
* @return array[]
*/
private static function meta_fields( $product_id, array $acf_names ) {
$all = get_post_meta( $product_id );
if ( ! is_array( $all ) ) {
return array();
}
$acf = array_flip( $acf_names );
$out = array();
foreach ( $all as $key => $values ) {
$key = (string) $key;
if ( isset( $acf[ $key ] ) || is_protected_meta( $key, 'post' ) || self::skipped_meta_key( $key ) ) {
continue;
}
$raw = is_array( $values ) && isset( $values[0] ) ? maybe_unserialize( $values[0] ) : null;
$value = '';
$plain = false;
if ( is_scalar( $raw ) ) {
$value = trim( (string) $raw );
$plain = true;
} elseif ( is_array( $raw ) ) {
$items = array();
foreach ( $raw as $item ) {
if ( ! is_scalar( $item ) ) {
$items = array();
break;
}
$items[] = trim( (string) $item );
}
$value = implode( ', ', array_filter( $items, 'strlen' ) );
}
if ( '' === $value ) {
continue;
}
$out[] = array(
'namespace' => 'meta',
'key' => $key,
'value' => $value,
'type' => ( $plain && $value !== wp_strip_all_tags( $value ) ) ? 'wp:html' : 'wp:string',
'source' => 'wp_meta',
'writable' => $plain,
);
}
return $out;
}
/**
* Whether a meta key belongs to a known plugin's configuration.
*
* @param string $key Meta key.
* @return bool
*/
public static function skipped_meta_key( $key ) {
$lower = strtolower( $key );
foreach ( self::SKIP_META_PREFIXES as $prefix ) {
if ( 0 === strpos( $lower, $prefix ) ) {
return true;
}
}
return false;
}
}