diff --git a/posts-bridge/addons/airtable/hooks.php b/posts-bridge/addons/airtable/hooks.php index 20bb685..bf3da27 100644 --- a/posts-bridge/addons/airtable/hooks.php +++ b/posts-bridge/addons/airtable/hooks.php @@ -72,22 +72,17 @@ function posts_bridge_airtable_register_http_defaults( $setting ) { $setting['backends'] = $backends; } - $schemas = array_column( $credentials, 'schema' ); - $exists = array_search( $credential_schema, $schemas, true ); - - if ( false === $exists ) { - $names = array_column( $credentials, 'name' ); - - if ( ! in_array( $credential_name, $names, true ) ) { - $credentials[] = array( - 'name' => $credential_name, - 'schema' => $credential_schema, - 'access_token' => 'your-api-key', - 'expires_at' => time() + 60 * 60 * 24 * 365 * 100, - ); + $names = array_column( $credentials, 'name' ); + + if ( ! in_array( $credential_name, $names, true ) ) { + $credentials[] = array( + 'name' => $credential_name, + 'schema' => $credential_schema, + 'access_token' => 'your-api-key', + 'expires_at' => time() + 60 * 60 * 24 * 365 * 100, + ); - $setting['credentials'] = $credentials; - } + $setting['credentials'] = $credentials; } } diff --git a/posts-bridge/addons/grist/hooks.php b/posts-bridge/addons/grist/hooks.php index 7d7ed97..d4393ba 100644 --- a/posts-bridge/addons/grist/hooks.php +++ b/posts-bridge/addons/grist/hooks.php @@ -17,8 +17,8 @@ function ( $schema, $addon ) { } $schema['properties']['foreign_key']['const'] = 'id'; - $schema['properties']['endpoint']['default'] = '/v0/{baseId}/{tableName}/records'; - $schema['properties']['single_endpoint']['const'] = '/v0/{baseId}/{tableName}/records'; + $schema['properties']['endpoint']['default'] = '/api/docs/{docId}/tables/{tableId}/records'; + $schema['properties']['single_endpoint']['const'] = '/api/docs/{docId}/tables/{tableId}/records'; $schema['properties']['backend']['default'] = 'Grist API'; $schema['properties']['method']['const'] = 'GET'; @@ -54,7 +54,7 @@ function posts_bridge_grist_register_http_defaults( $setting ) { $exists = array_search( $backend_url, $urls, true ); if ( false === $exists ) { - $names = array_column( $backends, 'base_url' ); + $names = array_column( $backends, 'name' ); if ( ! in_array( $backend_name, $names, true ) ) { $backends[] = array( @@ -72,22 +72,17 @@ function posts_bridge_grist_register_http_defaults( $setting ) { $setting['backends'] = $backends; } - $schemas = array_column( $credentials, 'schema' ); - $exists = array_search( $credential_schema, $schemas, true ); + $names = array_column( $credentials, 'name' ); - if ( false === $exists ) { - $names = array_column( $credentials, 'name' ); - - if ( ! in_array( $credential_name, $names, true ) ) { - $credentials[] = array( - 'name' => $credential_name, - 'schema' => $credential_schema, - 'access_token' => 'your-api-key', - 'expires_at' => time() + 60 * 60 * 24 * 365 * 100, - ); + if ( ! in_array( $credential_name, $names, true ) ) { + $credentials[] = array( + 'name' => $credential_name, + 'schema' => $credential_schema, + 'access_token' => 'your-api-key', + 'expires_at' => time() + 60 * 60 * 24 * 365 * 100, + ); - $setting['credentials'] = $credentials; - } + $setting['credentials'] = $credentials; } } diff --git a/posts-bridge/includes/class-api.php b/posts-bridge/includes/class-api.php index 04ae772..1b08614 100644 --- a/posts-bridge/includes/class-api.php +++ b/posts-bridge/includes/class-api.php @@ -6,7 +6,6 @@ */ use POSTS_BRIDGE\Settings_Store; -use POSTS_BRIDGE\Posts_Bridge; use POSTS_BRIDGE\Addon; use POSTS_BRIDGE\Post_Bridge; use HTTP_BRIDGE\Backend; diff --git a/posts-bridge/includes/class-custom-post-type.php b/posts-bridge/includes/class-custom-post-type.php index 236d122..ea7c7e6 100644 --- a/posts-bridge/includes/class-custom-post-type.php +++ b/posts-bridge/includes/class-custom-post-type.php @@ -36,6 +36,21 @@ class Custom_Post_Type { 'wp_navigation', 'wp_font_family', 'wp_font_face', + /* ACF */ + 'acf-field', + 'acf-field-group', + 'acf-post-type', + 'acf-taxonomy', + /* WPForms */ + 'wpforms', + 'wpforms-template', + /* Contact Form 7 */ + 'wpcf7_contact_form', + /* Forminator */ + 'frm_form_actions', + 'frm_styles', + /* Ninja forms */ + 'nf_sub', ); /** @@ -407,7 +422,9 @@ private static function registry() { return self::$registry; } - self::$registry = (array) get_option( self::OPTION_NAME, array() ); + $post_types = (array) get_option( self::OPTION_NAME, array() ); + self::$registry = $post_types; + return self::$registry; } @@ -436,14 +453,14 @@ private static function init() { public static function post_types() { $post_types = array_keys( get_post_types() ); - return array_values( - array_filter( - $post_types, - static function ( $post_type ) { - return ! in_array( $post_type, self::INTERNAL_TYPES, true ); - } - ) - ); + $public_post_types = array(); + foreach ( $post_types as $pt ) { + if ( ! in_array( $pt, self::INTERNAL_TYPES, true ) ) { + $public_post_types[] = $pt; + } + } + + return $public_post_types; } /** @@ -452,7 +469,7 @@ static function ( $post_type ) { * @param string $name Type name. * @param array $args Registration arguments. * - * @return WP_Post_Type|WP_Error + * @return \WP_Post_Type|\WP_Error */ private static function register_post_type( $name, $args ) { $args['labels'] = array( @@ -504,6 +521,7 @@ static function ( $store ) { 'general', static function ( $data ) { $data['post_types'] = self::post_types(); + return $data; } ); @@ -514,6 +532,7 @@ static function ( $data ) { if ( isset( $data['post_types'] ) ) { unset( $data['post_types'] ); } + return $data; }, 9 @@ -522,6 +541,50 @@ static function ( $data ) { ); } + private static function get_acf_cpts( $filter = null ) { + if ( ! function_exists( 'acf_get_raw_post_types' ) ) { + return array(); + } + + $acf_raw_cpts = acf_get_raw_post_types(); + + $acf_cpts = array(); + foreach ( $acf_raw_cpts as $acf_raw_cpt ) { + $name = $acf_raw_cpt['post_type']; + + if ( is_array( $filter ) && ! in_array( $name, $filter, true ) ) { + continue; + } + + $acf_cpts[] = array( + 'name' => $name, + 'label' => $acf_raw_cpt['labels']['name'], + 'singular_label' => $acf_raw_cpt['labels']['singular_label'], + 'description' => $acf_raw_cpt['description'], + 'public' => $acf_raw_cpt['public'], + 'exclude_from_search' => $acf_raw_cpt['exclude_from_search'], + 'publicly_queryable' => $acf_raw_cpt['publicly_queryable'], + 'show_ui' => $acf_raw_cpt['show_ui'], + 'show_in_menu' => $acf_raw_cpt['show_in_menu'], + 'show_in_nav_menus' => $acf_raw_cpt['show_in_nav_menus'], + 'show_in_rest' => $acf_raw_cpt['show_in_rest'], + 'rest_base' => $acf_raw_cpt['rest_base'], + 'menu_position' => $acf_raw_cpt['menu_position'], + 'capability_type' => $acf_raw_cpt['singular_capability_name'], + 'map_meta_cap' => true, + 'supports' => $acf_raw_cpt['supports'], + 'taxonomies' => $acf_raw_cpt['taxonomies'], + 'has_archive' => $acf_raw_cpt['has_archive'], + 'rewrite' => $acf_raw_cpt['rewrite']['permalink_rewrite'] ?? false, + 'query_var' => $acf_raw_cpt['query_var'], + 'meta' => array(), + '_acf' => true, + ); + } + + return $acf_cpts; + } + /** * Post type registration arguments sanitizer. * @@ -561,34 +624,34 @@ private static function sanitize_args( $name, $args ) { : 'post', 'map_meta_cap' => boolval( $args['map_meta_cap'] ?? true ), 'supports' => isset( $args['supports'] ) && is_array( $args['supports'] ) - ? array_map( - function ( $token ) { - return trim( $token ); - }, - array_filter( - $args['supports'], - static function ( - $token - ) { - $token = trim( $token ); - $tokens = self::schema()['properties']['supports']['enum']; - - return in_array( $token, $tokens, true ); - } - ) + ? array_map( + function ( $token ) { + return trim( $token ); + }, + array_filter( + $args['supports'], + static function ( + $token + ) { + $token = trim( $token ); + $tokens = self::schema()['properties']['supports']['enum']; + + return in_array( $token, $tokens, true ); + } ) - : self::schema()['properties']['supports']['default'], + ) + : self::schema()['properties']['supports']['default'], 'taxonomies' => isset( $args['taxonomies'] ) && is_string( $args['taxonomies'] ) - ? implode( - ',', - array_map( - function ( $tax ) { - return sanitize_text_field( trim( $tax ) ); - }, - explode( ',', $args['taxonomies'] ) - ) + ? implode( + ',', + array_map( + function ( $tax ) { + return sanitize_text_field( trim( $tax ) ); + }, + explode( ',', $args['taxonomies'] ) ) - : array(), + ) + : array(), 'has_archive' => boolval( $args['has_archive'] ?? false ), 'rewrite' => ! empty( $args['rewrite'] ) && is_string( $args['rewrite'] ) ? sanitize_title( $args['rest_base'] ) diff --git a/posts-bridge/includes/class-posts-bridge.php b/posts-bridge/includes/class-posts-bridge.php index 79f6172..db7885b 100644 --- a/posts-bridge/includes/class-posts-bridge.php +++ b/posts-bridge/includes/class-posts-bridge.php @@ -43,10 +43,18 @@ class Posts_Bridge extends Base_Plugin { /** * Handle post types REST Controller instances * - * @var array $rest_controllers Array of active REST_Controller instances + * @var array<\WP_REST_Controller> */ private static $rest_controllers = array(); + /** + * Handle a boolean value indicating if Posts Bridge should support ACF custom fields and + * post types. + * + * @var bool + */ + private static $acf_support = false; + /** * Initialized addons, sets up plugin hooks and run db migrations if db version mismatches * the plugin version. @@ -152,6 +160,27 @@ function ( $plugin_data, $response ) { 10, 2 ); + + add_action( + 'init', + static function () { + if ( defined( 'ACF_VERSION' ) ) { + self::$acf_support = function_exists( 'acf_get_field_groups' ) + && function_exists( 'acf_get_fields' ); + } + }, + 9, + 0, + ); + } + + /** + * Public proxy to the static acf_support attribute. + * + * @return bool + */ + public static function acf_support() { + return self::$acf_support; } /** @@ -201,7 +230,7 @@ protected static function init() { } /** - * Enqueue admin client scripts + * Enqueue admin client scripts. */ private static function admin_enqueue_scripts() { $version = self::version(); diff --git a/posts-bridge/includes/class-posts-synchronizer.php b/posts-bridge/includes/class-posts-synchronizer.php index 0f8c7e7..34a9721 100644 --- a/posts-bridge/includes/class-posts-synchronizer.php +++ b/posts-bridge/includes/class-posts-synchronizer.php @@ -624,6 +624,8 @@ private static function sync_bridge( $bridge, $mode, $queue ) { return false; } + $custom_fields = Remote_CPT::custom_fields( $post_type ); + $remote_pairs = array(); foreach ( $foreign_ids as $id ) { $remote_pairs[ $id ] = 0; @@ -731,6 +733,18 @@ private static function sync_bridge( $bridge, $mode, $queue ) { Logger::log( "Remote CPT({$post_type}) #{$foreign_id} remote data after mappers" ); Logger::log( $post_data ); + $meta_input = &$post_data['meta_input']; + $acf_fields = array(); + + foreach ( $custom_fields as $cf ) { + if ( ! $cf['_acf'] || ! isset( $meta_input[ $cf['name'] ] ) ) { + continue; + } + + $acf_fields[ $cf['name'] ] = $meta_input[ $cf['name'] ]; + unset( $meta_input[ $cf['name'] ] ); + } + $post_id = wp_insert_post( $post_data ); if ( is_wp_error( $post_id ) || ! $post_id ) { @@ -738,6 +752,10 @@ private static function sync_bridge( $bridge, $mode, $queue ) { throw new Exception( 'insert_error', 500 ); } + foreach ( $acf_fields as $field_name => $field_value ) { + update_field( $field_name, $field_value, $post_id ); + } + update_post_meta( $post_id, Remote_CPT::FOREIGN_KEY_HANDLE, $foreign_id ); if ( isset( $post_data['featured_media'] ) ) { diff --git a/posts-bridge/includes/class-remote-cpt.php b/posts-bridge/includes/class-remote-cpt.php index 35020fb..8a6ed94 100644 --- a/posts-bridge/includes/class-remote-cpt.php +++ b/posts-bridge/includes/class-remote-cpt.php @@ -193,4 +193,141 @@ public function terms( $tax_name ) { public function meta( $field, $single = true ) { return get_post_meta( $this->ID, $field, $single ); } + + /** + * Returns the collection of known custom fields related to a given post type. + * Known custom fields includes the global $wp_meta_keys object, ACF field groups + * and the meta_key column of the wp_postmeta db table. + * + * @param string $post_type Post type key. + * + * @return array + */ + public static function custom_fields( $post_type ) { + global $wp_meta_keys; + $pt_meta = $wp_meta_keys['post'][ $post_type ] ?? array(); + + $custom_fields = array(); + + foreach ( $pt_meta as $name => $defn ) { + $custom_fields[] = array( + 'name' => $name, + 'schema' => array( + 'type' => $defn['type'], + 'default' => $defn['default'] ?? '', + ), + '_acf' => false, + ); + } + + if ( Posts_Bridge::acf_support() ) { + $groups = acf_get_field_groups(); + + foreach ( $groups as $group ) { + if ( ! is_array( $group['location'] ?? false ) ) { + continue; + } + + $match = true; + + // Iterate over a list of location rule_groups and evaluate + // with or operators: a match in any of the groups results + // in a positive output. + foreach ( $group['location'] as $rule_group ) { + // Iterate over a list of location rules for each rule_group + // and evaluate with and operators: all rules must be passed + // in order to result in a positive output. + foreach ( $rule_group as $rule ) { + $rule = acf_validate_location_rule( $rule ); + + if ( 'post_type' !== $rule['param'] ) { + continue; + } + + if ( '==' === $rule['operator'] && $rule['value'] !== $post_type ) { + $match = false; + } elseif ( '!=' === $rule['operator'] && $rule['value'] === $post_type ) { + $match = false; + } + + if ( ! $match ) { + break; + } + } + + if ( ! $match ) { + break; + } + } + + if ( $match ) { + $fields = acf_get_fields( $group['ID'] ); + + foreach ( $fields as $field ) { + switch ( $field['type'] ) { + case 'number': + case 'boolean': + $field_type = $field['type']; + break; + case 'text': + case 'select': + default: + $field_type = 'string'; + break; + } + + $pt_meta[ $field['name'] ] = array( + 'type' => $field_type, + 'label' => $field['label'], + 'description' => '', + 'single' => true, + 'sanitize_callback' => null, + 'auth_callback' => '__return_true', + 'show_in_rest' => true, + 'revisions_enabled' => true, + '_name' => $field['_name'], + ); + + $pt_meta[ '_' . $field['name'] ] = $pt_meta[ $field['name'] ]; + + $custom_fields[] = array( + 'name' => $field['name'], + 'schema' => array( 'type' => $field_type ), + '_acf' => true, + ); + } + } + } + } + + global $wpdb; + + // phpcs:disable WordPress.DB.DirectDatabaseQuery + $result = $wpdb->get_results( + $wpdb->prepare( + "SELECT DISTINCT pm.meta_key FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE p.post_type = %s", + $post_type, + ), + ARRAY_A, + ); + // phpcs:enable + + $internal_fields = array( '_edit_lock', '_posts_bridge_foreign_key', '_thumbnail_id' ); + + foreach ( $result as $record ) { + if ( in_array( $record['meta_key'], $internal_fields, true ) ) { + continue; + } + + if ( ! isset( $pt_meta[ $record['meta_key'] ] ) ) { + $custom_fields[] = array( + 'name' => $record['meta_key'], + 'schema' => array( 'type' => 'string' ), + '_acf' => false, + ); + } + } + + return $custom_fields; + } } diff --git a/posts-bridge/includes/class-rest-remote-posts-controller.php b/posts-bridge/includes/class-rest-remote-posts-controller.php index 9c98b50..1934d57 100644 --- a/posts-bridge/includes/class-rest-remote-posts-controller.php +++ b/posts-bridge/includes/class-rest-remote-posts-controller.php @@ -181,9 +181,9 @@ private function filter_prepared_post( $prepared_post, $request ) { * Updates post custom fields after REST API inserts based on bridge custom fields and * maps featured_media custom fields to the request params. * - * @param WP_Post $post Writed post. - * @param WP_REST_Request $request Current request. - * @param boolean $is_new True if is a newly created post. + * @param \WP_Post $post Writed post. + * @param \WP_REST_Request $request Current request. + * @param boolean $is_new True if is a newly created post. */ private function on_rest_insert( $post, $request, $is_new ) { if ( ! $this->is_own_route( $request ) ) { @@ -246,9 +246,9 @@ private function on_rest_insert( $post, $request, $is_new ) { * Sanitize and validates request params before rest dispatches to prevent schema conflicts and uncompleted * payloads. * - * @param mixed $result Response to replace the requested version with. - * @param WP_REST_Server $server Server instance. - * @param WP_REST_Request $request Request instance. + * @param mixed $result Response to replace the requested version with. + * @param \WP_REST_Server $server Server instance. + * @param \WP_REST_Request $request Request instance. * * @return mixed Unaltered result. */ diff --git a/posts-bridge/includes/class-rest-settings-controller.php b/posts-bridge/includes/class-rest-settings-controller.php index 5ff1e27..1e9a5d8 100644 --- a/posts-bridge/includes/class-rest-settings-controller.php +++ b/posts-bridge/includes/class-rest-settings-controller.php @@ -272,7 +272,7 @@ private static function get_remote_cpts() { /** * Callback for GET requests to the post_types endpoint. * - * @param REST_Request $request Request instance. + * @param \WP_REST_Request $request Request instance. * * @return array|WP_Error Post type data. */ @@ -295,55 +295,19 @@ private static function get_post_type( $request ) { /** * Callback for the GET request to the post meta endpoint. * - * @param REST_Request $request Request object. + * @param \WP_REST_Request $request Request object. * * @return array|WP_Error */ private static function get_post_type_meta( $request ) { $key = sanitize_key( $request['name'] ); - - global $wp_meta_keys; - $meta = $wp_meta_keys['post'][ $key ] ?? array(); - - $custom_fields = array(); - foreach ( $meta as $name => $defn ) { - $custom_fields[] = array( - 'name' => $name, - 'schema' => array( - 'type' => $defn['type'], - 'default' => $defn['default'] ?? '', - ), - ); - } - - global $wpdb; - - // phpcs:disable WordPress.DB.DirectDatabaseQuery - $result = $wpdb->get_results( - $wpdb->prepare( - "SELECT DISTINCT pm.meta_key FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE p.post_type = %s", - $key, - ), - ARRAY_A, - ); - // phpcs:enable - - foreach ( $result as $record ) { - if ( ! isset( $meta[ $record['meta_key'] ] ) ) { - $custom_fields[] = array( - 'name' => $record['meta_key'], - 'schema' => array( 'type' => 'string' ), - ); - } - } - - return $custom_fields; + return Remote_CPT::custom_fields( $key ); } /** * Callback for POST requests to the post types endpoint. * - * @param REST_Request $request Request instance. + * @param \WP_REST_Request $request Request instance. * * @return array|WP_Error Registration result. */ @@ -377,7 +341,7 @@ private static function post_post_type( $request ) { /** * Callback for DELETE requests to the post types endpoint. * - * @param REST_Request $request Request instance. + * @param \WP_REST_Request $request Request instance. * * @return array|WP_Error Removal result. */ @@ -402,8 +366,8 @@ private static function delete_post_type( $request ) { /** * Performs a request validation and sanitization * - * @param string $addon Target addon name. - * @param WP_REST_Request $request Request instance. + * @param string $addon Target addon name. + * @param \WP_REST_Request $request Request instance. * * @return array{0:Addon, 1:string}|WP_Error */ @@ -460,8 +424,8 @@ private static function prepare_addon_backend_request_handler( /** * Callback to the backend ping endpoint. * - * @param string $addon Addon name. - * @param WP_REST_Request $request Request object. + * @param string $addon Addon name. + * @param \WP_REST_Request $request Request object. * * @return array|WP_Error */ @@ -502,8 +466,8 @@ private static function ping_backend( $addon, $request ) { /** * Backend endpoints route callback. * - * @param string $addon Addon name. - * @param WP_REST_Request $request Request object. + * @param string $addon Addon name. + * @param \WP_REST_Request $request Request object. * * @return array|WP_Error */ @@ -544,8 +508,8 @@ private static function get_backend_endpoints( $addon, $request ) { /** * Backend endpoint schema route callback. * - * @param string $addon Addon name. - * @param WP_REST_Request $request Request object. + * @param string $addon Addon name. + * @param \WP_REST_Request $request Request object. * * @return array|WP_Error */ diff --git a/src/components/General/index.jsx b/src/components/General/index.jsx index 656b13c..34ab621 100644 --- a/src/components/General/index.jsx +++ b/src/components/General/index.jsx @@ -32,8 +32,8 @@ export default function GeneralSettings() { return ( <> update({ synchronize })} + data={synchronize} + setData={(synchronize) => update({ synchronize })} /> diff --git a/src/components/Synchronize/index.jsx b/src/components/Synchronize/index.jsx index f813bc9..67f2b2f 100644 --- a/src/components/Synchronize/index.jsx +++ b/src/components/Synchronize/index.jsx @@ -39,15 +39,15 @@ const recurrenceOptions = [ }, ]; -export default function Synchronize({ synchronize, setSynchronize }) { +export default function Synchronize({ data, setData }) { const [loading] = useLoading(); const [error] = useError(); - const { enabled, recurrence } = synchronize; + const { enabled, recurrence } = data; const sync = useAjaxSync(); - const update = (field) => setSynchronize({ ...synchronize, ...field }); + const update = (field) => setData({ ...data, ...field }); return ( <>