admin-field-groups.php 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. <?php
  2. if ( ! defined( 'ABSPATH' ) ) {
  3. exit; // Exit if accessed directly
  4. }
  5. if ( ! class_exists( 'ACF_Admin_Field_Groups' ) ) :
  6. class ACF_Admin_Field_Groups {
  7. /**
  8. * Array of field groups availbale for sync.
  9. *
  10. * @since 5.9.0
  11. * @var array
  12. */
  13. public $sync = array();
  14. /**
  15. * The current view (post_status).
  16. *
  17. * @since 5.9.0
  18. * @var string
  19. */
  20. public $view = '';
  21. /**
  22. * Constructor.
  23. *
  24. * @date 5/03/2014
  25. * @since 5.0.0
  26. *
  27. * @param void
  28. * @return void
  29. */
  30. public function __construct() {
  31. // Add hooks.
  32. add_action( 'load-edit.php', array( $this, 'handle_redirection' ) );
  33. add_action( 'current_screen', array( $this, 'current_screen' ) );
  34. // Handle post status change events.
  35. add_action( 'trashed_post', array( $this, 'trashed_post' ) );
  36. add_action( 'untrashed_post', array( $this, 'untrashed_post' ) );
  37. add_action( 'deleted_post', array( $this, 'deleted_post' ) );
  38. }
  39. /**
  40. * Returns the Field Groups admin URL.
  41. *
  42. * @date 27/3/20
  43. * @since 5.9.0
  44. *
  45. * @param string $params Extra URL params.
  46. * @return string
  47. */
  48. public function get_admin_url( $params = '' ) {
  49. return admin_url( "edit.php?post_type=acf-field-group{$params}" );
  50. }
  51. /**
  52. * Returns the Field Groups admin URL taking into account the current view.
  53. *
  54. * @date 27/3/20
  55. * @since 5.9.0
  56. *
  57. * @param string $params Extra URL params.
  58. * @return string
  59. */
  60. public function get_current_admin_url( $params = '' ) {
  61. return $this->get_admin_url( ( $this->view ? '&post_status=' . $this->view : '' ) . $params );
  62. }
  63. /**
  64. * Redirects users from ACF 4.0 admin page.
  65. *
  66. * @date 17/9/18
  67. * @since 5.7.6
  68. *
  69. * @param void
  70. * @return void
  71. */
  72. public function handle_redirection() {
  73. if ( isset( $_GET['post_type'] ) && $_GET['post_type'] === 'acf' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  74. wp_redirect( $this->get_admin_url() );
  75. exit;
  76. }
  77. }
  78. /**
  79. * Constructor for the Field Groups admin page.
  80. *
  81. * @date 21/07/2014
  82. * @since 5.0.0
  83. *
  84. * @param void
  85. * @return void
  86. */
  87. public function current_screen() {
  88. // Bail early if not Field Groups admin page.
  89. if ( ! acf_is_screen( 'edit-acf-field-group' ) ) {
  90. return;
  91. }
  92. // Get the current view.
  93. $this->view = isset( $_GET['post_status'] ) ? sanitize_text_field( $_GET['post_status'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  94. // Setup and check for custom actions..
  95. $this->setup_sync();
  96. $this->check_sync();
  97. $this->check_duplicate();
  98. $this->check_activate();
  99. $this->check_deactivate();
  100. // Modify publish post status text and order.
  101. global $wp_post_statuses;
  102. $wp_post_statuses['publish']->label_count = _n_noop( 'Active <span class="count">(%s)</span>', 'Active <span class="count">(%s)</span>', 'acf' );
  103. $wp_post_statuses['trash'] = acf_extract_var( $wp_post_statuses, 'trash' );
  104. // Add hooks.
  105. add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
  106. add_action( 'admin_body_class', array( $this, 'admin_body_class' ) );
  107. add_filter( 'views_edit-acf-field-group', array( $this, 'admin_table_views' ), 10, 1 );
  108. add_filter( 'manage_acf-field-group_posts_columns', array( $this, 'admin_table_columns' ), 10, 1 );
  109. add_action( 'manage_acf-field-group_posts_custom_column', array( $this, 'admin_table_columns_html' ), 10, 2 );
  110. add_filter( 'display_post_states', array( $this, 'display_post_states' ), 10, 2 );
  111. add_filter( 'bulk_actions-edit-acf-field-group', array( $this, 'admin_table_bulk_actions' ), 10, 1 );
  112. add_action( 'admin_footer', array( $this, 'admin_footer' ), 1 );
  113. if ( $this->view !== 'trash' ) {
  114. add_filter( 'page_row_actions', array( $this, 'page_row_actions' ), 10, 2 );
  115. }
  116. // Add hooks for "sync" view.
  117. if ( $this->view === 'sync' ) {
  118. add_action( 'admin_footer', array( $this, 'admin_footer__sync' ), 1 );
  119. }
  120. }
  121. /**
  122. * Sets up the field groups ready for sync.
  123. *
  124. * @date 17/4/20
  125. * @since 5.9.0
  126. *
  127. * @param void
  128. * @return void
  129. */
  130. public function setup_sync() {
  131. // Review local json field groups.
  132. if ( acf_get_local_json_files() ) {
  133. // Get all groups in a single cached query to check if sync is available.
  134. $all_field_groups = acf_get_field_groups();
  135. foreach ( $all_field_groups as $field_group ) {
  136. // Extract vars.
  137. $local = acf_maybe_get( $field_group, 'local' );
  138. $modified = acf_maybe_get( $field_group, 'modified' );
  139. $private = acf_maybe_get( $field_group, 'private' );
  140. // Ignore if is private.
  141. if ( $private ) {
  142. continue;
  143. // Ignore not local "json".
  144. } elseif ( $local !== 'json' ) {
  145. continue;
  146. // Append to sync if not yet in database.
  147. } elseif ( ! $field_group['ID'] ) {
  148. $this->sync[ $field_group['key'] ] = $field_group;
  149. // Append to sync if "json" modified time is newer than database.
  150. } elseif ( $modified && $modified > get_post_modified_time( 'U', true, $field_group['ID'] ) ) {
  151. $this->sync[ $field_group['key'] ] = $field_group;
  152. }
  153. }
  154. }
  155. }
  156. /**
  157. * Enqueues admin scripts.
  158. *
  159. * @date 18/4/20
  160. * @since 5.9.0
  161. *
  162. * @param void
  163. * @return void
  164. */
  165. public function admin_enqueue_scripts() {
  166. acf_enqueue_script( 'acf' );
  167. // Localize text.
  168. acf_localize_text(
  169. array(
  170. 'Review local JSON changes' => __( 'Review local JSON changes', 'acf' ),
  171. 'Loading diff' => __( 'Loading diff', 'acf' ),
  172. 'Sync changes' => __( 'Sync changes', 'acf' ),
  173. )
  174. );
  175. }
  176. /**
  177. * Modifies the admin body class.
  178. *
  179. * @date 18/4/20
  180. * @since 5.9.0
  181. *
  182. * @param string $classes Space-separated list of CSS classes.
  183. * @return string
  184. */
  185. public function admin_body_class( $classes ) {
  186. $classes .= ' acf-admin-field-groups';
  187. if ( $this->view ) {
  188. $classes .= " view-{$this->view}";
  189. }
  190. return $classes;
  191. }
  192. /**
  193. * returns the disabled post state HTML.
  194. *
  195. * @date 17/4/20
  196. * @since 5.9.0
  197. *
  198. * @param void
  199. * @return string
  200. */
  201. public function get_disabled_post_state() {
  202. return '<span class="dashicons dashicons-hidden"></span> ' . _x( 'Inactive', 'post status', 'acf' );
  203. }
  204. /**
  205. * Adds the "disabled" post state for the admin table title.
  206. *
  207. * @date 1/4/20
  208. * @since 5.9.0
  209. *
  210. * @param array $post_states An array of post display states.
  211. * @param WP_Post $post The current post object.
  212. * @return array
  213. */
  214. public function display_post_states( $post_states, $post ) {
  215. if ( $post->post_status === 'acf-disabled' ) {
  216. $post_states['acf-disabled'] = $this->get_disabled_post_state();
  217. }
  218. return $post_states;
  219. }
  220. /**
  221. * Get the HTML for when a file is not found.
  222. *
  223. * @since 6.0.0
  224. *
  225. * @return string html.
  226. */
  227. public function get_not_found_html() {
  228. ob_start();
  229. acf_get_view( 'field-groups-empty' );
  230. return ob_get_clean();
  231. }
  232. /**
  233. * Customizes the admin table columns.
  234. *
  235. * @date 1/4/20
  236. * @since 5.9.0
  237. *
  238. * @param array $_columns The columns array.
  239. * @return array
  240. */
  241. public function admin_table_columns( $_columns ) {
  242. // Set the "no found" label to be our custom HTML for no results.
  243. global $wp_post_types;
  244. $this->not_found_label = $wp_post_types['acf-field-group']->labels->not_found;
  245. $wp_post_types['acf-field-group']->labels->not_found = $this->get_not_found_html();
  246. $columns = array(
  247. 'cb' => $_columns['cb'],
  248. 'title' => $_columns['title'],
  249. 'acf-description' => __( 'Description', 'acf' ),
  250. 'acf-key' => __( 'Key', 'acf' ),
  251. 'acf-location' => __( 'Location', 'acf' ),
  252. 'acf-count' => __( 'Fields', 'acf' ),
  253. );
  254. if ( acf_get_local_json_files() ) {
  255. $columns['acf-json'] = __( 'Local JSON', 'acf' );
  256. }
  257. return $columns;
  258. }
  259. /**
  260. * Renders the admin table column HTML
  261. *
  262. * @date 1/4/20
  263. * @since 5.9.0
  264. *
  265. * @param string $column_name The name of the column to display.
  266. * @param int $post_id The current post ID.
  267. * @return void
  268. */
  269. public function admin_table_columns_html( $column_name, $post_id ) {
  270. $field_group = acf_get_field_group( $post_id );
  271. if ( $field_group ) {
  272. $this->render_admin_table_column( $column_name, $field_group );
  273. }
  274. }
  275. /**
  276. * Renders a specific admin table column.
  277. *
  278. * @date 17/4/20
  279. * @since 5.9.0
  280. *
  281. * @param string $column_name The name of the column to display.
  282. * @param array $field_group The field group.
  283. * @return void
  284. */
  285. public function render_admin_table_column( $column_name, $field_group ) {
  286. switch ( $column_name ) {
  287. // Key.
  288. case 'acf-key':
  289. echo '<i class="acf-icon acf-icon-key-solid"></i>';
  290. echo esc_html( $field_group['key'] );
  291. break;
  292. // Description.
  293. case 'acf-description':
  294. if ( $field_group['description'] ) {
  295. echo '<span class="acf-description">' . acf_esc_html( $field_group['description'] ) . '</span>';
  296. }
  297. break;
  298. // Location.
  299. case 'acf-location':
  300. $this->render_admin_table_column_locations( $field_group );
  301. break;
  302. // Count.
  303. case 'acf-count':
  304. echo esc_html( acf_get_field_count( $field_group ) );
  305. break;
  306. // Local JSON.
  307. case 'acf-json':
  308. $this->render_admin_table_column_local_status( $field_group );
  309. break;
  310. }
  311. }
  312. /**
  313. * Displays a visual representation of the field group's locations.
  314. *
  315. * @date 1/4/20
  316. * @since 5.9.0
  317. *
  318. * @param array $field_group The field group.
  319. * @return void
  320. */
  321. public function render_admin_table_column_locations( $field_group ) {
  322. $objects = array();
  323. // Loop over location rules and determine connected object types.
  324. if ( $field_group['location'] ) {
  325. foreach ( $field_group['location'] as $i => $rules ) {
  326. // Determine object types for each rule.
  327. foreach ( $rules as $j => $rule ) {
  328. // Get location type and subtype for the current rule.
  329. $location = acf_get_location_rule( $rule['param'] );
  330. $location_object_type = '';
  331. $location_object_subtype = '';
  332. if ( $location ) {
  333. $location_object_type = $location->get_object_type( $rule );
  334. $location_object_subtype = $location->get_object_subtype( $rule );
  335. }
  336. $rules[ $j ]['object_type'] = $location_object_type;
  337. $rules[ $j ]['object_subtype'] = $location_object_subtype;
  338. }
  339. // Now that each $rule conains object type data...
  340. $object_types = array_column( $rules, 'object_type' );
  341. $object_types = array_filter( $object_types );
  342. $object_types = array_values( $object_types );
  343. if ( $object_types ) {
  344. $object_type = $object_types[0];
  345. } else {
  346. continue;
  347. }
  348. $object_subtypes = array_column( $rules, 'object_subtype' );
  349. $object_subtypes = array_filter( $object_subtypes );
  350. $object_subtypes = array_values( $object_subtypes );
  351. $object_subtypes = array_map( 'acf_array', $object_subtypes );
  352. if ( count( $object_subtypes ) > 1 ) {
  353. $object_subtypes = call_user_func_array( 'array_intersect', $object_subtypes );
  354. $object_subtypes = array_values( $object_subtypes );
  355. } elseif ( $object_subtypes ) {
  356. $object_subtypes = $object_subtypes[0];
  357. } else {
  358. $object_subtypes = array( '' );
  359. }
  360. // Append to objects.
  361. foreach ( $object_subtypes as $object_subtype ) {
  362. $object = acf_get_object_type( $object_type, $object_subtype );
  363. if ( $object ) {
  364. $objects[ $object->name ] = $object;
  365. }
  366. }
  367. }
  368. }
  369. // Reset keys.
  370. $objects = array_values( $objects );
  371. // Display.
  372. $html = '';
  373. if ( $objects ) {
  374. $limit = 3;
  375. $total = count( $objects );
  376. // Icon.
  377. $html .= '<span class="dashicons ' . $objects[0]->icon . ( $total > 1 ? ' acf-multi-dashicon' : '' ) . '"></span>';
  378. // Labels.
  379. $labels = array_column( $objects, 'label' );
  380. $labels = array_slice( $labels, 0, 3 );
  381. $html .= implode( ', ', $labels );
  382. // More.
  383. if ( $total > $limit ) {
  384. $html .= ', ...';
  385. }
  386. } else {
  387. $html = '<span class="dashicons dashicons-businesswoman"></span> ' . __( 'Various', 'acf' );
  388. }
  389. // Filter.
  390. echo acf_esc_html( $html );
  391. }
  392. /**
  393. * Returns a human readable file location.
  394. *
  395. * @date 17/4/20
  396. * @since 5.9.0
  397. *
  398. * @param string $file The full file path.
  399. * @return string
  400. */
  401. public function get_human_readable_file_location( $file ) {
  402. // Generate friendly file path.
  403. $theme_path = get_stylesheet_directory();
  404. if ( strpos( $file, $theme_path ) !== false ) {
  405. $rel_file = str_replace( $theme_path, '', $file );
  406. $located = sprintf( __( 'Located in theme: %s', 'acf' ), $rel_file );
  407. } elseif ( strpos( $file, WP_PLUGIN_DIR ) !== false ) {
  408. $rel_file = str_replace( WP_PLUGIN_DIR, '', $file );
  409. $located = sprintf( __( 'Located in plugin: %s', 'acf' ), $rel_file );
  410. } else {
  411. $rel_file = str_replace( ABSPATH, '', $file );
  412. $located = sprintf( __( 'Located in: %s', 'acf' ), $rel_file );
  413. }
  414. return $located;
  415. }
  416. /**
  417. * Displays the local JSON status of a field group.
  418. *
  419. * @date 14/4/20
  420. * @since 5.9.0
  421. *
  422. * @param type $var Description. Default.
  423. * @return type Description.
  424. */
  425. public function render_admin_table_column_local_status( $field_group ) {
  426. $json = acf_get_local_json_files();
  427. if ( isset( $json[ $field_group['key'] ] ) ) {
  428. $file = $json[ $field_group['key'] ];
  429. if ( isset( $this->sync[ $field_group['key'] ] ) ) {
  430. $url = $this->get_admin_url( '&acfsync=' . $field_group['key'] . '&_wpnonce=' . wp_create_nonce( 'bulk-posts' ) );
  431. echo '<strong>' . __( 'Sync available', 'acf' ) . '</strong>';
  432. if ( $field_group['ID'] ) {
  433. echo '<div class="row-actions">
  434. <span class="sync"><a href="' . esc_url( $url ) . '">' . __( 'Sync', 'acf' ) . '</a> | </span>
  435. <span class="review"><a href="#" data-event="review-sync" data-id="' . esc_attr( $field_group['ID'] ) . '" data-href="' . esc_url( $url ) . '">' . __( 'Review changes', 'acf' ) . '</a></span>
  436. </div>';
  437. } else {
  438. echo '<div class="row-actions">
  439. <span class="sync"><a href="' . esc_url( $url ) . '">' . __( 'Import', 'acf' ) . '</a></span>
  440. </div>';
  441. }
  442. } else {
  443. echo __( 'Saved', 'acf' );
  444. }
  445. } else {
  446. echo '<span class="acf-secondary-text">' . __( 'Awaiting save', 'acf' ) . '</span>';
  447. }
  448. }
  449. /**
  450. * Customizes the page row actions visible on hover.
  451. *
  452. * @date 14/4/20
  453. * @since 5.9.0
  454. *
  455. * @param array $actions The array of actions HTML.
  456. * @param WP_Post $post The post.
  457. * @return array
  458. */
  459. public function page_row_actions( $actions, $post ) {
  460. // Remove "Quick Edit" action.
  461. unset( $actions['inline'], $actions['inline hide-if-no-js'] );
  462. // Append "Duplicate" action.
  463. $duplicate_action_url = $this->get_admin_url( '&acfduplicate=' . $post->ID . '&_wpnonce=' . wp_create_nonce( 'bulk-posts' ) );
  464. $actions['acfduplicate'] = '<a href="' . esc_url( $duplicate_action_url ) . '" aria-label="' . esc_attr__( 'Duplicate this item', 'acf' ) . '">' . __( 'Duplicate', 'acf' ) . '</a>';
  465. // Append the "Activate" or "Deactivate" actions.
  466. if ( 'acf-disabled' === $post->post_status ) {
  467. $activate_deactivate_action = 'acfactivate';
  468. $activate_action_url = $this->get_admin_url( '&acfactivate=' . $post->ID . '&_wpnonce=' . wp_create_nonce( 'bulk-posts' ) );
  469. $actions['acfactivate'] = '<a href="' . esc_url( $activate_action_url ) . '" aria-label="' . esc_attr__( 'Activate this item', 'acf' ) . '">' . __( 'Activate', 'acf' ) . '</a>';
  470. } else {
  471. $activate_deactivate_action = 'acfdeactivate';
  472. $deactivate_action_url = $this->get_admin_url( '&acfdeactivate=' . $post->ID . '&_wpnonce=' . wp_create_nonce( 'bulk-posts' ) );
  473. $actions['acfdeactivate'] = '<a href="' . esc_url( $deactivate_action_url ) . '" aria-label="' . esc_attr__( 'Deactivate this item', 'acf' ) . '">' . __( 'Deactivate', 'acf' ) . '</a>';
  474. }
  475. // Return actions in custom order.
  476. $order = array( 'edit', 'acfduplicate', $activate_deactivate_action, 'trash' );
  477. return array_merge( array_flip( $order ), $actions );
  478. }
  479. /**
  480. * Modifies the admin table bulk actions dropdown.
  481. *
  482. * @date 15/4/20
  483. * @since 5.9.0
  484. *
  485. * @param array $actions The actions array.
  486. * @return array
  487. */
  488. public function admin_table_bulk_actions( $actions ) {
  489. if ( ! in_array( $this->view, array( 'sync', 'trash' ), true ) ) {
  490. $actions['acfduplicate'] = __( 'Duplicate', 'acf' );
  491. $actions['acfactivate'] = __( 'Activate', 'acf' );
  492. $actions['acfdeactivate'] = __( 'Deactivate', 'acf' );
  493. }
  494. if ( $this->sync ) {
  495. if ( $this->view === 'sync' ) {
  496. $actions = array();
  497. }
  498. $actions['acfsync'] = __( 'Sync changes', 'acf' );
  499. }
  500. return $actions;
  501. }
  502. /**
  503. * Checks for the custom "Activate" bulk action.
  504. *
  505. * @since 6.0
  506. */
  507. public function check_activate() {
  508. // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used for redirect notice.
  509. // Display notice on success redirect.
  510. if ( isset( $_GET['acfactivatecomplete'] ) ) {
  511. $ids = array_map( 'intval', explode( ',', $_GET['acfactivatecomplete'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with intval().
  512. // phpcs:enable WordPress.Security.NonceVerification.Recommended
  513. // Generate text.
  514. $text = sprintf(
  515. _n( 'Field group activated.', '%s field groups activated.', count( $ids ), 'acf' ),
  516. count( $ids )
  517. );
  518. // Append links to text.
  519. $links = array();
  520. foreach ( $ids as $id ) {
  521. $links[] = '<a href="' . get_edit_post_link( $id ) . '">' . get_the_title( $id ) . '</a>';
  522. }
  523. $text .= ' ' . implode( ', ', $links );
  524. // Add notice.
  525. acf_add_admin_notice( $text, 'success' );
  526. return;
  527. }
  528. // Find items to activate.
  529. $ids = array();
  530. if ( isset( $_GET['acfactivate'] ) ) {
  531. $ids[] = intval( $_GET['acfactivate'] );
  532. } elseif ( isset( $_GET['post'], $_GET['action2'] ) && $_GET['action2'] === 'acfactivate' ) {
  533. $ids = array_map( 'intval', $_GET['post'] );
  534. }
  535. if ( $ids ) {
  536. check_admin_referer( 'bulk-posts' );
  537. // Activate the field groups and return an array of IDs that were activated.
  538. $new_ids = array();
  539. foreach ( $ids as $id ) {
  540. if ( acf_update_field_group_active_status( $id ) ) {
  541. $new_ids[] = $id;
  542. }
  543. }
  544. wp_redirect( $this->get_admin_url( '&acfactivatecomplete=' . implode( ',', $new_ids ) ) );
  545. exit;
  546. }
  547. }
  548. /**
  549. * Checks for the custom "Deactivate" bulk action.
  550. *
  551. * @since 6.0
  552. */
  553. public function check_deactivate() {
  554. // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used for redirect notice.
  555. // Display notice on success redirect.
  556. if ( isset( $_GET['acfdeactivatecomplete'] ) ) {
  557. $ids = array_map( 'intval', explode( ',', $_GET['acfdeactivatecomplete'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with intval().
  558. // phpcs:enable WordPress.Security.NonceVerification.Recommended
  559. // Generate text.
  560. $text = sprintf(
  561. _n( 'Field group deactivated.', '%s field groups deactivated.', count( $ids ), 'acf' ),
  562. count( $ids )
  563. );
  564. // Append links to text.
  565. $links = array();
  566. foreach ( $ids as $id ) {
  567. $links[] = '<a href="' . get_edit_post_link( $id ) . '">' . get_the_title( $id ) . '</a>';
  568. }
  569. $text .= ' ' . implode( ', ', $links );
  570. // Add notice.
  571. acf_add_admin_notice( $text, 'success' );
  572. return;
  573. }
  574. // Find items to activate.
  575. $ids = array();
  576. if ( isset( $_GET['acfdeactivate'] ) ) {
  577. $ids[] = intval( $_GET['acfdeactivate'] );
  578. } elseif ( isset( $_GET['post'], $_GET['action2'] ) && $_GET['action2'] === 'acfdeactivate' ) {
  579. $ids = array_map( 'intval', $_GET['post'] );
  580. }
  581. if ( $ids ) {
  582. check_admin_referer( 'bulk-posts' );
  583. // Activate the field groups and return an array of IDs.
  584. $new_ids = array();
  585. foreach ( $ids as $id ) {
  586. if ( acf_update_field_group_active_status( $id, false ) ) {
  587. $new_ids[] = $id;
  588. }
  589. }
  590. wp_redirect( $this->get_admin_url( '&acfdeactivatecomplete=' . implode( ',', $new_ids ) ) );
  591. exit;
  592. }
  593. }
  594. /**
  595. * Checks for the custom "duplicate" action.
  596. *
  597. * @date 15/4/20
  598. * @since 5.9.0
  599. *
  600. * @param void
  601. * @return void
  602. */
  603. public function check_duplicate() {
  604. // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used for redirect notice.
  605. // Display notice on success redirect.
  606. if ( isset( $_GET['acfduplicatecomplete'] ) ) {
  607. $ids = array_map( 'intval', explode( ',', $_GET['acfduplicatecomplete'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with intval().
  608. // phpcs:enable WordPress.Security.NonceVerification.Recommended
  609. // Generate text.
  610. $text = sprintf(
  611. _n( 'Field group duplicated.', '%s field groups duplicated.', count( $ids ), 'acf' ),
  612. count( $ids )
  613. );
  614. // Append links to text.
  615. $links = array();
  616. foreach ( $ids as $id ) {
  617. $links[] = '<a href="' . get_edit_post_link( $id ) . '">' . get_the_title( $id ) . '</a>';
  618. }
  619. $text .= ' ' . implode( ', ', $links );
  620. // Add notice.
  621. acf_add_admin_notice( $text, 'success' );
  622. return;
  623. }
  624. // Find items to duplicate.
  625. $ids = array();
  626. if ( isset( $_GET['acfduplicate'] ) ) {
  627. $ids[] = intval( $_GET['acfduplicate'] );
  628. } elseif ( isset( $_GET['post'], $_GET['action2'] ) && $_GET['action2'] === 'acfduplicate' ) {
  629. $ids = array_map( 'intval', $_GET['post'] );
  630. }
  631. if ( $ids ) {
  632. check_admin_referer( 'bulk-posts' );
  633. // Duplicate field groups and generate array of new IDs.
  634. $new_ids = array();
  635. foreach ( $ids as $id ) {
  636. $field_group = acf_duplicate_field_group( $id );
  637. $new_ids[] = $field_group['ID'];
  638. }
  639. // Redirect.
  640. wp_redirect( $this->get_admin_url( '&acfduplicatecomplete=' . implode( ',', $new_ids ) ) );
  641. exit;
  642. }
  643. }
  644. /**
  645. * Checks for the custom "acfsync" action.
  646. *
  647. * @date 15/4/20
  648. * @since 5.9.0
  649. *
  650. * @param void
  651. * @return void
  652. */
  653. public function check_sync() {
  654. // phpcs:disable WordPress.Security.NonceVerification.Recommended
  655. // Display notice on success redirect.
  656. if ( isset( $_GET['acfsynccomplete'] ) ) {
  657. $ids = array_map( 'intval', explode( ',', $_GET['acfsynccomplete'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized with intval().
  658. // phpcs:enable WordPress.Security.NonceVerification.Recommended
  659. // Generate text.
  660. $text = sprintf(
  661. _n( 'Field group synchronised.', '%s field groups synchronised.', count( $ids ), 'acf' ),
  662. count( $ids )
  663. );
  664. // Append links to text.
  665. $links = array();
  666. foreach ( $ids as $id ) {
  667. $links[] = '<a href="' . get_edit_post_link( $id ) . '">' . get_the_title( $id ) . '</a>';
  668. }
  669. $text .= ' ' . implode( ', ', $links );
  670. // Add notice.
  671. acf_add_admin_notice( $text, 'success' );
  672. return;
  673. }
  674. // Find items to sync.
  675. $keys = array();
  676. if ( isset( $_GET['acfsync'] ) ) {
  677. $keys[] = sanitize_text_field( $_GET['acfsync'] );
  678. } elseif ( isset( $_GET['post'], $_GET['action2'] ) && $_GET['action2'] === 'acfsync' ) {
  679. $keys = array_map( 'sanitize_text_field', $_GET['post'] );
  680. }
  681. if ( $keys && $this->sync ) {
  682. check_admin_referer( 'bulk-posts' );
  683. // Disabled "Local JSON" controller to prevent the .json file from being modified during import.
  684. acf_update_setting( 'json', false );
  685. // Sync field groups and generate array of new IDs.
  686. $files = acf_get_local_json_files();
  687. $new_ids = array();
  688. foreach ( $this->sync as $key => $field_group ) {
  689. if ( $field_group['key'] && in_array( $field_group['key'], $keys ) ) {
  690. // Import.
  691. } elseif ( $field_group['ID'] && in_array( $field_group['ID'], $keys ) ) {
  692. // Import.
  693. } else {
  694. // Ignore.
  695. continue;
  696. }
  697. $local_field_group = json_decode( file_get_contents( $files[ $key ] ), true );
  698. $local_field_group['ID'] = $field_group['ID'];
  699. $result = acf_import_field_group( $local_field_group );
  700. $new_ids[] = $result['ID'];
  701. }
  702. // Redirect.
  703. wp_redirect( $this->get_current_admin_url( '&acfsynccomplete=' . implode( ',', $new_ids ) ) );
  704. exit;
  705. }
  706. }
  707. /**
  708. * Customizes the admin table subnav.
  709. *
  710. * @date 17/4/20
  711. * @since 5.9.0
  712. *
  713. * @param array $views The available views.
  714. * @return array
  715. */
  716. public function admin_table_views( $views ) {
  717. global $wp_list_table, $wp_query;
  718. // Count items.
  719. $count = count( $this->sync );
  720. // Append "sync" link to subnav.
  721. if ( $count ) {
  722. $views['sync'] = sprintf(
  723. '<a %s href="%s">%s <span class="count">(%s)</span></a>',
  724. ( $this->view === 'sync' ? 'class="current"' : '' ),
  725. esc_url( $this->get_admin_url( '&post_status=sync' ) ),
  726. esc_html( __( 'Sync available', 'acf' ) ),
  727. $count
  728. );
  729. }
  730. // Modify table pagination args to match JSON data.
  731. if ( $this->view === 'sync' ) {
  732. $wp_list_table->set_pagination_args(
  733. array(
  734. 'total_items' => $count,
  735. 'total_pages' => 1,
  736. 'per_page' => $count,
  737. )
  738. );
  739. $wp_query->post_count = 1; // At least one post is needed to render bulk drop-down.
  740. }
  741. return $views;
  742. }
  743. /**
  744. * Prints scripts into the admin footer.
  745. *
  746. * @date 20/4/20
  747. * @since 5.9.0
  748. *
  749. * @param void
  750. * @return void
  751. */
  752. function admin_footer() {
  753. ?>
  754. <script type="text/javascript">
  755. (function($){
  756. // Displays a modal comparing local changes.
  757. function reviewSync( props ) {
  758. var modal = acf.newModal({
  759. title: acf.__('Review local JSON changes'),
  760. content: '<p class="acf-modal-feedback"><i class="acf-loading"></i> ' + acf.__('Loading diff') + '</p>',
  761. toolbar: '<a href="' + props.href + '" class="button button-primary button-sync-changes disabled">' + acf.__('Sync changes') + '</a>',
  762. });
  763. // Call AJAX.
  764. var xhr = $.ajax({
  765. url: acf.get('ajaxurl'),
  766. method: 'POST',
  767. dataType: 'json',
  768. data: acf.prepareForAjax({
  769. action: 'acf/ajax/local_json_diff',
  770. id: props.id
  771. })
  772. })
  773. .done(function( data, textStatus, jqXHR ) {
  774. modal.content( data.html );
  775. modal.$('.button-sync-changes').removeClass('disabled');
  776. })
  777. .fail(function( jqXHR, textStatus, errorThrown ) {
  778. if( error = acf.getXhrError(jqXHR) ) {
  779. modal.content( '<p class="acf-modal-feedback error">' + error + '</p>' );
  780. }
  781. });
  782. }
  783. // Add event listener.
  784. $(document).on('click', 'a[data-event="review-sync"]', function( e ){
  785. e.preventDefault();
  786. reviewSync( $(this).data() );
  787. });
  788. })(jQuery);
  789. </script>
  790. <?php
  791. }
  792. /**
  793. * Customizes the admin table HTML when viewing "sync" post_status.
  794. *
  795. * @date 17/4/20
  796. * @since 5.9.0
  797. *
  798. * @param array $views The available views.
  799. * @return array
  800. */
  801. public function admin_footer__sync() {
  802. global $wp_list_table;
  803. // Get table columns.
  804. $columns = $wp_list_table->get_columns();
  805. $hidden = get_hidden_columns( $wp_list_table->screen );
  806. ?>
  807. <div style="display: none;">
  808. <table>
  809. <tbody id="acf-the-list">
  810. <?php
  811. foreach ( $this->sync as $k => $field_group ) {
  812. echo '<tr>';
  813. foreach ( $columns as $column_name => $column_label ) {
  814. $el = 'td';
  815. if ( $column_name === 'cb' ) {
  816. $el = 'th';
  817. $classes = 'check-column';
  818. $column_label = '';
  819. } elseif ( $column_name === 'title' ) {
  820. $classes = "$column_name column-$column_name column-primary";
  821. } else {
  822. $classes = "$column_name column-$column_name";
  823. }
  824. if ( in_array( $column_name, $hidden, true ) ) {
  825. $classes .= ' hidden';
  826. }
  827. echo "<$el class=\"$classes\" data-colname=\"$column_label\">";
  828. switch ( $column_name ) {
  829. // Checkbox.
  830. case 'cb':
  831. echo '<label for="cb-select-' . esc_attr( $k ) . '" class="screen-reader-text">' . esc_html( sprintf( __( 'Select %s', 'acf' ), $field_group['title'] ) ) . '</label>';
  832. echo '<input id="cb-select-' . esc_attr( $k ) . '" type="checkbox" value="' . esc_attr( $k ) . '" name="post[]">';
  833. break;
  834. // Title.
  835. case 'title':
  836. $post_state = '';
  837. if ( ! $field_group['active'] ) {
  838. $post_state = ' — <span class="post-state">' . $this->get_disabled_post_state() . '</span>';
  839. }
  840. echo '<strong><span class="row-title">' . esc_html( $field_group['title'] ) . '</span>' . $post_state . '</strong>';
  841. echo '<div class="row-actions"><span class="file acf-secondary-text">' . $this->get_human_readable_file_location( $field_group['local_file'] ) . '</span></div>';
  842. echo '<button type="button" class="toggle-row"><span class="screen-reader-text">Show more details</span></button>';
  843. break;
  844. // All other columns.
  845. default:
  846. $this->render_admin_table_column( $column_name, $field_group );
  847. break;
  848. }
  849. echo "</$el>";
  850. }
  851. echo '</tr>';
  852. }
  853. ?>
  854. </tbody>
  855. </table>
  856. </div>
  857. <script type="text/javascript">
  858. (function($){
  859. $('#the-list').html( $('#acf-the-list').children() );
  860. })(jQuery);
  861. </script>
  862. <?php
  863. }
  864. /**
  865. * Fires when trashing a field group post.
  866. *
  867. * @date 8/01/2014
  868. * @since 5.0.0
  869. *
  870. * @param int $post_id The post ID.
  871. * @return void
  872. */
  873. public function trashed_post( $post_id ) {
  874. if ( get_post_type( $post_id ) === 'acf-field-group' ) {
  875. acf_trash_field_group( $post_id );
  876. }
  877. }
  878. /**
  879. * Fires when untrashing a field group post.
  880. *
  881. * @date 8/01/2014
  882. * @since 5.0.0
  883. *
  884. * @param int $post_id The post ID.
  885. * @return void
  886. */
  887. public function untrashed_post( $post_id ) {
  888. if ( get_post_type( $post_id ) === 'acf-field-group' ) {
  889. acf_untrash_field_group( $post_id );
  890. }
  891. }
  892. /**
  893. * Fires when deleting a field group post.
  894. *
  895. * @date 8/01/2014
  896. * @since 5.0.0
  897. *
  898. * @param int $post_id The post ID.
  899. * @return void
  900. */
  901. public function deleted_post( $post_id ) {
  902. if ( get_post_type( $post_id ) === 'acf-field-group' ) {
  903. acf_delete_field_group( $post_id );
  904. }
  905. }
  906. }
  907. // Instantiate.
  908. acf_new_instance( 'ACF_Admin_Field_Groups' );
  909. endif; // class_exists check