Source: my-calendar-location-manager.php

  1. <?php
  2. /**
  3. * Manage Locations
  4. *
  5. * @category Locations
  6. * @package My Calendar
  7. * @author Joe Dolson
  8. * @license GPLv2 or later
  9. * @link https://www.joedolson.com/my-calendar/
  10. */
  11. if ( ! defined( 'ABSPATH' ) ) {
  12. exit;
  13. }
  14. /**
  15. * List of locations to edit.
  16. */
  17. function my_calendar_manage_locations() {
  18. ?>
  19. <div class="wrap my-calendar-admin my-calendar-locations">
  20. <?php
  21. my_calendar_check_db();
  22. // We do some checking to see what we're doing.
  23. mc_mass_delete_locations();
  24. mc_clean_duplicate_locations();
  25. if ( ! empty( $_POST ) && ( ! isset( $_POST['mc_locations'] ) && ! isset( $_POST['mass_delete'] ) ) ) {
  26. $nonce = $_REQUEST['_wpnonce'];
  27. if ( ! wp_verify_nonce( $nonce, 'my-calendar-nonce' ) ) {
  28. wp_die( 'My Calendar: Security check failed' );
  29. }
  30. }
  31. if ( isset( $_GET['location_id'] ) && 'delete' === $_GET['mode'] ) {
  32. $verify = isset( $_GET['nonce'] ) ? wp_verify_nonce( $_GET['nonce'], 'my-calendar-delete-location' ) : false;
  33. $loc = absint( $_GET['location_id'] );
  34. if ( isset( $_GET['confirm'] ) && $verify ) {
  35. echo wp_kses_post( mc_delete_location( $loc ) );
  36. } else {
  37. $nonce = wp_create_nonce( 'my-calendar-delete-location' );
  38. $args = array(
  39. 'location_id' => $loc,
  40. 'nonce' => $nonce,
  41. );
  42. // Translators: Delete link.
  43. $notice = sprintf( __( 'Are you sure you want to delete this location? %s', 'my-calendar' ), '<a class="button delete" href="' . esc_url( add_query_arg( $args, admin_url( 'admin.php?page=my-calendar-location-manager&mode=delete&confirm=true' ) ) ) . '">' . __( 'Delete', 'my-calendar' ) . '</a>' );
  44. mc_show_notice( $notice, true, false, 'warning' );
  45. }
  46. }
  47. if ( isset( $_GET['default'] ) && is_numeric( $_GET['default'] ) ) {
  48. $mcnonce = wp_verify_nonce( $_GET['_mcnonce'], 'mcnonce' );
  49. if ( $mcnonce ) {
  50. mc_update_option( 'default_location', (int) $_GET['default'] );
  51. mc_show_notice( __( 'Default Location Changed', 'my-calendar' ), true, false, 'success' );
  52. } else {
  53. mc_show_error( __( 'Invalid security check; please try again!', 'my-calendar' ) );
  54. }
  55. }
  56. ?>
  57. <h1 class="wp-heading-inline"><?php esc_html_e( 'Locations', 'my-calendar' ); ?></h1>
  58. <a href="<?php echo esc_url( admin_url( 'admin.php?page=my-calendar-locations' ) ); ?>" class="page-title-action"><?php esc_html_e( 'Add New', 'my-calendar' ); ?></a>
  59. <?php
  60. if ( '' === mc_get_option( 'location_cpt_base', '' ) ) {
  61. ?>
  62. <a class="page-title-action" href="<?php echo esc_url( admin_url( 'options-permalink.php#mc_location_cpt_base' ) ); ?>"><?php esc_html_e( 'Update location permalinks', 'my-calendar' ); ?></a>
  63. <?php
  64. }
  65. ?>
  66. <hr class="wp-header-end">
  67. <div class="postbox-container jcd-wide">
  68. <div class="metabox-holder">
  69. <div class="ui-sortable meta-box-sortables">
  70. <div class="postbox">
  71. <h2><?php esc_html_e( 'Manage Locations', 'my-calendar' ); ?></h2>
  72. <div class="inside">
  73. <?php mc_manage_locations(); ?>
  74. </div>
  75. </div>
  76. </div>
  77. </div>
  78. </div>
  79. <?php mc_show_sidebar(); ?>
  80. </div>
  81. <?php
  82. }
  83. /**
  84. * Fetch default location data.
  85. *
  86. * @return string
  87. */
  88. function mc_default_location() {
  89. $default = mc_get_option( 'default_location' );
  90. $output = '';
  91. if ( $default ) {
  92. $location = mc_get_location( $default );
  93. if ( ! $location ) {
  94. return '';
  95. }
  96. $output = mc_hcard( $location, 'true', false, 'location' );
  97. $output .= '<p><a href="' . admin_url( "admin.php?page=my-calendar-locations&amp;mode=edit&amp;location_id=$default" ) . '">' . __( 'Edit Default Location', 'my-calendar' ) . '</a></p>';
  98. }
  99. if ( ! $output ) {
  100. $output = '<p>' . __( 'No default location selected.', 'my-calendar' ) . '</p>';
  101. }
  102. return $output;
  103. }
  104. /**
  105. * Mass replace locations.
  106. *
  107. * @return mixed boolean/int query result.
  108. */
  109. function mc_clean_duplicate_locations() {
  110. global $wpdb;
  111. // Mass delete locations.
  112. if ( ! empty( $_POST['mass_edit'] ) && isset( $_POST['mass_replace'] ) ) {
  113. $nonce = $_REQUEST['_wpnonce'];
  114. $post = map_deep( $_POST, 'sanitize_text_field' );
  115. if ( ! wp_verify_nonce( $nonce, 'my-calendar-nonce' ) ) {
  116. wp_die( 'My Calendar: Security check failed' );
  117. }
  118. $locations = $post['mass_edit'];
  119. $replace = absint( $post['mass_replace_id'] );
  120. $location = mc_get_location( $replace );
  121. if ( ! $location ) {
  122. // If this isn't a valid location, don't continue.
  123. mc_show_error( esc_html__( 'An invalid ID was provided for the replacement location.', 'my-calendar' ) );
  124. return;
  125. }
  126. $i = 0;
  127. $total = 0;
  128. $deleted = array();
  129. foreach ( $locations as $value ) {
  130. // If the replacement location is checked, ignore it.
  131. if ( (int) $replace === (int) $value ) {
  132. continue;
  133. }
  134. $total = count( $locations );
  135. $result = mc_delete_location( $value, 'bool' );
  136. if ( ! $result ) {
  137. $failed[] = absint( $value );
  138. } else {
  139. $deleted[] = absint( $value );
  140. }
  141. $wpdb->update(
  142. my_calendar_table(),
  143. array(
  144. 'event_location' => $replace,
  145. ),
  146. array(
  147. 'event_location' => $value,
  148. ),
  149. '%d',
  150. '%d'
  151. );
  152. ++$i;
  153. }
  154. if ( ! empty( $deleted ) ) {
  155. /**
  156. * Run when action to clean up duplicate locations is run.
  157. *
  158. * @hook mc_clean_duplicate_locations
  159. *
  160. * @param {array} $deleted Array of location IDs successfully deleted.
  161. * @param {array} $failed Array of location IDs that were not successfully deleted.
  162. */
  163. do_action( 'mc_clean_duplicate_locations', $deleted, $failed );
  164. // Translators: Number of locations deleted, number selected.
  165. $message = mc_show_notice( sprintf( __( '%1$d locations deleted successfully out of %2$d selected', 'my-calendar' ), count( $deleted ), $total ), false, false, 'success' );
  166. } else {
  167. $message = mc_show_error( __( 'Your locations have not been deleted. Please investigate.', 'my-calendar' ), false );
  168. }
  169. echo wp_kses_post( $message );
  170. }
  171. }
  172. /**
  173. * Mass delete locations.
  174. *
  175. * @return mixed boolean/int query result.
  176. */
  177. function mc_mass_delete_locations() {
  178. global $wpdb;
  179. // Mass delete locations.
  180. if ( ! empty( $_POST['mass_edit'] ) && isset( $_POST['mass_delete'] ) ) {
  181. $post = map_deep( $_POST, 'sanitize_text_field' );
  182. $nonce = $_REQUEST['_wpnonce'];
  183. if ( ! wp_verify_nonce( $nonce, 'my-calendar-nonce' ) ) {
  184. wp_die( 'My Calendar: Security check failed' );
  185. }
  186. $locations = $post['mass_edit'];
  187. $i = 0;
  188. $total = 0;
  189. $deleted = array();
  190. $failed = array();
  191. foreach ( $locations as $value ) {
  192. $total = count( $locations );
  193. $result = mc_delete_location( $value, 'bool' );
  194. if ( ! $result ) {
  195. $failed[] = absint( $value );
  196. } else {
  197. $deleted[] = absint( $value );
  198. }
  199. ++$i;
  200. }
  201. if ( ! empty( $deleted ) ) {
  202. /**
  203. * Run when multiple locations are deleted.
  204. *
  205. * @hook mc_mass_delete_locations
  206. *
  207. * @param {array} $deleted Array of location IDs successfully deleted.
  208. * @param {array} $failed Array of location IDs that were not successfully deleted.
  209. */
  210. do_action( 'mc_mass_delete_locations', $deleted, $failed );
  211. // Translators: Number of locations deleted, number selected.
  212. $message = mc_show_notice( sprintf( __( '%1$d locations deleted successfully out of %2$d selected', 'my-calendar' ), count( $deleted ), $total ), false );
  213. } else {
  214. $message = mc_show_error( __( 'Your locations have not been deleted. Please investigate.', 'my-calendar' ), false );
  215. }
  216. echo wp_kses_post( $message );
  217. }
  218. }
  219. /**
  220. * Generate list of locations.
  221. */
  222. function mc_manage_locations() {
  223. global $wpdb;
  224. $orderby = 'location_label';
  225. $sortby = 'location';
  226. if ( isset( $_GET['orderby'] ) ) {
  227. $sortby = $_GET['orderby'];
  228. switch ( $sortby ) {
  229. case 'city':
  230. $orderby = 'location_city';
  231. break;
  232. case 'state':
  233. $orderby = 'location_state';
  234. break;
  235. case 'id':
  236. $orderby = 'location_id';
  237. break;
  238. default:
  239. $orderby = 'location_label';
  240. }
  241. }
  242. $order = 'ASC';
  243. $query_order = 'DESC';
  244. if ( isset( $_GET['order'] ) ) {
  245. switch ( $_GET['order'] ) {
  246. case 'asc':
  247. $order = 'DESC';
  248. $query_order = 'ASC';
  249. break;
  250. case 'desc':
  251. $order = 'ASC';
  252. $query_order = 'DESC';
  253. break;
  254. default:
  255. $order = 'ASC';
  256. $query_order = 'DESC';
  257. }
  258. }
  259. // Pull the locations from the database.
  260. $items_per_page = 50;
  261. $search = '';
  262. $current = empty( $_GET['paged'] ) ? 1 : intval( $_GET['paged'] );
  263. if ( isset( $_POST['mcl'] ) ) {
  264. $query = esc_sql( $_POST['mcl'] );
  265. $length = strlen( $query );
  266. $db_type = mc_get_db_type();
  267. if ( '' !== $query ) {
  268. if ( 'MyISAM' === $db_type && $length > 3 ) {
  269. /**
  270. * Customize admin search MATCH columns when db is MyISAM.
  271. *
  272. * @hook mc_search_fields
  273. *
  274. * @param {string} $fields Comma-separated list of column names.
  275. *
  276. * @return {string}
  277. */
  278. $search = ' WHERE MATCH(' . apply_filters( 'mc_search_fields', 'location_label,location_city,location_state,location_region,location_country,location_street,location_street2,location_phone' ) . ") AGAINST ( '$query' IN BOOLEAN MODE ) ";
  279. } else {
  280. $search = " WHERE location_label LIKE '%$query%' OR location_city LIKE '%$query%' OR location_state LIKE '%$query%' OR location_region LIKE '%$query%' OR location_country LIKE '%$query%' OR location_street LIKE '%$query%' OR location_street2 LIKE '%$query%' OR location_phone LIKE '%$query%' ";
  281. }
  282. } else {
  283. $search = '';
  284. }
  285. }
  286. $query_limit = ( ( $current - 1 ) * $items_per_page );
  287. $locations = $wpdb->get_results( $wpdb->prepare( 'SELECT location_id FROM ' . my_calendar_locations_table() . " $search ORDER BY $orderby $query_order LIMIT %d, %d", $query_limit, $items_per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
  288. $found_rows = $wpdb->get_col( 'SELECT COUNT(*) FROM ' . my_calendar_locations_table() . " $search ORDER BY $orderby $query_order" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
  289. $items = $found_rows[0];
  290. $pagination = '';
  291. $num_pages = ceil( $items / $items_per_page );
  292. if ( $num_pages > 1 ) {
  293. $page_links = paginate_links(
  294. array(
  295. 'base' => add_query_arg( 'paged', '%#%' ),
  296. 'format' => '',
  297. 'prev_text' => __( '&laquo; Previous<span class="screen-reader-text"> Locations</span>', 'my-calendar' ),
  298. 'next_text' => __( 'Next<span class="screen-reader-text"> Locations</span> &raquo;', 'my-calendar' ),
  299. 'total' => $num_pages,
  300. 'current' => $current,
  301. 'mid_size' => 1,
  302. )
  303. );
  304. $nav_label = esc_attr( __( 'Locations Pagination', 'my-calendar' ) );
  305. $pagination = sprintf( "<nav class='tablenav' aria-label='$nav_label'><div class='tablenav-pages'>%s</div></nav>", $page_links );
  306. }
  307. if ( ! empty( $locations ) ) {
  308. echo wp_kses_post( $pagination );
  309. ?>
  310. <div class="mc-admin-header locations">
  311. <div class='mc-search'>
  312. <form action="<?php echo esc_url( admin_url( 'admin.php?page=my-calendar-location-manager' ) ); ?>" method="post" role='search'>
  313. <div>
  314. <input type="hidden" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( 'my-calendar-nonce' ) ); ?>"/>
  315. </div>
  316. <div>
  317. <label for="mc_search" class='screen-reader-text'><?php esc_html_e( 'Search Locations', 'my-calendar' ); ?></label>
  318. <input type='text' name='mcl' id='mc_search' value='<?php echo ( isset( $_POST['mcl'] ) ) ? esc_attr( $_POST['mcl'] ) : ''; ?>'/>
  319. <input type='submit' value='<?php esc_attr_e( 'Search', 'my-calendar' ); ?>' class='button-secondary' />
  320. </div>
  321. </form>
  322. </div>
  323. </div>
  324. <form action="<?php echo esc_url( add_query_arg( $_GET, admin_url( 'admin.php' ) ) ); ?>" method="post">
  325. <div><input type="hidden" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( 'my-calendar-nonce' ) ); ?>"/></div>
  326. <div class='mc-actions'>
  327. <input type="submit" class="button-secondary delete" name="mass_delete" value="<?php esc_attr_e( 'Delete locations', 'my-calendar' ); ?>" />
  328. <div class="mass-replace-wrap">
  329. <input type="checkbox" name="mass_replace_on" disabled id="mass_replace_on" value="true"><label for="mass_replace_on"><?php esc_attr_e( 'Merge duplicates', 'my-calendar' ); ?></label>
  330. <div class="mass-replace-container">
  331. <label for="mass-replace"><?php esc_html_e( 'Replacement ID', 'my-calendar' ); ?></label><input type="text" size="4" id="mass-replace" name="mass_replace_id" class="mass-replace" value="" />
  332. <input type="submit" class="button-secondary delete" name="mass_replace" value="<?php esc_html_e( 'Replace', 'my-calendar' ); ?>" />
  333. </div>
  334. </div>
  335. <div><input type='checkbox' class='selectall' id='mass_edit' data-action="mass_edit" /> <label for='mass_edit'><?php esc_html_e( 'Check all', 'my-calendar' ); ?></label></div>
  336. </div>
  337. <table class="widefat striped page mc-responsive-table mc-locations-table" id="my-calendar-admin-table">
  338. <caption class="screen-reader-text"><?php esc_html_e( 'Location list. Use column headers to sort.', 'my-calendar' ); ?></caption>
  339. <thead>
  340. <tr>
  341. <?php
  342. $admin_url = admin_url( "admin.php?page=my-calendar-location-manager&paged=$current&order=" . strtolower( $order ) );
  343. $url = add_query_arg( 'orderby', 'id', $admin_url );
  344. $col_head = mc_table_header( __( 'ID', 'my-calendar' ), $order, $sortby, 'id', $url );
  345. $url = add_query_arg( 'orderby', 'location', $admin_url );
  346. $col_head .= mc_table_header( __( 'Location', 'my-calendar' ), $order, $sortby, 'location', $url );
  347. $url = add_query_arg( 'orderby', 'city', $admin_url );
  348. $col_head .= mc_table_header( __( 'City', 'my-calendar' ), $order, $sortby, 'city', $url );
  349. $url = add_query_arg( 'orderby', 'state', $admin_url );
  350. $col_head .= mc_table_header( __( 'State/Province', 'my-calendar' ), $order, $sortby, 'state', $url );
  351. $col_head .= mc_table_header( __( 'Events', 'my-calendar' ), $order, $sortby, 'count', '' );
  352. echo wp_kses( $col_head, mc_kses_elements() );
  353. /**
  354. * Add custom column table headers to Location Manager.
  355. *
  356. * @hook mc_location_manager_headers
  357. *
  358. * @param {string} $headers HTML output. Appends HTML in the end column of the location manager table row.
  359. *
  360. * @return {string}
  361. */
  362. $headers = apply_filters( 'mc_location_manager_headers', '' );
  363. echo wp_kses( $headers, mc_kses_elements() );
  364. ?>
  365. </tr>
  366. </thead>
  367. <tbody>
  368. <?php
  369. $default_location = mc_get_option( 'default_location', '' );
  370. if ( $default_location ) {
  371. $default = mc_get_location( $default_location );
  372. echo wp_kses( mc_location_manager_row( $default ), mc_kses_elements() );
  373. }
  374. foreach ( $locations as $loc ) {
  375. if ( (int) $default_location === (int) $loc->location_id ) {
  376. continue;
  377. }
  378. $location = mc_get_location( $loc->location_id );
  379. echo wp_kses( mc_location_manager_row( $location ), mc_kses_elements() );
  380. }
  381. ?>
  382. </tbody>
  383. </table>
  384. <div class="mc-actions">
  385. <p>
  386. <input type="submit" class="button-secondary delete" name="mass_delete" value="<?php esc_html_e( 'Delete locations', 'my-calendar' ); ?>" />
  387. </p>
  388. </div>
  389. </form>
  390. <?php
  391. } else {
  392. if ( isset( $_POST['mcl'] ) ) {
  393. echo '<p>' . esc_html__( 'No results found for your search query.', 'my-calendar' ) . '</p>';
  394. }
  395. echo '<p><a class="button" href="' . esc_url( admin_url( 'admin.php?page=my-calendar-locations' ) ) . '">' . esc_html__( 'Create a new location', 'my-calendar' ) . '</a></p>';
  396. }
  397. }
  398. /**
  399. * Verify that a location has valid fields. If a location has no valid data, delete it.
  400. *
  401. * @param object $location Location object.
  402. *
  403. * @return bool
  404. */
  405. function mc_verify_location( $location ) {
  406. if ( ! is_object( $location ) ) {
  407. return true;
  408. }
  409. $location_id = $location->location_id;
  410. $copy = clone $location;
  411. // Unset location ID and location Post, which will always exist.
  412. $copy->location_id = '';
  413. $copy->location_post = '';
  414. $json = wp_json_encode( $copy );
  415. if ( '{"location_id":"","location_label":"","location_street":"","location_street2":"","location_city":"","location_state":"","location_postcode":"","location_region":"","location_url":"","location_country":"","location_longitude":"0.000000","location_latitude":"0.000000","location_zoom":"16","location_phone":"","location_phone2":"","location_access":"","location_post":""}' === $json ) {
  416. if ( $location_id ) {
  417. mc_delete_location( $location_id );
  418. mc_location_delete_post( true, $location_id );
  419. }
  420. return false;
  421. }
  422. return true;
  423. }
  424. /**
  425. * Generate the location manager row for a location.
  426. *
  427. * @param object $location Location object.
  428. *
  429. * @return string
  430. */
  431. function mc_location_manager_row( $location ) {
  432. $card = mc_hcard( $location, 'true', 'false', 'location' );
  433. $verify = mc_verify_location( $location );
  434. $count = mc_count_location_events( $location->location_id );
  435. $filters = array(
  436. 'filter' => $location->location_id,
  437. 'restrict' => 'where',
  438. );
  439. $location_filter = add_query_arg( $filters, admin_url( 'admin.php?page=my-calendar-manage' ) );
  440. if ( $count ) {
  441. $count = '<a href="' . esc_url( $location_filter ) . '">' . $count . '</a>';
  442. }
  443. if ( ! $verify ) {
  444. return '';
  445. }
  446. if ( (int) mc_get_option( 'default_location' ) === (int) $location->location_id ) {
  447. $card = str_replace( '</strong>', ' ' . __( '(Default)', 'my-calendar' ) . '</strong>', $card );
  448. $default = '<span class="mc_default">' . __( 'Default Location', 'my-calendar' ) . '</span>';
  449. } else {
  450. $mcnonce = wp_create_nonce( 'mcnonce' );
  451. $url = add_query_arg( '_mcnonce', $mcnonce, admin_url( "admin.php?page=my-calendar-location-manager&amp;default=$location->location_id" ) );
  452. $default = '<a href="' . esc_url( $url ) . '">' . __( 'Set as Default', 'my-calendar' ) . '</a>';
  453. }
  454. $delete_url = admin_url( "admin.php?page=my-calendar-location-manager&amp;mode=delete&amp;location_id=$location->location_id" );
  455. $view_url = get_the_permalink( mc_get_location_post( $location->location_id, false ) );
  456. $edit_url = admin_url( "admin.php?page=my-calendar-locations&amp;mode=edit&amp;location_id=$location->location_id" );
  457. $view_link = '';
  458. if ( $view_url && esc_url( $view_url ) ) {
  459. $view_link = "<a href='" . esc_url( $view_url ) . "' class='view' aria-describedby='location" . absint( $location->location_id ) . "'>" . esc_html__( 'View', 'my-calendar' ) . '</a> | ';
  460. }
  461. /**
  462. * Add custom column table cells to Location Manager.
  463. *
  464. * @hook mc_location_manager_cells
  465. *
  466. * @param {string} $custom_location_cells HTML output. Appends HTML in the end column of the location manager table row.
  467. * @param {object} $location Locatino object.
  468. * @return {string}
  469. */
  470. $custom_location_cells = apply_filters( 'mc_location_manager_cells', '', $location );
  471. $row = '';
  472. $row .= '
  473. <tr>
  474. <th scope="row">
  475. <input type="checkbox" value="' . absint( $location->location_id ) . '" name="mass_edit[]" id="mc' . absint( $location->location_id ) . '"/>
  476. <label for="mc' . absint( $location->location_id ) . '">' . $location->location_id . '</label>
  477. </th>
  478. <td>' . $card . '
  479. <div class="row-actions">' . $view_link . '
  480. <a href="' . esc_url( $edit_url ) . '" class="edit" aria-describedby="location' . absint( $location->location_id ) . '">' . esc_html__( 'Edit', 'my-calendar' ) . '</a> | ' . $default . ' |
  481. <a href="' . esc_url( $delete_url ) . '" class="delete" aria-describedby="location' . absint( $location->location_id ) . '">' . esc_html__( 'Delete', 'my-calendar' ) . '</a>
  482. </div>
  483. </td>
  484. <td>' . esc_html( $location->location_city ) . '</td>
  485. <td>' . esc_html( $location->location_state ) . '</td>' . $custom_location_cells . '
  486. <td>' . $count . '</td>
  487. </tr>';
  488. return $row;
  489. }