<?php
/**
* Manage Locations
*
* @category Locations
* @package My Calendar
* @author Joe Dolson
* @license GPLv2 or later
* @link https://www.joedolson.com/my-calendar/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* List of locations to edit.
*/
function my_calendar_manage_locations() {
?>
<div class="wrap my-calendar-admin">
<?php
my_calendar_check_db();
// We do some checking to see what we're doing.
mc_mass_delete_locations();
mc_clean_duplicate_locations();
if ( ! empty( $_POST ) && ( ! isset( $_POST['mc_locations'] ) && ! isset( $_POST['mass_delete'] ) ) ) {
$nonce = $_REQUEST['_wpnonce'];
if ( ! wp_verify_nonce( $nonce, 'my-calendar-nonce' ) ) {
wp_die( 'My Calendar: Security check failed' );
}
}
if ( isset( $_GET['location_id'] ) && 'delete' === $_GET['mode'] ) {
$verify = isset( $_GET['nonce'] ) ? wp_verify_nonce( $_GET['nonce'], 'my-calendar-delete-location' ) : false;
$loc = absint( $_GET['location_id'] );
if ( isset( $_GET['confirm'] ) && $verify ) {
echo wp_kses_post( mc_delete_location( $loc ) );
} else {
$nonce = wp_create_nonce( 'my-calendar-delete-location' );
$args = array(
'location_id' => $loc,
'nonce' => $nonce,
);
// Translators: Delete link.
$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>' );
mc_show_notice( $notice, true, false, 'warning' );
}
}
if ( isset( $_GET['default'] ) && is_numeric( $_GET['default'] ) ) {
$mcnonce = wp_verify_nonce( $_GET['_mcnonce'], 'mcnonce' );
if ( $mcnonce ) {
mc_update_option( 'default_location', (int) $_GET['default'] );
mc_show_notice( __( 'Default Location Changed', 'my-calendar' ), true, false, 'success' );
} else {
mc_show_error( __( 'Invalid security check; please try again!', 'my-calendar' ) );
}
}
?>
<h1 class="wp-heading-inline"><?php esc_html_e( 'Locations', 'my-calendar' ); ?></h1>
<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>
<?php
if ( '' === mc_get_option( 'location_cpt_base', '' ) ) {
?>
<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>
<?php
}
?>
<hr class="wp-header-end">
<div class="postbox-container jcd-wide">
<div class="metabox-holder">
<div class="ui-sortable meta-box-sortables">
<div class="postbox">
<h2><?php esc_html_e( 'Manage Locations', 'my-calendar' ); ?></h2>
<div class="inside">
<?php mc_manage_locations(); ?>
</div>
</div>
</div>
</div>
</div>
<?php mc_show_sidebar(); ?>
</div>
<?php
}
/**
* Fetch default location data.
*
* @return string
*/
function mc_default_location() {
$default = mc_get_option( 'default_location' );
$output = '';
if ( $default ) {
$location = mc_get_location( $default );
if ( ! $location ) {
return '';
}
$output = mc_hcard( $location, 'true', false, 'location' );
$output .= '<p><a href="' . admin_url( "admin.php?page=my-calendar-locations&mode=edit&location_id=$default" ) . '">' . __( 'Edit Default Location', 'my-calendar' ) . '</a></p>';
}
if ( ! $output ) {
$output = '<p>' . __( 'No default location selected.', 'my-calendar' ) . '</p>';
}
return $output;
}
/**
* Mass replace locations.
*
* @return mixed boolean/int query result.
*/
function mc_clean_duplicate_locations() {
global $wpdb;
// Mass delete locations.
if ( ! empty( $_POST['mass_edit'] ) && isset( $_POST['mass_replace'] ) ) {
$nonce = $_REQUEST['_wpnonce'];
$post = map_deep( $_POST, 'sanitize_text_field' );
if ( ! wp_verify_nonce( $nonce, 'my-calendar-nonce' ) ) {
wp_die( 'My Calendar: Security check failed' );
}
$locations = $post['mass_edit'];
$replace = absint( $post['mass_replace_id'] );
$location = mc_get_location( $replace );
if ( ! $location ) {
// If this isn't a valid location, don't continue.
echo mc_show_error( __( 'An invalid ID was provided for the replacement location.', 'my-calendar' ) );
return;
}
$i = 0;
$total = 0;
$deleted = array();
foreach ( $locations as $value ) {
// If the replacement location is checked, ignore it.
if ( (int) $replace === (int) $value ) {
continue;
}
$total = count( $locations );
$result = mc_delete_location( $value, 'bool' );
if ( ! $result ) {
$failed[] = absint( $value );
} else {
$deleted[] = absint( $value );
}
$wpdb->update(
my_calendar_table(),
array(
'event_location' => $replace,
),
array(
'event_location' => $value,
),
'%d',
'%d'
);
++$i;
}
if ( ! empty( $deleted ) ) {
/**
* Run when action to clean up duplicate locations is run.
*
* @hook mc_clean_duplicate_locations
*
* @param {array} $deleted Array of location IDs successfully deleted.
* @param {array} $failed Array of location IDs that were not successfully deleted.
*/
do_action( 'mc_clean_duplicate_locations', $deleted, $failed );
// Translators: Number of locations deleted, number selected.
$message = mc_show_notice( sprintf( __( '%1$d locations deleted successfully out of %2$d selected', 'my-calendar' ), count( $deleted ), $total ), false, false, 'success' );
} else {
$message = mc_show_error( __( 'Your locations have not been deleted. Please investigate.', 'my-calendar' ), false );
}
echo wp_kses_post( $message );
}
}
/**
* Mass delete locations.
*
* @return mixed boolean/int query result.
*/
function mc_mass_delete_locations() {
global $wpdb;
// Mass delete locations.
if ( ! empty( $_POST['mass_edit'] ) && isset( $_POST['mass_delete'] ) ) {
$post = map_deep( $_POST, 'sanitize_text_field' );
$nonce = $_REQUEST['_wpnonce'];
if ( ! wp_verify_nonce( $nonce, 'my-calendar-nonce' ) ) {
wp_die( 'My Calendar: Security check failed' );
}
$locations = $post['mass_edit'];
$i = 0;
$total = 0;
$deleted = array();
$failed = array();
foreach ( $locations as $value ) {
$total = count( $locations );
$result = mc_delete_location( $value, 'bool' );
if ( ! $result ) {
$failed[] = absint( $value );
} else {
$deleted[] = absint( $value );
}
++$i;
}
if ( ! empty( $deleted ) ) {
/**
* Run when multiple locations are deleted.
*
* @hook mc_mass_delete_locations
*
* @param {array} $deleted Array of location IDs successfully deleted.
* @param {array} $failed Array of location IDs that were not successfully deleted.
*/
do_action( 'mc_mass_delete_locations', $deleted, $failed );
// Translators: Number of locations deleted, number selected.
$message = mc_show_notice( sprintf( __( '%1$d locations deleted successfully out of %2$d selected', 'my-calendar' ), count( $deleted ), $total ), false );
} else {
$message = mc_show_error( __( 'Your locations have not been deleted. Please investigate.', 'my-calendar' ), false );
}
echo wp_kses_post( $message );
}
}
/**
* Generate list of locations.
*/
function mc_manage_locations() {
global $wpdb;
$orderby = 'location_label';
$sortby = 'location';
if ( isset( $_GET['orderby'] ) ) {
$sortby = $_GET['orderby'];
switch ( $sortby ) {
case 'city':
$orderby = 'location_city';
break;
case 'state':
$orderby = 'location_state';
break;
case 'id':
$orderby = 'location_id';
break;
default:
$orderby = 'location_label';
}
}
$order = 'ASC';
$query_order = 'DESC';
if ( isset( $_GET['order'] ) ) {
switch ( $_GET['order'] ) {
case 'asc':
$order = 'DESC';
$query_order = 'ASC';
break;
case 'desc':
$order = 'ASC';
$query_order = 'DESC';
break;
default:
$order = 'ASC';
$query_order = 'DESC';
}
}
// Pull the locations from the database.
$items_per_page = 50;
$search = '';
$current = empty( $_GET['paged'] ) ? 1 : intval( $_GET['paged'] );
if ( isset( $_POST['mcl'] ) ) {
$query = esc_sql( $_POST['mcl'] );
$length = strlen( $query );
$db_type = mc_get_db_type();
if ( '' !== $query ) {
if ( 'MyISAM' === $db_type && $length > 3 ) {
/**
* Customize admin search MATCH columns when db is MyISAM.
*
* @hook mc_search_fields
*
* @param {string} $fields Comma-separated list of column names.
*
* @return {string}
*/
$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 ) ";
} else {
$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%' ";
}
} else {
$search = '';
}
}
$query_limit = ( ( $current - 1 ) * $items_per_page );
$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
$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
$items = $found_rows[0];
$pagination = '';
$num_pages = ceil( $items / $items_per_page );
if ( $num_pages > 1 ) {
$page_links = paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'prev_text' => __( '« Previous<span class="screen-reader-text"> Locations</span>', 'my-calendar' ),
'next_text' => __( 'Next<span class="screen-reader-text"> Locations</span> »', 'my-calendar' ),
'total' => $num_pages,
'current' => $current,
'mid_size' => 1,
)
);
$nav_label = esc_attr( __( 'Locations Pagination', 'my-calendar' ) );
$pagination = sprintf( "<nav class='tablenav' aria-label='$nav_label'><div class='tablenav-pages'>%s</div></nav>", $page_links );
}
if ( ! empty( $locations ) ) {
echo wp_kses_post( $pagination );
?>
<div class="mc-admin-header locations">
<div class='mc-search'>
<form action="<?php echo esc_url( admin_url( 'admin.php?page=my-calendar-location-manager' ) ); ?>" method="post" role='search'>
<div>
<input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce( 'my-calendar-nonce' ); ?>"/>
</div>
<div>
<label for="mc_search" class='screen-reader-text'><?php esc_html_e( 'Search Locations', 'my-calendar' ); ?></label>
<input type='text' name='mcl' id='mc_search' value='<?php echo ( isset( $_POST['mcl'] ) ) ? esc_attr( $_POST['mcl'] ) : ''; ?>'/>
<input type='submit' value='<?php esc_attr_e( 'Search', 'my-calendar' ); ?>' class='button-secondary' />
</div>
</form>
</div>
</div>
<form action="<?php echo esc_url( add_query_arg( $_GET, admin_url( 'admin.php' ) ) ); ?>" method="post">
<div><input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce( 'my-calendar-nonce' ); ?>"/></div>
<div class='mc-actions'>
<input type="submit" class="button-secondary delete" name="mass_delete" value="<?php esc_attr_e( 'Delete locations', 'my-calendar' ); ?>" />
<div class="mass-replace-wrap">
<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>
<div class="mass-replace-container">
<label for="mass-replace"><?php _e( 'Replacement ID', 'my-calendar' ); ?></label><input type="text" size="4" id="mass-replace" name="mass_replace_id" class="mass-replace" value="" />
<input type="submit" class="button-secondary delete" name="mass_replace" value="<?php _e( 'Replace', 'my-calendar' ); ?>" />
</div>
</div>
<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>
</div>
<table class="widefat striped page" id="my-calendar-admin-table">
<caption class="screen-reader-text"><?php esc_html_e( 'Location list. Use column headers to sort.', 'my-calendar' ); ?></caption>
<thead>
<tr>
<?php
$admin_url = admin_url( "admin.php?page=my-calendar-location-manager&paged=$current&order=" . strtolower( $order ) );
$url = add_query_arg( 'orderby', 'id', $admin_url );
$col_head = mc_table_header( __( 'ID', 'my-calendar' ), $order, $sortby, 'id', $url );
$url = add_query_arg( 'orderby', 'location', $admin_url );
$col_head .= mc_table_header( __( 'Location', 'my-calendar' ), $order, $sortby, 'location', $url );
$url = add_query_arg( 'orderby', 'city', $admin_url );
$col_head .= mc_table_header( __( 'City', 'my-calendar' ), $order, $sortby, 'city', $url );
$url = add_query_arg( 'orderby', 'state', $admin_url );
$col_head .= mc_table_header( __( 'State/Province', 'my-calendar' ), $order, $sortby, 'state', $url );
$col_head .= mc_table_header( __( 'Events', 'my-calendar' ), $order, $sortby, 'count', '' );
echo wp_kses( $col_head, mc_kses_elements() );
/**
* Add custom column table headers to Location Manager.
*
* @hook mc_location_manager_headers
*
* @param {string} $headers HTML output. Appends HTML in the end column of the location manager table row.
*
* @return {string}
*/
$headers = apply_filters( 'mc_location_manager_headers', '' );
echo $headers;
?>
</tr>
</thead>
<tbody>
<?php
$default_location = mc_get_option( 'default_location', '' );
if ( $default_location ) {
$default = mc_get_location( $default_location );
echo wp_kses( mc_location_manager_row( $default ), mc_kses_elements() );
}
foreach ( $locations as $loc ) {
if ( (int) $default_location === (int) $loc->location_id ) {
continue;
}
$location = mc_get_location( $loc->location_id );
echo wp_kses( mc_location_manager_row( $location ), mc_kses_elements() );
}
?>
</tbody>
</table>
<div class="mc-actions">
<p>
<input type="submit" class="button-secondary delete" name="mass_delete" value="<?php _e( 'Delete locations', 'my-calendar' ); ?>" />
</p>
</div>
</form>
<?php
} else {
if ( isset( $_POST['mcl'] ) ) {
echo '<p>' . esc_html__( 'No results found for your search query.', 'my-calendar' ) . '</p>';
}
echo '<p><a class="button" href="' . esc_url( admin_url( 'admin.php?page=my-calendar-locations' ) ) . '">' . __( 'Create a new location', 'my-calendar' ) . '</a></p>';
}
}
/**
* Verify that a location has valid fields. If a location has no valid data, delete it.
*
* @param object $location Location object.
*
* @return bool
*/
function mc_verify_location( $location ) {
if ( ! is_object( $location ) ) {
return true;
}
$location_id = $location->location_id;
$copy = clone $location;
// Unset location ID and location Post, which will always exist.
$copy->location_id = '';
$copy->location_post = '';
$json = json_encode( $copy );
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 ) {
if ( $location_id ) {
mc_delete_location( $location_id );
mc_location_delete_post( true, $location_id );
}
return false;
}
return true;
}
/**
* Generate the location manager row for a location.
*
* @param object $location Location object.
*
* @return string
*/
function mc_location_manager_row( $location ) {
$card = mc_hcard( $location, 'true', 'false', 'location' );
$verify = mc_verify_location( $location );
$count = mc_count_location_events( $location->location_id );
$filters = array(
'filter' => $location->location_id,
'restrict' => 'where',
);
$location_filter = add_query_arg( $filters, admin_url( 'admin.php?page=my-calendar-manage' ) );
if ( $count ) {
$count = '<a href="' . esc_url( $location_filter ) . '">' . $count . '</a>';
}
if ( ! $verify ) {
return '';
}
if ( (int) mc_get_option( 'default_location' ) === (int) $location->location_id ) {
$card = str_replace( '</strong>', ' ' . __( '(Default)', 'my-calendar' ) . '</strong>', $card );
$default = '<span class="mc_default">' . __( 'Default Location', 'my-calendar' ) . '</span>';
} else {
$mcnonce = wp_create_nonce( 'mcnonce' );
$url = add_query_arg( '_mcnonce', $mcnonce, admin_url( "admin.php?page=my-calendar-location-manager&default=$location->location_id" ) );
$default = '<a href="' . esc_url( $url ) . '">' . __( 'Set as Default', 'my-calendar' ) . '</a>';
}
$delete_url = admin_url( "admin.php?page=my-calendar-location-manager&mode=delete&location_id=$location->location_id" );
$view_url = get_the_permalink( mc_get_location_post( $location->location_id, false ) );
$edit_url = admin_url( "admin.php?page=my-calendar-locations&mode=edit&location_id=$location->location_id" );
$view_link = '';
if ( $view_url && esc_url( $view_url ) ) {
$view_link = "<a href='" . esc_url( $view_url ) . "' class='view' aria-describedby='location" . absint( $location->location_id ) . "'>" . esc_html__( 'View', 'my-calendar' ) . '</a> | ';
}
/**
* Add custom column table cells to Location Manager.
*
* @hook mc_location_manager_cells
*
* @param {string} $custom_location_cells HTML output. Appends HTML in the end column of the location manager table row.
* @param {object} $location Locatino object.
* @return {string}
*/
$custom_location_cells = apply_filters( 'mc_location_manager_cells', '', $location );
$row = '';
$row .= '
<tr>
<th scope="row">
<input type="checkbox" value="' . absint( $location->location_id ) . '" name="mass_edit[]" id="mc' . absint( $location->location_id ) . '"/>
<label for="mc' . absint( $location->location_id ) . '">' . $location->location_id . '</label>
</th>
<td>' . $card . '
<div class="row-actions">' . $view_link . '
<a href="' . esc_url( $edit_url ) . '" class="edit" aria-describedby="location' . absint( $location->location_id ) . '">' . esc_html__( 'Edit', 'my-calendar' ) . '</a> | ' . $default . ' |
<a href="' . esc_url( $delete_url ) . '" class="delete" aria-describedby="location' . absint( $location->location_id ) . '">' . esc_html__( 'Delete', 'my-calendar' ) . '</a>
</div>
</td>
<td>' . esc_html( $location->location_city ) . '</td>
<td>' . esc_html( $location->location_state ) . '</td>' . $custom_location_cells . '
<td>' . $count . '</td>
</tr>';
return $row;
}