get_admin_url( ( $this->view ? '&post_status=' . $this->view : '' ) . $params ); } /** * Redirects users from ACF 4.0 admin page. * * @date 17/9/18 * @since 5.7.6 * * @param void * @return void */ public function handle_redirection() { if ( isset( $_GET['post_type'] ) && $_GET['post_type'] === 'acf' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended wp_redirect( $this->get_admin_url() ); exit; } } /** * Constructor for the Field Groups admin page. * * @date 21/07/2014 * @since 5.0.0 * * @param void * @return void */ public function current_screen() { // Bail early if not Field Groups admin page. if ( ! acf_is_screen( 'edit-acf-field-group' ) ) { return; } // Get the current view. $this->view = isset( $_GET['post_status'] ) ? sanitize_text_field( $_GET['post_status'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Setup and check for custom actions.. $this->setup_sync(); $this->check_sync(); $this->check_duplicate(); $this->check_activate(); $this->check_deactivate(); // Modify publish post status text and order. global $wp_post_statuses; $wp_post_statuses['publish']->label_count = _n_noop( 'Active (%s)', 'Active (%s)', 'acf' ); $wp_post_statuses['trash'] = acf_extract_var( $wp_post_statuses, 'trash' ); // Add hooks. add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); add_action( 'admin_body_class', array( $this, 'admin_body_class' ) ); add_filter( 'views_edit-acf-field-group', array( $this, 'admin_table_views' ), 10, 1 ); add_filter( 'manage_acf-field-group_posts_columns', array( $this, 'admin_table_columns' ), 10, 1 ); add_action( 'manage_acf-field-group_posts_custom_column', array( $this, 'admin_table_columns_html' ), 10, 2 ); add_filter( 'display_post_states', array( $this, 'display_post_states' ), 10, 2 ); add_filter( 'bulk_actions-edit-acf-field-group', array( $this, 'admin_table_bulk_actions' ), 10, 1 ); add_action( 'admin_footer', array( $this, 'admin_footer' ), 1 ); if ( $this->view !== 'trash' ) { add_filter( 'page_row_actions', array( $this, 'page_row_actions' ), 10, 2 ); } // Add hooks for "sync" view. if ( $this->view === 'sync' ) { add_action( 'admin_footer', array( $this, 'admin_footer__sync' ), 1 ); } } /** * Sets up the field groups ready for sync. * * @date 17/4/20 * @since 5.9.0 * * @param void * @return void */ public function setup_sync() { // Review local json field groups. if ( acf_get_local_json_files() ) { // Get all groups in a single cached query to check if sync is available. $all_field_groups = acf_get_field_groups(); foreach ( $all_field_groups as $field_group ) { // Extract vars. $local = acf_maybe_get( $field_group, 'local' ); $modified = acf_maybe_get( $field_group, 'modified' ); $private = acf_maybe_get( $field_group, 'private' ); // Ignore if is private. if ( $private ) { continue; // Ignore not local "json". } elseif ( $local !== 'json' ) { continue; // Append to sync if not yet in database. } elseif ( ! $field_group['ID'] ) { $this->sync[ $field_group['key'] ] = $field_group; // Append to sync if "json" modified time is newer than database. } elseif ( $modified && $modified > get_post_modified_time( 'U', true, $field_group['ID'] ) ) { $this->sync[ $field_group['key'] ] = $field_group; } } } } /** * Enqueues admin scripts. * * @date 18/4/20 * @since 5.9.0 * * @param void * @return void */ public function admin_enqueue_scripts() { acf_enqueue_script( 'acf' ); // Localize text. acf_localize_text( array( 'Review local JSON changes' => __( 'Review local JSON changes', 'acf' ), 'Loading diff' => __( 'Loading diff', 'acf' ), 'Sync changes' => __( 'Sync changes', 'acf' ), ) ); } /** * Modifies the admin body class. * * @date 18/4/20 * @since 5.9.0 * * @param string $classes Space-separated list of CSS classes. * @return string */ public function admin_body_class( $classes ) { $classes .= ' acf-admin-field-groups'; if ( $this->view ) { $classes .= " view-{$this->view}"; } return $classes; } /** * returns the disabled post state HTML. * * @date 17/4/20 * @since 5.9.0 * * @param void * @return string */ public function get_disabled_post_state() { return ' ' . _x( 'Inactive', 'post status', 'acf' ); } /** * Adds the "disabled" post state for the admin table title. * * @date 1/4/20 * @since 5.9.0 * * @param array $post_states An array of post display states. * @param WP_Post $post The current post object. * @return array */ public function display_post_states( $post_states, $post ) { if ( $post->post_status === 'acf-disabled' ) { $post_states['acf-disabled'] = $this->get_disabled_post_state(); } return $post_states; } /** * Get the HTML for when a file is not found. * * @since 6.0.0 * * @return string html. */ public function get_not_found_html() { ob_start(); acf_get_view( 'field-groups-empty' ); return ob_get_clean(); } /** * Customizes the admin table columns. * * @date 1/4/20 * @since 5.9.0 * * @param array $_columns The columns array. * @return array */ public function admin_table_columns( $_columns ) { // Set the "no found" label to be our custom HTML for no results. global $wp_post_types; $this->not_found_label = $wp_post_types['acf-field-group']->labels->not_found; $wp_post_types['acf-field-group']->labels->not_found = $this->get_not_found_html(); $columns = array( 'cb' => $_columns['cb'], 'title' => $_columns['title'], 'acf-description' => __( 'Description', 'acf' ), 'acf-key' => __( 'Key', 'acf' ), 'acf-location' => __( 'Location', 'acf' ), 'acf-count' => __( 'Fields', 'acf' ), ); if ( acf_get_local_json_files() ) { $columns['acf-json'] = __( 'Local JSON', 'acf' ); } return $columns; } /** * Renders the admin table column HTML * * @date 1/4/20 * @since 5.9.0 * * @param string $column_name The name of the column to display. * @param int $post_id The current post ID. * @return void */ public function admin_table_columns_html( $column_name, $post_id ) { $field_group = acf_get_field_group( $post_id ); if ( $field_group ) { $this->render_admin_table_column( $column_name, $field_group ); } } /** * Renders a specific admin table column. * * @date 17/4/20 * @since 5.9.0 * * @param string $column_name The name of the column to display. * @param array $field_group The field group. * @return void */ public function render_admin_table_column( $column_name, $field_group ) { switch ( $column_name ) { // Key. case 'acf-key': echo ''; echo esc_html( $field_group['key'] ); break; // Description. case 'acf-description': if ( $field_group['description'] ) { echo '' . acf_esc_html( $field_group['description'] ) . ''; } break; // Location. case 'acf-location': $this->render_admin_table_column_locations( $field_group ); break; // Count. case 'acf-count': echo esc_html( acf_get_field_count( $field_group ) ); break; // Local JSON. case 'acf-json': $this->render_admin_table_column_local_status( $field_group ); break; } } /** * Displays a visual representation of the field group's locations. * * @date 1/4/20 * @since 5.9.0 * * @param array $field_group The field group. * @return void */ public function render_admin_table_column_locations( $field_group ) { $objects = array(); // Loop over location rules and determine connected object types. if ( $field_group['location'] ) { foreach ( $field_group['location'] as $i => $rules ) { // Determine object types for each rule. foreach ( $rules as $j => $rule ) { // Get location type and subtype for the current rule. $location = acf_get_location_rule( $rule['param'] ); $location_object_type = ''; $location_object_subtype = ''; if ( $location ) { $location_object_type = $location->get_object_type( $rule ); $location_object_subtype = $location->get_object_subtype( $rule ); } $rules[ $j ]['object_type'] = $location_object_type; $rules[ $j ]['object_subtype'] = $location_object_subtype; } // Now that each $rule conains object type data... $object_types = array_column( $rules, 'object_type' ); $object_types = array_filter( $object_types ); $object_types = array_values( $object_types ); if ( $object_types ) { $object_type = $object_types[0]; } else { continue; } $object_subtypes = array_column( $rules, 'object_subtype' ); $object_subtypes = array_filter( $object_subtypes ); $object_subtypes = array_values( $object_subtypes ); $object_subtypes = array_map( 'acf_array', $object_subtypes ); if ( count( $object_subtypes ) > 1 ) { $object_subtypes = call_user_func_array( 'array_intersect', $object_subtypes ); $object_subtypes = array_values( $object_subtypes ); } elseif ( $object_subtypes ) { $object_subtypes = $object_subtypes[0]; } else { $object_subtypes = array( '' ); } // Append to objects. foreach ( $object_subtypes as $object_subtype ) { $object = acf_get_object_type( $object_type, $object_subtype ); if ( $object ) { $objects[ $object->name ] = $object; } } } } // Reset keys. $objects = array_values( $objects ); // Display. $html = ''; if ( $objects ) { $limit = 3; $total = count( $objects ); // Icon. $html .= ''; // Labels. $labels = array_column( $objects, 'label' ); $labels = array_slice( $labels, 0, 3 ); $html .= implode( ', ', $labels ); // More. if ( $total > $limit ) { $html .= ', ...'; } } else { $html = ' ' . __( 'Various', 'acf' ); } // Filter. echo acf_esc_html( $html ); } /** * Returns a human readable file location. * * @date 17/4/20 * @since 5.9.0 * * @param string $file The full file path. * @return string */ public function get_human_readable_file_location( $file ) { // Generate friendly file path. $theme_path = get_stylesheet_directory(); if ( strpos( $file, $theme_path ) !== false ) { $rel_file = str_replace( $theme_path, '', $file ); $located = sprintf( __( 'Located in theme: %s', 'acf' ), $rel_file ); } elseif ( strpos( $file, WP_PLUGIN_DIR ) !== false ) { $rel_file = str_replace( WP_PLUGIN_DIR, '', $file ); $located = sprintf( __( 'Located in plugin: %s', 'acf' ), $rel_file ); } else { $rel_file = str_replace( ABSPATH, '', $file ); $located = sprintf( __( 'Located in: %s', 'acf' ), $rel_file ); } return $located; } /** * Displays the local JSON status of a field group. * * @date 14/4/20 * @since 5.9.0 * * @param type $var Description. Default. * @return type Description. */ public function render_admin_table_column_local_status( $field_group ) { $json = acf_get_local_json_files(); if ( isset( $json[ $field_group['key'] ] ) ) { $file = $json[ $field_group['key'] ]; if ( isset( $this->sync[ $field_group['key'] ] ) ) { $url = $this->get_admin_url( '&acfsync=' . $field_group['key'] . '&_wpnonce=' . wp_create_nonce( 'bulk-posts' ) ); echo '' . __( 'Sync available', 'acf' ) . ''; if ( $field_group['ID'] ) { echo '
'; } else { echo ''; } } else { echo __( 'Saved', 'acf' ); } } else { echo '' . __( 'Awaiting save', 'acf' ) . ''; } } /** * Customizes the page row actions visible on hover. * * @date 14/4/20 * @since 5.9.0 * * @param array $actions The array of actions HTML. * @param WP_Post $post The post. * @return array */ public function page_row_actions( $actions, $post ) { // Remove "Quick Edit" action. unset( $actions['inline'], $actions['inline hide-if-no-js'] ); // Append "Duplicate" action. $duplicate_action_url = $this->get_admin_url( '&acfduplicate=' . $post->ID . '&_wpnonce=' . wp_create_nonce( 'bulk-posts' ) ); $actions['acfduplicate'] = '' . __( 'Duplicate', 'acf' ) . ''; // Append the "Activate" or "Deactivate" actions. if ( 'acf-disabled' === $post->post_status ) { $activate_deactivate_action = 'acfactivate'; $activate_action_url = $this->get_admin_url( '&acfactivate=' . $post->ID . '&_wpnonce=' . wp_create_nonce( 'bulk-posts' ) ); $actions['acfactivate'] = '' . __( 'Activate', 'acf' ) . ''; } else { $activate_deactivate_action = 'acfdeactivate'; $deactivate_action_url = $this->get_admin_url( '&acfdeactivate=' . $post->ID . '&_wpnonce=' . wp_create_nonce( 'bulk-posts' ) ); $actions['acfdeactivate'] = '' . __( 'Deactivate', 'acf' ) . ''; } // Return actions in custom order. $order = array( 'edit', 'acfduplicate', $activate_deactivate_action, 'trash' ); return array_merge( array_flip( $order ), $actions ); } /** * Modifies the admin table bulk actions dropdown. * * @date 15/4/20 * @since 5.9.0 * * @param array $actions The actions array. * @return array */ public function admin_table_bulk_actions( $actions ) { if ( ! in_array( $this->view, array( 'sync', 'trash' ), true ) ) { $actions['acfduplicate'] = __( 'Duplicate', 'acf' ); $actions['acfactivate'] = __( 'Activate', 'acf' ); $actions['acfdeactivate'] = __( 'Deactivate', 'acf' ); } if ( $this->sync ) { if ( $this->view === 'sync' ) { $actions = array(); } $actions['acfsync'] = __( 'Sync changes', 'acf' ); } return $actions; } /** * Checks for the custom "Activate" bulk action. * * @since 6.0 */ public function check_activate() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used for redirect notice. // Display notice on success redirect. if ( isset( $_GET['acfactivatecomplete'] ) ) { $ids = array_map( 'intval', explode( ',', $_GET['acfactivatecomplete'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with intval(). // phpcs:enable WordPress.Security.NonceVerification.Recommended // Generate text. $text = sprintf( _n( 'Field group activated.', '%s field groups activated.', count( $ids ), 'acf' ), count( $ids ) ); // Append links to text. $links = array(); foreach ( $ids as $id ) { $links[] = '' . get_the_title( $id ) . ''; } $text .= ' ' . implode( ', ', $links ); // Add notice. acf_add_admin_notice( $text, 'success' ); return; } // Find items to activate. $ids = array(); if ( isset( $_GET['acfactivate'] ) ) { $ids[] = intval( $_GET['acfactivate'] ); } elseif ( isset( $_GET['post'], $_GET['action2'] ) && $_GET['action2'] === 'acfactivate' ) { $ids = array_map( 'intval', $_GET['post'] ); } if ( $ids ) { check_admin_referer( 'bulk-posts' ); // Activate the field groups and return an array of IDs that were activated. $new_ids = array(); foreach ( $ids as $id ) { if ( acf_update_field_group_active_status( $id ) ) { $new_ids[] = $id; } } wp_redirect( $this->get_admin_url( '&acfactivatecomplete=' . implode( ',', $new_ids ) ) ); exit; } } /** * Checks for the custom "Deactivate" bulk action. * * @since 6.0 */ public function check_deactivate() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used for redirect notice. // Display notice on success redirect. if ( isset( $_GET['acfdeactivatecomplete'] ) ) { $ids = array_map( 'intval', explode( ',', $_GET['acfdeactivatecomplete'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with intval(). // phpcs:enable WordPress.Security.NonceVerification.Recommended // Generate text. $text = sprintf( _n( 'Field group deactivated.', '%s field groups deactivated.', count( $ids ), 'acf' ), count( $ids ) ); // Append links to text. $links = array(); foreach ( $ids as $id ) { $links[] = '' . get_the_title( $id ) . ''; } $text .= ' ' . implode( ', ', $links ); // Add notice. acf_add_admin_notice( $text, 'success' ); return; } // Find items to activate. $ids = array(); if ( isset( $_GET['acfdeactivate'] ) ) { $ids[] = intval( $_GET['acfdeactivate'] ); } elseif ( isset( $_GET['post'], $_GET['action2'] ) && $_GET['action2'] === 'acfdeactivate' ) { $ids = array_map( 'intval', $_GET['post'] ); } if ( $ids ) { check_admin_referer( 'bulk-posts' ); // Activate the field groups and return an array of IDs. $new_ids = array(); foreach ( $ids as $id ) { if ( acf_update_field_group_active_status( $id, false ) ) { $new_ids[] = $id; } } wp_redirect( $this->get_admin_url( '&acfdeactivatecomplete=' . implode( ',', $new_ids ) ) ); exit; } } /** * Checks for the custom "duplicate" action. * * @date 15/4/20 * @since 5.9.0 * * @param void * @return void */ public function check_duplicate() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used for redirect notice. // Display notice on success redirect. if ( isset( $_GET['acfduplicatecomplete'] ) ) { $ids = array_map( 'intval', explode( ',', $_GET['acfduplicatecomplete'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with intval(). // phpcs:enable WordPress.Security.NonceVerification.Recommended // Generate text. $text = sprintf( _n( 'Field group duplicated.', '%s field groups duplicated.', count( $ids ), 'acf' ), count( $ids ) ); // Append links to text. $links = array(); foreach ( $ids as $id ) { $links[] = '' . get_the_title( $id ) . ''; } $text .= ' ' . implode( ', ', $links ); // Add notice. acf_add_admin_notice( $text, 'success' ); return; } // Find items to duplicate. $ids = array(); if ( isset( $_GET['acfduplicate'] ) ) { $ids[] = intval( $_GET['acfduplicate'] ); } elseif ( isset( $_GET['post'], $_GET['action2'] ) && $_GET['action2'] === 'acfduplicate' ) { $ids = array_map( 'intval', $_GET['post'] ); } if ( $ids ) { check_admin_referer( 'bulk-posts' ); // Duplicate field groups and generate array of new IDs. $new_ids = array(); foreach ( $ids as $id ) { $field_group = acf_duplicate_field_group( $id ); $new_ids[] = $field_group['ID']; } // Redirect. wp_redirect( $this->get_admin_url( '&acfduplicatecomplete=' . implode( ',', $new_ids ) ) ); exit; } } /** * Checks for the custom "acfsync" action. * * @date 15/4/20 * @since 5.9.0 * * @param void * @return void */ public function check_sync() { // phpcs:disable WordPress.Security.NonceVerification.Recommended // Display notice on success redirect. if ( isset( $_GET['acfsynccomplete'] ) ) { $ids = array_map( 'intval', explode( ',', $_GET['acfsynccomplete'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with intval(). // phpcs:enable WordPress.Security.NonceVerification.Recommended // Generate text. $text = sprintf( _n( 'Field group synchronised.', '%s field groups synchronised.', count( $ids ), 'acf' ), count( $ids ) ); // Append links to text. $links = array(); foreach ( $ids as $id ) { $links[] = '' . get_the_title( $id ) . ''; } $text .= ' ' . implode( ', ', $links ); // Add notice. acf_add_admin_notice( $text, 'success' ); return; } // Find items to sync. $keys = array(); if ( isset( $_GET['acfsync'] ) ) { $keys[] = sanitize_text_field( $_GET['acfsync'] ); } elseif ( isset( $_GET['post'], $_GET['action2'] ) && $_GET['action2'] === 'acfsync' ) { $keys = array_map( 'sanitize_text_field', $_GET['post'] ); } if ( $keys && $this->sync ) { check_admin_referer( 'bulk-posts' ); // Disabled "Local JSON" controller to prevent the .json file from being modified during import. acf_update_setting( 'json', false ); // Sync field groups and generate array of new IDs. $files = acf_get_local_json_files(); $new_ids = array(); foreach ( $this->sync as $key => $field_group ) { if ( $field_group['key'] && in_array( $field_group['key'], $keys ) ) { // Import. } elseif ( $field_group['ID'] && in_array( $field_group['ID'], $keys ) ) { // Import. } else { // Ignore. continue; } $local_field_group = json_decode( file_get_contents( $files[ $key ] ), true ); $local_field_group['ID'] = $field_group['ID']; $result = acf_import_field_group( $local_field_group ); $new_ids[] = $result['ID']; } // Redirect. wp_redirect( $this->get_current_admin_url( '&acfsynccomplete=' . implode( ',', $new_ids ) ) ); exit; } } /** * Customizes the admin table subnav. * * @date 17/4/20 * @since 5.9.0 * * @param array $views The available views. * @return array */ public function admin_table_views( $views ) { global $wp_list_table, $wp_query; // Count items. $count = count( $this->sync ); // Append "sync" link to subnav. if ( $count ) { $views['sync'] = sprintf( '%s (%s)', ( $this->view === 'sync' ? 'class="current"' : '' ), esc_url( $this->get_admin_url( '&post_status=sync' ) ), esc_html( __( 'Sync available', 'acf' ) ), $count ); } // Modify table pagination args to match JSON data. if ( $this->view === 'sync' ) { $wp_list_table->set_pagination_args( array( 'total_items' => $count, 'total_pages' => 1, 'per_page' => $count, ) ); $wp_query->post_count = 1; // At least one post is needed to render bulk drop-down. } return $views; } /** * Prints scripts into the admin footer. * * @date 20/4/20 * @since 5.9.0 * * @param void * @return void */ function admin_footer() { ?> get_columns(); $hidden = get_hidden_columns( $wp_list_table->screen ); ?>