mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-12 02:03:55 +00:00
feat(abilities): enhance schema compat, user meta, e-commerce meta, and creator pipeline
- fix(schema-compat): prefer 'string' in non-null scalar unions to fix SanitizeSchemaUnionItemsTypeTest - fix(content): define featured_image output properties and return resilient empty object structure for posts without thumbnails - feat(content): add get_allowed_protected_meta() with WooCommerce and paywall whitelist - feat(user): add sanitized meta support to create-user, update-user, and get-user with privilege escalation guard - feat(database): support 'query' argument alias in emcp-tools/query - feat(woo): expand create/update-product with author/vendor ownership, virtual/downloadable, prices, categories, tags, and meta; add list-vendors - test: add 14 new automated unit tests across User, Content, Woo, and Database suites
This commit is contained in:
@@ -274,6 +274,52 @@ class EMCP_Tools_Content_Abilities {
|
||||
return ! in_array( $post_type, $this->internal_post_types(), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe protected meta keys permitted for e-commerce, paywalls, and standard publishing.
|
||||
*
|
||||
* @since 3.4.3
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_allowed_protected_meta(): array {
|
||||
$default = array(
|
||||
// WooCommerce Product Pricing & Inventory
|
||||
'_price',
|
||||
'_regular_price',
|
||||
'_sale_price',
|
||||
'_sale_price_dates_from',
|
||||
'_sale_price_dates_to',
|
||||
'_virtual',
|
||||
'_downloadable',
|
||||
'_sku',
|
||||
'_stock_status',
|
||||
'_stock',
|
||||
'_manage_stock',
|
||||
'_backorders',
|
||||
'_sold_individually',
|
||||
'_tax_status',
|
||||
'_tax_class',
|
||||
'_purchase_note',
|
||||
'_featured',
|
||||
'_weight',
|
||||
'_length',
|
||||
'_width',
|
||||
'_height',
|
||||
'_product_url',
|
||||
'_button_text',
|
||||
'_thumbnail_id',
|
||||
'_product_image_gallery',
|
||||
// Paywalls, Subscriptions, and Access
|
||||
'_pay_per_post_price',
|
||||
'_ppp_price',
|
||||
'_ppp_enable',
|
||||
'_wcs_subscription_period',
|
||||
'_wcs_subscription_period_interval',
|
||||
// Display & Template
|
||||
'_wp_page_template',
|
||||
);
|
||||
return (array) apply_filters( 'emcp_tools_content_allowed_protected_meta', $default );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a meta map against the protected-meta guard.
|
||||
*
|
||||
@@ -281,7 +327,7 @@ class EMCP_Tools_Content_Abilities {
|
||||
* @return true|\WP_Error
|
||||
*/
|
||||
private function reject_protected_meta( array $meta ) {
|
||||
$allowed = (array) apply_filters( 'emcp_tools_content_allowed_protected_meta', array() );
|
||||
$allowed = self::get_allowed_protected_meta();
|
||||
foreach ( array_keys( $meta ) as $key ) {
|
||||
$key = (string) $key;
|
||||
if ( in_array( $key, $allowed, true ) ) {
|
||||
@@ -541,7 +587,14 @@ class EMCP_Tools_Content_Abilities {
|
||||
'permalink' => array( 'type' => 'string' ),
|
||||
'edit_link' => array( 'type' => 'string' ), 'author' => array( 'type' => 'object' ),
|
||||
'terms' => array( 'type' => 'object' ), 'meta' => array( 'type' => 'object' ),
|
||||
'featured_image' => array( 'type' => array( 'object', 'null' ) ),
|
||||
'featured_image' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array( 'type' => 'integer' ),
|
||||
'url' => array( 'type' => 'string' ),
|
||||
'alt' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'is_elementor' => array( 'type' => 'boolean' ),
|
||||
) ),
|
||||
'meta' => array(
|
||||
@@ -591,7 +644,7 @@ class EMCP_Tools_Content_Abilities {
|
||||
$meta_raw = get_post_meta( $post_id );
|
||||
$meta = array();
|
||||
if ( is_array( $meta_raw ) ) {
|
||||
$allowed = (array) apply_filters( 'emcp_tools_content_allowed_protected_meta', array() );
|
||||
$allowed = self::get_allowed_protected_meta();
|
||||
foreach ( $meta_raw as $key => $vals ) {
|
||||
$key = (string) $key;
|
||||
if ( ! in_array( $key, $allowed, true ) && ( '_' === substr( $key, 0, 1 ) || is_protected_meta( $key, 'post' ) ) ) {
|
||||
@@ -606,7 +659,11 @@ class EMCP_Tools_Content_Abilities {
|
||||
'id' => $thumb_id,
|
||||
'url' => (string) wp_get_attachment_image_url( $thumb_id, 'full' ),
|
||||
'alt' => (string) get_post_meta( $thumb_id, '_wp_attachment_image_alt', true ),
|
||||
) : null;
|
||||
) : array(
|
||||
'id' => 0,
|
||||
'url' => '',
|
||||
'alt' => '',
|
||||
);
|
||||
|
||||
$author_id = (int) ( $post->post_author ?? 0 );
|
||||
$author_obj = $author_id ? get_userdata( $author_id ) : null;
|
||||
|
||||
@@ -92,10 +92,25 @@ class EMCP_Tools_Database_Abilities {
|
||||
}
|
||||
|
||||
private function register_query(): void {
|
||||
$this->ability( 'emcp-tools/query', __( 'Query (read-only)', 'emcp-tools' ), __( 'Run a read-only SQL query (SELECT/SHOW/DESCRIBE/EXPLAIN). Writes/DDL and file-access SQL are rejected. Results are capped.', 'emcp-tools' ), 'execute_query', array( 'sql' => array( 'type' => 'string' ), 'limit' => array( 'type' => 'integer' ) ), array( 'sql' ), true );
|
||||
$this->ability(
|
||||
'emcp-tools/query',
|
||||
__( 'Query (read-only)', 'emcp-tools' ),
|
||||
__( 'Run a read-only SQL query (SELECT/SHOW/DESCRIBE/EXPLAIN). Writes/DDL and file-access SQL are rejected. Results are capped.', 'emcp-tools' ),
|
||||
'execute_query',
|
||||
array(
|
||||
'sql' => array( 'type' => 'string', 'description' => __( 'The SQL query to run.', 'emcp-tools' ) ),
|
||||
'query' => array( 'type' => 'string', 'description' => __( 'Alias for sql.', 'emcp-tools' ) ),
|
||||
'limit' => array( 'type' => 'integer', 'description' => __( 'Maximum rows to return.', 'emcp-tools' ) ),
|
||||
),
|
||||
array(),
|
||||
true
|
||||
);
|
||||
}
|
||||
public function execute_query( $input ) {
|
||||
$sql = (string) ( $input['sql'] ?? '' );
|
||||
$sql = trim( (string) ( $input['sql'] ?? $input['query'] ?? '' ) );
|
||||
if ( '' === $sql ) {
|
||||
return new \WP_Error( 'missing_sql', __( 'A "sql" (or "query") parameter is required.', 'emcp-tools' ) );
|
||||
}
|
||||
$ro = EMCP_Tools_Database_Guard::is_read_only_sql( $sql );
|
||||
if ( is_wp_error( $ro ) ) {
|
||||
return $ro;
|
||||
|
||||
@@ -97,6 +97,37 @@ class EMCP_Tools_User_Abilities {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user meta key is forbidden (privilege escalation, auth secrets, roles).
|
||||
*
|
||||
* @since 3.4.3
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_forbidden_user_meta_key( string $key ): bool {
|
||||
$lower = strtolower( trim( $key ) );
|
||||
if ( '' === $lower ) {
|
||||
return true;
|
||||
}
|
||||
$forbidden_exact = array(
|
||||
'capabilities',
|
||||
'user_level',
|
||||
'session_tokens',
|
||||
'wp_user_level',
|
||||
'default_password_nag',
|
||||
'account_status',
|
||||
);
|
||||
if ( in_array( $lower, $forbidden_exact, true ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( preg_match( '/(^|_)capabilities$/i', $lower ) ||
|
||||
preg_match( '/(^|_)user_level$/i', $lower ) ||
|
||||
preg_match( '/(pass|token|secret|auth|nonce|activation|digest)/i', $lower ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// list-users
|
||||
// -------------------------------------------------------------------
|
||||
@@ -203,7 +234,10 @@ class EMCP_Tools_User_Abilities {
|
||||
'permission_callback' => array( $this, 'can_list' ),
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array( 'id' => array( 'type' => 'integer', 'description' => __( 'User ID.', 'emcp-tools' ) ) ),
|
||||
'properties' => array(
|
||||
'id' => array( 'type' => 'integer', 'description' => __( 'User ID.', 'emcp-tools' ) ),
|
||||
'include_meta' => array( 'type' => 'boolean', 'description' => __( 'Whether to include safe user metadata. Default: false.', 'emcp-tools' ) ),
|
||||
),
|
||||
'required' => array( 'id' ),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object', 'properties' => array(
|
||||
@@ -214,6 +248,7 @@ class EMCP_Tools_User_Abilities {
|
||||
'description' => array( 'type' => 'string' ), 'roles' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
|
||||
'registered' => array( 'type' => 'string' ), 'post_count' => array( 'type' => 'integer' ),
|
||||
'is_admin' => array( 'type' => 'boolean' ),
|
||||
'meta' => array( 'type' => 'object' ),
|
||||
) ),
|
||||
'meta' => array( 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ), 'show_in_rest' => true ),
|
||||
)
|
||||
@@ -233,6 +268,21 @@ class EMCP_Tools_User_Abilities {
|
||||
if ( ! $u ) {
|
||||
return new \WP_Error( 'user_not_found', __( 'User not found.', 'emcp-tools' ) );
|
||||
}
|
||||
|
||||
$meta = array();
|
||||
if ( ! empty( $input['include_meta'] ) && function_exists( 'get_user_meta' ) ) {
|
||||
$raw_meta = get_user_meta( $id );
|
||||
if ( is_array( $raw_meta ) ) {
|
||||
foreach ( $raw_meta as $mk => $mvals ) {
|
||||
$mk = (string) $mk;
|
||||
if ( self::is_forbidden_user_meta_key( $mk ) ) {
|
||||
continue;
|
||||
}
|
||||
$meta[ $mk ] = is_array( $mvals ) && 1 === count( $mvals ) ? maybe_unserialize( $mvals[0] ) : array_map( 'maybe_unserialize', (array) $mvals );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => (int) $u->ID,
|
||||
'username' => (string) ( $u->user_login ?? '' ),
|
||||
@@ -247,6 +297,7 @@ class EMCP_Tools_User_Abilities {
|
||||
'registered' => (string) ( $u->user_registered ?? '' ),
|
||||
'post_count' => function_exists( 'count_user_posts' ) ? (int) count_user_posts( $id ) : 0,
|
||||
'is_admin' => $this->user_has_admin_caps( $id ),
|
||||
'meta' => ! empty( $meta ) ? $meta : (object) array(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,6 +326,7 @@ class EMCP_Tools_User_Abilities {
|
||||
'display_name' => array( 'type' => 'string' ),
|
||||
'url' => array( 'type' => 'string' ),
|
||||
'description' => array( 'type' => 'string' ),
|
||||
'meta' => array( 'type' => 'object', 'description' => __( 'Optional key-value pairs of user meta to save.', 'emcp-tools' ) ),
|
||||
),
|
||||
'required' => array( 'username', 'email' ),
|
||||
),
|
||||
@@ -340,6 +392,16 @@ class EMCP_Tools_User_Abilities {
|
||||
}
|
||||
$user_id = (int) $user_id;
|
||||
|
||||
if ( isset( $input['meta'] ) && is_array( $input['meta'] ) && function_exists( 'update_user_meta' ) ) {
|
||||
foreach ( $input['meta'] as $m_key => $m_val ) {
|
||||
$m_key = sanitize_key( $m_key );
|
||||
if ( self::is_forbidden_user_meta_key( $m_key ) ) {
|
||||
continue;
|
||||
}
|
||||
update_user_meta( $user_id, $m_key, $m_val );
|
||||
}
|
||||
}
|
||||
|
||||
// Email the new user a set-password link. The password is NEVER returned.
|
||||
if ( function_exists( 'wp_send_new_user_notifications' ) ) {
|
||||
wp_send_new_user_notifications( $user_id, 'user' );
|
||||
@@ -387,6 +449,7 @@ class EMCP_Tools_User_Abilities {
|
||||
'nickname' => array( 'type' => 'string' ),
|
||||
'url' => array( 'type' => 'string' ),
|
||||
'description' => array( 'type' => 'string' ),
|
||||
'meta' => array( 'type' => 'object', 'description' => __( 'Key-value pairs of user meta to save or update.', 'emcp-tools' ) ),
|
||||
),
|
||||
'required' => array( 'id' ),
|
||||
),
|
||||
@@ -443,6 +506,22 @@ class EMCP_Tools_User_Abilities {
|
||||
$updated[] = 'email';
|
||||
}
|
||||
|
||||
// Apply safe user metadata updates.
|
||||
if ( isset( $input['meta'] ) && is_array( $input['meta'] ) && function_exists( 'update_user_meta' ) ) {
|
||||
$meta_updated = false;
|
||||
foreach ( $input['meta'] as $m_key => $m_val ) {
|
||||
$m_key = sanitize_key( $m_key );
|
||||
if ( self::is_forbidden_user_meta_key( $m_key ) ) {
|
||||
continue;
|
||||
}
|
||||
update_user_meta( $id, $m_key, $m_val );
|
||||
$meta_updated = true;
|
||||
}
|
||||
if ( $meta_updated ) {
|
||||
$updated[] = 'meta';
|
||||
}
|
||||
}
|
||||
|
||||
// Capture prior values of exactly the fields being changed, for rollback.
|
||||
$emcp_before = array();
|
||||
if ( class_exists( 'EMCP_Tools_Change_Recorder' ) ) {
|
||||
|
||||
@@ -98,6 +98,7 @@ class EMCP_Tools_Woo_Integration {
|
||||
'get-order' => 'Get order by { order_id }',
|
||||
'list-customers' => 'List registered customers { limit? }',
|
||||
'list-coupons' => 'List coupons { limit? }',
|
||||
'list-vendors' => 'List marketplace vendors / models { role?, limit? }',
|
||||
'report-sales' => 'Report sales { period?: "week"|"month"|"year" }',
|
||||
'system-status' => 'WooCommerce system health and environment status',
|
||||
),
|
||||
@@ -114,11 +115,16 @@ class EMCP_Tools_Woo_Integration {
|
||||
);
|
||||
$items = array();
|
||||
foreach ( $posts as $p ) {
|
||||
$items[] = array(
|
||||
'id' => $p->ID,
|
||||
'name' => $p->post_title,
|
||||
'sku' => get_post_meta( $p->ID, '_sku', true ),
|
||||
'price' => get_post_meta( $p->ID, '_price', true ),
|
||||
$thumb_id = (int) get_post_thumbnail_id( $p->ID );
|
||||
$items[] = array(
|
||||
'id' => (int) $p->ID,
|
||||
'name' => (string) $p->post_title,
|
||||
'sku' => (string) get_post_meta( $p->ID, '_sku', true ),
|
||||
'price' => (string) get_post_meta( $p->ID, '_price', true ),
|
||||
'regular_price' => (string) get_post_meta( $p->ID, '_regular_price', true ),
|
||||
'author' => (int) ( $p->post_author ?? 0 ),
|
||||
'virtual' => 'yes' === get_post_meta( $p->ID, '_virtual', true ),
|
||||
'featured_image' => $thumb_id,
|
||||
);
|
||||
}
|
||||
return array( 'products' => $items, 'total' => count( $items ) );
|
||||
@@ -129,15 +135,40 @@ class EMCP_Tools_Woo_Integration {
|
||||
if ( ! $p || 'product' !== $p->post_type ) {
|
||||
return new WP_Error( 'not_found', __( 'Product not found.', 'emcp-tools' ) );
|
||||
}
|
||||
$thumb_id = (int) get_post_thumbnail_id( $p->ID );
|
||||
return array(
|
||||
'id' => $p->ID,
|
||||
'name' => $p->post_title,
|
||||
'description' => $p->post_content,
|
||||
'sku' => get_post_meta( $p->ID, '_sku', true ),
|
||||
'price' => get_post_meta( $p->ID, '_price', true ),
|
||||
'stock' => get_post_meta( $p->ID, '_stock', true ),
|
||||
'id' => (int) $p->ID,
|
||||
'name' => (string) $p->post_title,
|
||||
'description' => (string) $p->post_content,
|
||||
'author' => (int) ( $p->post_author ?? 0 ),
|
||||
'status' => (string) $p->post_status,
|
||||
'sku' => (string) get_post_meta( $p->ID, '_sku', true ),
|
||||
'price' => (string) get_post_meta( $p->ID, '_price', true ),
|
||||
'regular_price' => (string) get_post_meta( $p->ID, '_regular_price', true ),
|
||||
'sale_price' => (string) get_post_meta( $p->ID, '_sale_price', true ),
|
||||
'stock' => get_post_meta( $p->ID, '_stock', true ),
|
||||
'virtual' => 'yes' === get_post_meta( $p->ID, '_virtual', true ),
|
||||
'downloadable' => 'yes' === get_post_meta( $p->ID, '_downloadable', true ),
|
||||
'thumbnail_id' => $thumb_id,
|
||||
'permalink' => (string) get_permalink( $id ),
|
||||
);
|
||||
|
||||
case 'list-vendors':
|
||||
$role = sanitize_key( $in['role'] ?? 'wcfm_vendor' );
|
||||
$users = get_users( array( 'role' => $role, 'number' => (int) ( $in['limit'] ?? 50 ) ) );
|
||||
$vendors = array();
|
||||
foreach ( $users as $u ) {
|
||||
$store_settings = get_user_meta( $u->ID, 'wcfmmp_profile_settings', true );
|
||||
$store_name = is_array( $store_settings ) && ! empty( $store_settings['store_name'] ) ? $store_settings['store_name'] : $u->display_name;
|
||||
$vendors[] = array(
|
||||
'id' => (int) $u->ID,
|
||||
'username' => (string) $u->user_login,
|
||||
'store_name' => (string) $store_name,
|
||||
'email' => (string) $u->user_email,
|
||||
);
|
||||
}
|
||||
return array( 'vendors' => $vendors, 'total' => count( $vendors ) );
|
||||
|
||||
case 'list-orders':
|
||||
$posts = get_posts(
|
||||
array(
|
||||
@@ -199,41 +230,137 @@ class EMCP_Tools_Woo_Integration {
|
||||
|
||||
switch ( $op ) {
|
||||
case 'create-product':
|
||||
$title = sanitize_text_field( $in['name'] ?? 'New Product' );
|
||||
$id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $title,
|
||||
'post_content'=> (string) ( $in['description'] ?? '' ),
|
||||
)
|
||||
$title = sanitize_text_field( $in['name'] ?? $in['title'] ?? 'New Product' );
|
||||
$post_data = array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => sanitize_key( $in['status'] ?? 'publish' ),
|
||||
'post_title' => $title,
|
||||
'post_content' => (string) ( $in['description'] ?? $in['content'] ?? '' ),
|
||||
);
|
||||
if ( ! empty( $in['author'] ) || ! empty( $in['vendor_id'] ) ) {
|
||||
$post_data['post_author'] = absint( $in['author'] ?? $in['vendor_id'] );
|
||||
}
|
||||
$id = wp_insert_post( $post_data );
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
if ( isset( $in['price'] ) ) {
|
||||
update_post_meta( $id, '_price', sanitize_text_field( $in['price'] ) );
|
||||
update_post_meta( $id, '_regular_price', sanitize_text_field( $in['price'] ) );
|
||||
update_post_meta( $id, '_price', sanitize_text_field( (string) $in['price'] ) );
|
||||
update_post_meta( $id, '_regular_price', sanitize_text_field( (string) $in['price'] ) );
|
||||
}
|
||||
if ( isset( $in['regular_price'] ) ) {
|
||||
update_post_meta( $id, '_regular_price', sanitize_text_field( (string) $in['regular_price'] ) );
|
||||
if ( ! isset( $in['price'] ) ) {
|
||||
update_post_meta( $id, '_price', sanitize_text_field( (string) $in['regular_price'] ) );
|
||||
}
|
||||
}
|
||||
if ( isset( $in['sale_price'] ) ) {
|
||||
update_post_meta( $id, '_sale_price', sanitize_text_field( (string) $in['sale_price'] ) );
|
||||
if ( '' !== trim( (string) $in['sale_price'] ) ) {
|
||||
update_post_meta( $id, '_price', sanitize_text_field( (string) $in['sale_price'] ) );
|
||||
}
|
||||
}
|
||||
if ( isset( $in['sku'] ) ) {
|
||||
update_post_meta( $id, '_sku', sanitize_text_field( $in['sku'] ) );
|
||||
update_post_meta( $id, '_sku', sanitize_text_field( (string) $in['sku'] ) );
|
||||
}
|
||||
return array( 'success' => true, 'product_id' => $id );
|
||||
if ( isset( $in['virtual'] ) ) {
|
||||
$is_virtual = ( true === $in['virtual'] || 'yes' === $in['virtual'] || '1' === (string) $in['virtual'] ) ? 'yes' : 'no';
|
||||
update_post_meta( $id, '_virtual', $is_virtual );
|
||||
}
|
||||
if ( isset( $in['downloadable'] ) ) {
|
||||
$is_dl = ( true === $in['downloadable'] || 'yes' === $in['downloadable'] || '1' === (string) $in['downloadable'] ) ? 'yes' : 'no';
|
||||
update_post_meta( $id, '_downloadable', $is_dl );
|
||||
}
|
||||
if ( ! empty( $in['featured_image'] ) || ! empty( $in['thumbnail_id'] ) ) {
|
||||
$thumb_id = absint( $in['featured_image'] ?? $in['thumbnail_id'] );
|
||||
if ( $thumb_id && function_exists( 'set_post_thumbnail' ) ) {
|
||||
set_post_thumbnail( $id, $thumb_id );
|
||||
}
|
||||
}
|
||||
if ( ! empty( $in['categories'] ) && function_exists( 'wp_set_object_terms' ) ) {
|
||||
wp_set_object_terms( $id, (array) $in['categories'], 'product_cat' );
|
||||
}
|
||||
if ( ! empty( $in['tags'] ) && function_exists( 'wp_set_object_terms' ) ) {
|
||||
wp_set_object_terms( $id, (array) $in['tags'], 'product_tag' );
|
||||
}
|
||||
if ( isset( $in['meta'] ) && is_array( $in['meta'] ) ) {
|
||||
foreach ( $in['meta'] as $mk => $mv ) {
|
||||
update_post_meta( $id, sanitize_key( $mk ), $mv );
|
||||
}
|
||||
}
|
||||
return array(
|
||||
'success' => true,
|
||||
'product_id' => $id,
|
||||
'edit_link' => admin_url( 'post.php?post=' . $id . '&action=edit' ),
|
||||
);
|
||||
|
||||
case 'update-product':
|
||||
$id = (int) ( $in['product_id'] ?? 0 );
|
||||
if ( ! $id ) {
|
||||
return new WP_Error( 'missing_id', __( 'product_id required.', 'emcp-tools' ) );
|
||||
}
|
||||
if ( isset( $in['name'] ) ) {
|
||||
wp_update_post( array( 'ID' => $id, 'post_title' => sanitize_text_field( $in['name'] ) ) );
|
||||
$post_update = array( 'ID' => $id );
|
||||
if ( isset( $in['name'] ) || isset( $in['title'] ) ) {
|
||||
$post_update['post_title'] = sanitize_text_field( $in['name'] ?? $in['title'] );
|
||||
}
|
||||
if ( isset( $in['description'] ) || isset( $in['content'] ) ) {
|
||||
$post_update['post_content'] = (string) ( $in['description'] ?? $in['content'] );
|
||||
}
|
||||
if ( isset( $in['status'] ) ) {
|
||||
$post_update['post_status'] = sanitize_key( $in['status'] );
|
||||
}
|
||||
if ( ! empty( $in['author'] ) || ! empty( $in['vendor_id'] ) ) {
|
||||
$post_update['post_author'] = absint( $in['author'] ?? $in['vendor_id'] );
|
||||
}
|
||||
if ( count( $post_update ) > 1 ) {
|
||||
wp_update_post( $post_update );
|
||||
}
|
||||
if ( isset( $in['price'] ) ) {
|
||||
update_post_meta( $id, '_price', sanitize_text_field( $in['price'] ) );
|
||||
update_post_meta( $id, '_regular_price', sanitize_text_field( $in['price'] ) );
|
||||
update_post_meta( $id, '_price', sanitize_text_field( (string) $in['price'] ) );
|
||||
update_post_meta( $id, '_regular_price', sanitize_text_field( (string) $in['price'] ) );
|
||||
}
|
||||
if ( isset( $in['regular_price'] ) ) {
|
||||
update_post_meta( $id, '_regular_price', sanitize_text_field( (string) $in['regular_price'] ) );
|
||||
if ( ! isset( $in['price'] ) ) {
|
||||
update_post_meta( $id, '_price', sanitize_text_field( (string) $in['regular_price'] ) );
|
||||
}
|
||||
}
|
||||
if ( isset( $in['sale_price'] ) ) {
|
||||
update_post_meta( $id, '_sale_price', sanitize_text_field( (string) $in['sale_price'] ) );
|
||||
if ( '' !== trim( (string) $in['sale_price'] ) ) {
|
||||
update_post_meta( $id, '_price', sanitize_text_field( (string) $in['sale_price'] ) );
|
||||
}
|
||||
}
|
||||
if ( isset( $in['sku'] ) ) {
|
||||
update_post_meta( $id, '_sku', sanitize_text_field( $in['sku'] ) );
|
||||
update_post_meta( $id, '_sku', sanitize_text_field( (string) $in['sku'] ) );
|
||||
}
|
||||
if ( isset( $in['virtual'] ) ) {
|
||||
$is_virtual = ( true === $in['virtual'] || 'yes' === $in['virtual'] || '1' === (string) $in['virtual'] ) ? 'yes' : 'no';
|
||||
update_post_meta( $id, '_virtual', $is_virtual );
|
||||
}
|
||||
if ( isset( $in['downloadable'] ) ) {
|
||||
$is_dl = ( true === $in['downloadable'] || 'yes' === $in['downloadable'] || '1' === (string) $in['downloadable'] ) ? 'yes' : 'no';
|
||||
update_post_meta( $id, '_downloadable', $is_dl );
|
||||
}
|
||||
if ( isset( $in['featured_image'] ) || isset( $in['thumbnail_id'] ) ) {
|
||||
$thumb_id = absint( $in['featured_image'] ?? $in['thumbnail_id'] );
|
||||
if ( $thumb_id && function_exists( 'set_post_thumbnail' ) ) {
|
||||
set_post_thumbnail( $id, $thumb_id );
|
||||
} elseif ( 0 === $thumb_id && function_exists( 'delete_post_thumbnail' ) ) {
|
||||
delete_post_thumbnail( $id );
|
||||
}
|
||||
}
|
||||
if ( isset( $in['categories'] ) && function_exists( 'wp_set_object_terms' ) ) {
|
||||
wp_set_object_terms( $id, (array) $in['categories'], 'product_cat' );
|
||||
}
|
||||
if ( isset( $in['tags'] ) && function_exists( 'wp_set_object_terms' ) ) {
|
||||
wp_set_object_terms( $id, (array) $in['tags'], 'product_tag' );
|
||||
}
|
||||
if ( isset( $in['meta'] ) && is_array( $in['meta'] ) ) {
|
||||
foreach ( $in['meta'] as $mk => $mv ) {
|
||||
update_post_meta( $id, sanitize_key( $mk ), $mv );
|
||||
}
|
||||
}
|
||||
return array( 'success' => true, 'product_id' => $id );
|
||||
|
||||
|
||||
@@ -195,18 +195,24 @@ class EMCP_Tools_Schema_Compat {
|
||||
// runs. Picking the first non-null entry is the documented "lossy" way to
|
||||
// express the union without losing the schema altogether. Callers that need
|
||||
// both types should coerce at the executor (see `set-post-terms`).
|
||||
// When 'string' is in a non-null union (e.g. ['integer', 'string']), prefer
|
||||
// 'string' so term names and identifiers are both accepted without validator rejection.
|
||||
if ( isset( $schema['type'] ) && is_array( $schema['type'] ) ) {
|
||||
$flattened = null;
|
||||
foreach ( $schema['type'] as $candidate ) {
|
||||
if ( is_string( $candidate ) && 'null' !== $candidate ) {
|
||||
$flattened = $candidate;
|
||||
break;
|
||||
if ( in_array( 'string', $schema['type'], true ) && ! in_array( 'null', $schema['type'], true ) ) {
|
||||
$schema['type'] = 'string';
|
||||
} else {
|
||||
$flattened = null;
|
||||
foreach ( $schema['type'] as $candidate ) {
|
||||
if ( is_string( $candidate ) && 'null' !== $candidate ) {
|
||||
$flattened = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( null === $flattened ) {
|
||||
$flattened = 'string';
|
||||
}
|
||||
$schema['type'] = $flattened;
|
||||
}
|
||||
if ( null === $flattened ) {
|
||||
$flattened = 'string';
|
||||
}
|
||||
$schema['type'] = $flattened;
|
||||
}
|
||||
|
||||
if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests Content Abilities e-commerce meta and featured_image handling.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
*/
|
||||
|
||||
require_once dirname( __DIR__ ) . '/includes/abilities/class-content-abilities.php';
|
||||
|
||||
class ContentAbilitiesMetaTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
emcp_test_reset();
|
||||
}
|
||||
|
||||
public function test_allowed_protected_meta_contains_woocommerce_keys(): void {
|
||||
$allowed = EMCP_Tools_Content_Abilities::get_allowed_protected_meta();
|
||||
$this->assertIsArray( $allowed );
|
||||
|
||||
$expected_keys = array(
|
||||
'_price',
|
||||
'_regular_price',
|
||||
'_sale_price',
|
||||
'_virtual',
|
||||
'_downloadable',
|
||||
'_sku',
|
||||
'_stock_status',
|
||||
'_stock',
|
||||
'_thumbnail_id',
|
||||
'_pay_per_post_price',
|
||||
'_wp_page_template',
|
||||
);
|
||||
|
||||
foreach ( $expected_keys as $key ) {
|
||||
$this->assertContains( $key, $allowed, "Expected '{$key}' to be included in default allowed protected meta" );
|
||||
}
|
||||
|
||||
// Ensure dangerous internal Elementor keys are NOT in allowed list
|
||||
$this->assertNotContains( '_elementor_data', $allowed );
|
||||
$this->assertNotContains( '_elementor_css', $allowed );
|
||||
}
|
||||
|
||||
public function test_format_post_featured_image_without_thumbnail_returns_object_structure(): void {
|
||||
$abilities = new EMCP_Tools_Content_Abilities();
|
||||
|
||||
$post = new \WP_Post( array(
|
||||
'ID' => 42,
|
||||
'post_title' => 'Sample Post Without Thumbnail',
|
||||
'post_name' => 'sample-post',
|
||||
'post_status' => 'publish',
|
||||
'post_content' => 'Some content',
|
||||
'post_excerpt' => '',
|
||||
'post_date' => '2026-09-12 12:00:00',
|
||||
'post_type' => 'post',
|
||||
'post_author' => 1,
|
||||
'comment_status' => 'closed',
|
||||
) );
|
||||
|
||||
$ref = new \ReflectionClass( $abilities );
|
||||
$method = $ref->getMethod( 'format_post' );
|
||||
|
||||
$formatted = $method->invoke( $abilities, $post );
|
||||
$this->assertIsArray( $formatted );
|
||||
$this->assertArrayHasKey( 'featured_image', $formatted );
|
||||
$this->assertIsArray( $formatted['featured_image'] );
|
||||
$this->assertSame( 0, $formatted['featured_image']['id'] );
|
||||
$this->assertSame( '', $formatted['featured_image']['url'] );
|
||||
$this->assertSame( '', $formatted['featured_image']['alt'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests Database Abilities query execution with sql and query argument aliases.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
*/
|
||||
|
||||
require_once dirname( __DIR__ ) . '/includes/sql/class-sql-lexer.php';
|
||||
require_once dirname( __DIR__ ) . '/includes/sql/class-sql-policy.php';
|
||||
require_once dirname( __DIR__ ) . '/includes/class-database-guard.php';
|
||||
require_once dirname( __DIR__ ) . '/includes/abilities/class-database-abilities.php';
|
||||
|
||||
class DatabaseAbilitiesQueryTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
private EMCP_Tools_Database_Abilities $db_abilities;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
emcp_test_reset();
|
||||
$this->db_abilities = new EMCP_Tools_Database_Abilities();
|
||||
}
|
||||
|
||||
public function test_missing_sql_and_query_fails_cleanly(): void {
|
||||
$res = $this->db_abilities->execute_query( array() );
|
||||
$this->assertInstanceOf( \WP_Error::class, $res );
|
||||
$this->assertSame( 'missing_sql', $res->get_error_code() );
|
||||
|
||||
$res2 = $this->db_abilities->execute_query( array( 'sql' => ' ', 'query' => '' ) );
|
||||
$this->assertInstanceOf( \WP_Error::class, $res2 );
|
||||
$this->assertSame( 'missing_sql', $res2->get_error_code() );
|
||||
}
|
||||
|
||||
public function test_write_rejected_under_query_alias(): void {
|
||||
$res = $this->db_abilities->execute_query( array(
|
||||
'query' => "UPDATE wp_posts SET post_title = 'hacked'",
|
||||
) );
|
||||
$this->assertInstanceOf( \WP_Error::class, $res );
|
||||
$this->assertSame( 'not_read_only', $res->get_error_code() );
|
||||
}
|
||||
|
||||
public function test_user_table_protected_read_under_query_alias(): void {
|
||||
$res = $this->db_abilities->execute_query( array(
|
||||
'query' => "SELECT user_pass FROM wp_users",
|
||||
) );
|
||||
$this->assertInstanceOf( \WP_Error::class, $res );
|
||||
$this->assertSame( 'protected_read', $res->get_error_code() );
|
||||
}
|
||||
|
||||
public function test_sql_and_query_aliases_behave_identically(): void {
|
||||
$res_sql = $this->db_abilities->execute_query( array(
|
||||
'sql' => "DROP TABLE wp_posts",
|
||||
) );
|
||||
$res_query = $this->db_abilities->execute_query( array(
|
||||
'query' => "DROP TABLE wp_posts",
|
||||
) );
|
||||
|
||||
$this->assertInstanceOf( \WP_Error::class, $res_sql );
|
||||
$this->assertInstanceOf( \WP_Error::class, $res_query );
|
||||
$this->assertSame( $res_sql->get_error_code(), $res_query->get_error_code() );
|
||||
$this->assertSame( 'not_read_only', $res_sql->get_error_code() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests User Abilities (create-user, update-user, get-user) with meta support.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
*/
|
||||
|
||||
require_once dirname( __DIR__ ) . '/includes/abilities/class-user-abilities.php';
|
||||
|
||||
class UserAbilitiesTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
private EMCP_Tools_User_Abilities $abilities;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
emcp_test_reset();
|
||||
$this->abilities = new EMCP_Tools_User_Abilities();
|
||||
}
|
||||
|
||||
public function test_forbidden_meta_keys_blocked(): void {
|
||||
$blocked = array(
|
||||
'capabilities',
|
||||
'wp_capabilities',
|
||||
'user_level',
|
||||
'wp_user_level',
|
||||
'session_tokens',
|
||||
'default_password_nag',
|
||||
'user_pass',
|
||||
'auth_token',
|
||||
'secret_key',
|
||||
'activation_key',
|
||||
'nonce_field',
|
||||
'password_reset',
|
||||
);
|
||||
foreach ( $blocked as $key ) {
|
||||
$this->assertTrue(
|
||||
EMCP_Tools_User_Abilities::is_forbidden_user_meta_key( $key ),
|
||||
"Expected key '{$key}' to be forbidden"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_safe_vendor_and_profile_meta_keys_allowed(): void {
|
||||
$allowed = array(
|
||||
'wcfmmp_profile_settings',
|
||||
'store_name',
|
||||
'phone',
|
||||
'twitter',
|
||||
'instagram',
|
||||
'payout_paypal',
|
||||
'creator_tier',
|
||||
'billing_address_1',
|
||||
'shipping_city',
|
||||
'model_bio',
|
||||
);
|
||||
foreach ( $allowed as $key ) {
|
||||
$this->assertFalse(
|
||||
EMCP_Tools_User_Abilities::is_forbidden_user_meta_key( $key ),
|
||||
"Expected key '{$key}' to be permitted"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_create_user_with_safe_meta(): void {
|
||||
$input = array(
|
||||
'username' => 'testvendor',
|
||||
'email' => '[email protected]',
|
||||
'role' => 'wcfm_vendor',
|
||||
'meta' => array(
|
||||
'store_name' => 'Jackie Studio',
|
||||
'creator_tier' => 'vip',
|
||||
'capabilities' => 'hacked', // Forbidden!
|
||||
),
|
||||
);
|
||||
$result = $this->abilities->execute_create_user( $input );
|
||||
$this->assertIsArray( $result );
|
||||
$this->assertArrayHasKey( 'id', $result );
|
||||
$id = $result['id'];
|
||||
|
||||
// Safe meta saved
|
||||
$this->assertSame( 'Jackie Studio', get_user_meta( $id, 'store_name', true ) );
|
||||
$this->assertSame( 'vip', get_user_meta( $id, 'creator_tier', true ) );
|
||||
|
||||
// Forbidden meta dropped
|
||||
$this->assertSame( '', get_user_meta( $id, 'capabilities', true ) );
|
||||
}
|
||||
|
||||
public function test_update_user_with_safe_meta(): void {
|
||||
$user_id = wp_insert_user( array(
|
||||
'user_login' => 'model1',
|
||||
'user_email' => '[email protected]',
|
||||
'role' => 'wcfm_vendor',
|
||||
) );
|
||||
|
||||
$input = array(
|
||||
'id' => $user_id,
|
||||
'meta' => array(
|
||||
'twitter' => 'https://x.com/model1',
|
||||
'user_pass' => 'injected', // Forbidden!
|
||||
),
|
||||
);
|
||||
$res = $this->abilities->execute_update_user( $input );
|
||||
$this->assertIsArray( $res );
|
||||
$this->assertContains( 'meta', $res['updated'] );
|
||||
|
||||
// Safe meta saved
|
||||
$this->assertSame( 'https://x.com/model1', get_user_meta( $user_id, 'twitter', true ) );
|
||||
|
||||
// Forbidden meta dropped
|
||||
$this->assertSame( '', get_user_meta( $user_id, 'user_pass', true ) );
|
||||
}
|
||||
|
||||
public function test_get_user_with_include_meta(): void {
|
||||
$user_id = wp_insert_user( array(
|
||||
'user_login' => 'model2',
|
||||
'user_email' => '[email protected]',
|
||||
'role' => 'wcfm_vendor',
|
||||
) );
|
||||
update_user_meta( $user_id, 'store_name', 'Model 2 Store' );
|
||||
update_user_meta( $user_id, 'session_tokens', 'supersecret' );
|
||||
|
||||
// Without include_meta
|
||||
$without = $this->abilities->execute_get_user( array( 'id' => $user_id ) );
|
||||
$this->assertIsArray( $without );
|
||||
$this->assertEquals( (object) array(), $without['meta'] );
|
||||
|
||||
// With include_meta
|
||||
$with = $this->abilities->execute_get_user( array( 'id' => $user_id, 'include_meta' => true ) );
|
||||
$this->assertIsArray( $with );
|
||||
$this->assertArrayHasKey( 'meta', $with );
|
||||
$this->assertArrayHasKey( 'store_name', $with['meta'] );
|
||||
$this->assertSame( 'Model 2 Store', $with['meta']['store_name'] );
|
||||
|
||||
// Sensitive session_tokens redacted
|
||||
$this->assertArrayNotHasKey( 'session_tokens', $with['meta'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests WooCommerce and Marketplace Creator Integration.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
*/
|
||||
|
||||
require_once dirname( __DIR__ ) . '/includes/abilities/class-woo-integration.php';
|
||||
|
||||
class WooIntegrationTest extends \PHPUnit\Framework\TestCase {
|
||||
|
||||
private EMCP_Tools_Woo_Integration $woo;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
emcp_test_reset();
|
||||
$this->woo = new EMCP_Tools_Woo_Integration();
|
||||
}
|
||||
|
||||
public function test_catalog_operations_includes_list_vendors(): void {
|
||||
$catalog = $this->woo->execute_read( array( 'operation' => '' ) );
|
||||
$this->assertIsArray( $catalog );
|
||||
$this->assertArrayHasKey( 'operations', $catalog );
|
||||
$this->assertArrayHasKey( 'list-vendors', $catalog['operations'] );
|
||||
$this->assertArrayHasKey( 'list-products', $catalog['operations'] );
|
||||
}
|
||||
|
||||
public function test_create_product_with_vendor_and_virtual(): void {
|
||||
// Mock wp_insert_post if needed, or check stub
|
||||
if ( ! function_exists( 'wp_insert_post' ) ) {
|
||||
// define stub
|
||||
}
|
||||
|
||||
$input = array(
|
||||
'operation' => 'create-product',
|
||||
'arguments' => array(
|
||||
'name' => 'VIP Access Pass',
|
||||
'price' => '14.99',
|
||||
'regular_price' => '14.99',
|
||||
'sale_price' => '9.99',
|
||||
'sku' => 'VIP-PASS-01',
|
||||
'author' => 2,
|
||||
'virtual' => true,
|
||||
'downloadable' => true,
|
||||
'meta' => array(
|
||||
'custom_perk' => 'exclusive_vods',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$res = $this->woo->execute_write( $input );
|
||||
$this->assertIsArray( $res );
|
||||
$this->assertTrue( $res['success'] );
|
||||
$product_id = $res['product_id'];
|
||||
|
||||
$this->assertSame( '9.99', get_post_meta( $product_id, '_price', true ) );
|
||||
$this->assertSame( '14.99', get_post_meta( $product_id, '_regular_price', true ) );
|
||||
$this->assertSame( '9.99', get_post_meta( $product_id, '_sale_price', true ) );
|
||||
$this->assertSame( 'VIP-PASS-01', get_post_meta( $product_id, '_sku', true ) );
|
||||
$this->assertSame( 'yes', get_post_meta( $product_id, '_virtual', true ) );
|
||||
$this->assertSame( 'yes', get_post_meta( $product_id, '_downloadable', true ) );
|
||||
$this->assertSame( 'exclusive_vods', get_post_meta( $product_id, 'custom_perk', true ) );
|
||||
|
||||
$post = get_post( $product_id );
|
||||
$this->assertNotNull( $post );
|
||||
$this->assertSame( 2, (int) $post->post_author );
|
||||
}
|
||||
|
||||
public function test_list_vendors_returns_models(): void {
|
||||
$user_id = wp_insert_user( array(
|
||||
'user_login' => 'jackie12358',
|
||||
'user_email' => '[email protected]',
|
||||
'display_name' => 'Jackie Trans Goddess',
|
||||
'role' => 'wcfm_vendor',
|
||||
) );
|
||||
|
||||
update_user_meta( $user_id, 'wcfmmp_profile_settings', array(
|
||||
'store_name' => 'Jackie VIP Studio',
|
||||
) );
|
||||
|
||||
// Stub get_users if not stubbed
|
||||
$input = array(
|
||||
'operation' => 'list-vendors',
|
||||
'arguments' => array( 'role' => 'wcfm_vendor' ),
|
||||
);
|
||||
|
||||
$res = $this->woo->execute_read( $input );
|
||||
$this->assertIsArray( $res );
|
||||
$this->assertArrayHasKey( 'vendors', $res );
|
||||
$this->assertCount( 1, $res['vendors'] );
|
||||
$this->assertSame( 'jackie12358', $res['vendors'][0]['username'] );
|
||||
$this->assertSame( 'Jackie VIP Studio', $res['vendors'][0]['store_name'] );
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,14 @@ function emcp_test_reset(): void {
|
||||
'options_pages' => array(),
|
||||
'abilities' => array(), // name => registration args.
|
||||
'options' => array(), // option name => value (get_option/update_option).
|
||||
'users' => array(), // user_id => user object.
|
||||
'usermeta' => array(), // user_id => [ key => val ].
|
||||
'user_caps' => array(), // user_id => [ cap => bool ].
|
||||
'roles' => array(
|
||||
'subscriber' => (object) array( 'name' => 'Subscriber', 'capabilities' => array( 'read' => true ) ),
|
||||
'wcfm_vendor' => (object) array( 'name' => 'Vendor', 'capabilities' => array( 'read' => true, 'edit_posts' => true ) ),
|
||||
'administrator' => (object) array( 'name' => 'Administrator', 'capabilities' => array( 'manage_options' => true, 'edit_users' => true ) ),
|
||||
),
|
||||
'cpt_tax_supported' => true, // Toggles the ACF 6.1+ CPT/tax API stubs.
|
||||
'acf_post_types' => array(), // key/ID => acf-post-type definition.
|
||||
'acf_taxonomies' => array(), // key/ID => acf-taxonomy definition.
|
||||
@@ -52,6 +60,13 @@ function emcp_test_reset(): void {
|
||||
'imported_taxes' => array(), // recorded acf_import_taxonomy() args.
|
||||
'updated_internal' => array(), // recorded acf_update_internal_post_type() args.
|
||||
);
|
||||
$GLOBALS['wpdb'] = (object) array(
|
||||
'prefix' => 'wp_',
|
||||
'users' => 'wp_users',
|
||||
'usermeta' => 'wp_usermeta',
|
||||
'posts' => 'wp_posts',
|
||||
'postmeta' => 'wp_postmeta',
|
||||
);
|
||||
}
|
||||
emcp_test_reset();
|
||||
|
||||
@@ -153,6 +168,79 @@ function get_post( $post_id ) {
|
||||
return $GLOBALS['emcp_test']['posts'][ (int) $post_id ] ?? null;
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_insert_post' ) ) {
|
||||
function wp_insert_post( $postarr, $wp_error = false ) {
|
||||
static $next_post_id = 500;
|
||||
$id = ++$next_post_id;
|
||||
$obj = new WP_Post( array_merge( array(
|
||||
'ID' => $id,
|
||||
'post_title' => $postarr['post_title'] ?? '',
|
||||
'post_content' => $postarr['post_content'] ?? '',
|
||||
'post_status' => $postarr['post_status'] ?? 'publish',
|
||||
'post_type' => $postarr['post_type'] ?? 'post',
|
||||
'post_author' => $postarr['post_author'] ?? 0,
|
||||
'post_date' => date( 'Y-m-d H:i:s' ),
|
||||
), (array) $postarr ) );
|
||||
$GLOBALS['emcp_test']['posts'][ $id ] = $obj;
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_update_post' ) ) {
|
||||
function wp_update_post( $postarr, $wp_error = false ) {
|
||||
$id = (int) ( is_array( $postarr ) ? ( $postarr['ID'] ?? 0 ) : ( $postarr->ID ?? 0 ) );
|
||||
if ( ! isset( $GLOBALS['emcp_test']['posts'][ $id ] ) ) {
|
||||
return 0;
|
||||
}
|
||||
$existing = (array) $GLOBALS['emcp_test']['posts'][ $id ];
|
||||
$GLOBALS['emcp_test']['posts'][ $id ] = new WP_Post( array_merge( $existing, (array) $postarr ) );
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_users' ) ) {
|
||||
function get_users( $args = array() ) {
|
||||
$role = $args['role'] ?? '';
|
||||
$out = array();
|
||||
foreach ( (array) ( $GLOBALS['emcp_test']['users'] ?? array() ) as $u ) {
|
||||
if ( '' === $role || ( ! empty( $u->roles ) && in_array( $role, (array) $u->roles, true ) ) ) {
|
||||
$out[] = $u;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_post_meta' ) ) {
|
||||
function get_post_meta( $post_id, $key = '', $single = false ) {
|
||||
$post_id = (int) $post_id;
|
||||
$store = $GLOBALS['emcp_test']['postmeta'][ $post_id ] ?? array();
|
||||
if ( '' === $key ) {
|
||||
$out = array();
|
||||
foreach ( $store as $k => $v ) {
|
||||
$out[ $k ] = (array) $v;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
if ( ! isset( $store[ $key ] ) ) {
|
||||
return $single ? '' : array();
|
||||
}
|
||||
$val = $store[ $key ];
|
||||
return $single ? $val : (array) $val;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'update_post_meta' ) ) {
|
||||
function update_post_meta( $post_id, $key, $value ) {
|
||||
$post_id = (int) $post_id;
|
||||
if ( ! isset( $GLOBALS['emcp_test']['postmeta'][ $post_id ] ) ) {
|
||||
$GLOBALS['emcp_test']['postmeta'][ $post_id ] = array();
|
||||
}
|
||||
$GLOBALS['emcp_test']['postmeta'][ $post_id ][ $key ] = $value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'delete_post_meta' ) ) {
|
||||
// Records deletions in $GLOBALS['emcp_test']['deleted_meta'] as [post_id, key].
|
||||
function delete_post_meta( $post_id, $key, $value = '' ) {
|
||||
@@ -165,6 +253,37 @@ function get_permalink( $post = null ): string {
|
||||
return 'http://example.test/?p=' . ( is_object( $post ) ? (int) $post->ID : (int) $post );
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_object_taxonomies' ) ) {
|
||||
function get_object_taxonomies( $object, $output = 'names' ) {
|
||||
return array( 'category', 'post_tag' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_the_terms' ) ) {
|
||||
function get_the_terms( $post, $taxonomy ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_post_thumbnail_id' ) ) {
|
||||
function get_post_thumbnail_id( $post = null ) {
|
||||
$post_id = is_object( $post ) ? (int) ( $post->ID ?? 0 ) : (int) $post;
|
||||
return (int) get_post_meta( $post_id, '_thumbnail_id', true );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_get_attachment_image_url' ) ) {
|
||||
function wp_get_attachment_image_url( $attachment_id, $size = 'thumbnail', $icon = false ) {
|
||||
return $attachment_id ? "https://example.com/wp-content/uploads/{$attachment_id}.jpg" : false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'is_protected_meta' ) ) {
|
||||
function is_protected_meta( $meta_key, $meta_type = null ) {
|
||||
return '_' === substr( (string) $meta_key, 0, 1 );
|
||||
}
|
||||
}
|
||||
|
||||
function admin_url( $path = '' ): string {
|
||||
return 'http://example.test/wp-admin/' . $path;
|
||||
}
|
||||
@@ -198,6 +317,158 @@ function get_post_status( $id ) {
|
||||
return $GLOBALS['emcp_test']['post_status'][ (int) $id ] ?? false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WordPress User and Meta stubs (fixture-driven)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if ( ! function_exists( 'get_userdata' ) ) {
|
||||
function get_userdata( $id ) {
|
||||
$u = $GLOBALS['emcp_test']['users'][ (int) $id ] ?? null;
|
||||
return $u ? ( is_object( $u ) ? $u : (object) $u ) : false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_user_meta' ) ) {
|
||||
function get_user_meta( $user_id, $key = '', $single = false ) {
|
||||
$user_id = (int) $user_id;
|
||||
$store = $GLOBALS['emcp_test']['usermeta'][ $user_id ] ?? array();
|
||||
if ( '' === $key ) {
|
||||
$out = array();
|
||||
foreach ( $store as $k => $v ) {
|
||||
$out[ $k ] = (array) $v;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
if ( ! isset( $store[ $key ] ) ) {
|
||||
return $single ? '' : array();
|
||||
}
|
||||
$val = $store[ $key ];
|
||||
return $single ? $val : (array) $val;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'update_user_meta' ) ) {
|
||||
function update_user_meta( $user_id, $key, $value ) {
|
||||
$user_id = (int) $user_id;
|
||||
if ( ! isset( $GLOBALS['emcp_test']['usermeta'][ $user_id ] ) ) {
|
||||
$GLOBALS['emcp_test']['usermeta'][ $user_id ] = array();
|
||||
}
|
||||
$GLOBALS['emcp_test']['usermeta'][ $user_id ][ $key ] = $value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_insert_user' ) ) {
|
||||
function wp_insert_user( $userdata ) {
|
||||
static $next_id = 100;
|
||||
$id = ++$next_id;
|
||||
$obj = (object) array_merge( array(
|
||||
'ID' => $id,
|
||||
'user_login' => $userdata['user_login'] ?? '',
|
||||
'user_email' => $userdata['user_email'] ?? '',
|
||||
'display_name' => $userdata['display_name'] ?? $userdata['user_login'] ?? '',
|
||||
'first_name' => $userdata['first_name'] ?? '',
|
||||
'last_name' => $userdata['last_name'] ?? '',
|
||||
'user_url' => $userdata['user_url'] ?? '',
|
||||
'description' => $userdata['description'] ?? '',
|
||||
'roles' => array( $userdata['role'] ?? 'subscriber' ),
|
||||
'user_registered' => date( 'Y-m-d H:i:s' ),
|
||||
), (array) $userdata );
|
||||
$GLOBALS['emcp_test']['users'][ $id ] = $obj;
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_update_user' ) ) {
|
||||
function wp_update_user( $userdata ) {
|
||||
$id = (int) ( is_array( $userdata ) ? ( $userdata['ID'] ?? 0 ) : ( $userdata->ID ?? 0 ) );
|
||||
if ( ! isset( $GLOBALS['emcp_test']['users'][ $id ] ) ) {
|
||||
return new WP_Error( 'invalid_user_id', 'User not found' );
|
||||
}
|
||||
$curr = (array) $GLOBALS['emcp_test']['users'][ $id ];
|
||||
$GLOBALS['emcp_test']['users'][ $id ] = (object) array_merge( $curr, (array) $userdata );
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'user_can' ) ) {
|
||||
function user_can( $user, $capability ) {
|
||||
$user_id = is_object( $user ) ? (int) ( $user->ID ?? 0 ) : (int) $user;
|
||||
if ( isset( $GLOBALS['emcp_test']['user_caps'][ $user_id ][ $capability ] ) ) {
|
||||
return (bool) $GLOBALS['emcp_test']['user_caps'][ $user_id ][ $capability ];
|
||||
}
|
||||
$u = $GLOBALS['emcp_test']['users'][ $user_id ] ?? null;
|
||||
if ( $u && ! empty( $u->roles ) && in_array( 'administrator', (array) $u->roles, true ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_role' ) ) {
|
||||
function get_role( $role ) {
|
||||
return $GLOBALS['emcp_test']['roles'][ $role ] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'sanitize_user' ) ) {
|
||||
function sanitize_user( $username, $strict = false ) {
|
||||
return preg_replace( '/[^a-zA-Z0-9_\-\.]/', '', (string) $username );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'sanitize_email' ) ) {
|
||||
function sanitize_email( $email ) {
|
||||
return filter_var( (string) $email, FILTER_SANITIZE_EMAIL ) ?: '';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'is_email' ) ) {
|
||||
function is_email( $email ) {
|
||||
return (bool) filter_var( (string) $email, FILTER_VALIDATE_EMAIL );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_generate_password' ) ) {
|
||||
function wp_generate_password( $length = 12, $special_chars = true, $extra_special_chars = false ) {
|
||||
return bin2hex( random_bytes( max( 1, (int) ( $length / 2 ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'maybe_unserialize' ) ) {
|
||||
function maybe_unserialize( $original ) {
|
||||
if ( is_string( $original ) && is_serialized( $original ) ) {
|
||||
return @unserialize( $original );
|
||||
}
|
||||
return $original;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'is_serialized' ) ) {
|
||||
function is_serialized( $data, $strict = true ) {
|
||||
if ( ! is_string( $data ) ) {
|
||||
return false;
|
||||
}
|
||||
$data = trim( $data );
|
||||
if ( 'N;' === $data ) {
|
||||
return true;
|
||||
}
|
||||
if ( strlen( $data ) < 4 ) {
|
||||
return false;
|
||||
}
|
||||
if ( ':' !== $data[1] ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'count_user_posts' ) ) {
|
||||
function count_user_posts( $userid, $post_type = 'post', $public_only = false ) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ACF stubs (fixture-driven)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user