Files

1537 lines
56 KiB
PHP

<?php
/**
* HSSM Frontend Class
*
* Handles front-end functionality including the slide creation dashboard
*
* @package Hi_School_Slide_Manager
* @since 1.0.0
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit( 'Direct access forbidden.' );
}
/**
* HSSM Frontend Class
*
* Frontend dashboard and slide management interface
*/
class HSSM_Frontend {
/**
* Constructor
*
* @since 1.0.0
*/
public function __construct() {
add_action( 'init', array( $this, 'init' ) );
}
/**
* Initialize frontend functionality
*
* @since 1.0.0
*/
public function init() {
// Register page template
add_filter( 'theme_page_templates', array( $this, 'register_page_template' ) );
add_filter( 'page_template', array( $this, 'load_page_template' ) );
add_filter( 'request', array( $this, 'keep_preview_page_query' ) );
// Handle page routing
add_action( 'template_redirect', array( $this, 'handle_page_routing' ) );
// AJAX handlers
add_action( 'wp_ajax_hssm_get_slides', array( $this, 'ajax_get_slides' ) );
add_action( 'wp_ajax_hssm_get_slide_for_edit', array( $this, 'ajax_get_slide_for_edit' ) );
add_action( 'wp_ajax_hssm_delete_slide', array( $this, 'ajax_delete_slide' ) );
add_action( 'wp_ajax_hssm_update_slide', array( $this, 'ajax_update_slide' ) );
add_action( 'wp_ajax_hssm_schedule_slide', array( $this, 'ajax_schedule_slide' ) );
add_action( 'wp_ajax_hssm_publish_slide', array( $this, 'ajax_publish_slide' ) );
add_action( 'wp_ajax_hssm_duplicate_slide', array( $this, 'ajax_duplicate_slide' ) );
add_action( 'wp_ajax_hssm_toggle_slide_status', array( $this, 'ajax_toggle_slide_status' ) );
add_action( 'wp_ajax_hssm_approve_slide', array( $this, 'ajax_approve_slide' ) );
// Notification Settings AJAX
add_action( 'wp_ajax_hssm_get_notification_settings', array( $this, 'ajax_get_notification_settings' ) );
add_action( 'wp_ajax_hssm_save_notification_settings', array( $this, 'ajax_save_notification_settings' ) );
// Frontend assets
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_filter( 'body_class', array( $this, 'add_tool_page_body_class' ) );
// Filter slides query on preview page
add_action( 'pre_get_posts', array( $this, 'filter_preview_slides_query' ) );
add_action( 'admin_init', array( $this, 'maybe_nest_plugin_pages' ) );
}
/**
* Register the page template in the template dropdown
*
* @since 1.0.0
* @param array $templates Array of page templates
* @return array Modified templates array
*/
public function register_page_template( $templates ) {
$templates['page-slide-dashboard.php'] = __( 'Slide Dashboard', 'hi-school-slide-manager' );
$templates['page-slide-preview.php'] = __( 'Slide Preview', 'hi-school-slide-manager' );
$templates['page-slide-confirmation.php'] = __( 'Slide Confirmation', 'hi-school-slide-manager' );
return $templates;
}
/**
* Load the custom page template
*
* @since 1.0.0
* @param string $template The path to the template
* @return string The modified template path
*/
public function load_page_template( $template ) {
global $post;
// Check if this is a page and has our template assigned
if ( is_page() ) {
$slug = get_page_template_slug( $post->ID );
if ( in_array( $slug, array( 'page-slide-dashboard.php', 'page-slide-preview.php', 'page-slide-confirmation.php' ), true ) ) {
// First try the theme directory
$theme_template = trailingslashit( get_template_directory() ) . $slug;
if ( file_exists( $theme_template ) ) {
return $theme_template;
}
// Fallback to our plugin templates
$plugin_template = HSSM_PLUGIN_DIR . 'templates/' . $slug;
if ( file_exists( $plugin_template ) ) {
return $plugin_template;
}
}
}
return $template;
}
/**
* Prevent ?hssm_slide= from hijacking the preview page.
*
* The slide CPT used query_var "hssm_slide", so that GET param turned
* the page request into a missing slide query and WordPress 404'd.
*
* @since 1.1.3
* @param array $query_vars Parsed request vars.
* @return array
*/
public function keep_preview_page_query( $query_vars ) {
if ( empty( $query_vars['pagename'] ) ) {
return $query_vars;
}
unset( $query_vars['hssm_slide'], $query_vars['hssm_slide_item'] );
if ( isset( $query_vars['name'] ) && isset( $query_vars['post_type'] ) && 'hssm_slide' === $query_vars['post_type'] ) {
unset( $query_vars['name'], $query_vars['post_type'] );
}
return $query_vars;
}
/**
* Handle frontend page routing
*
* @since 1.0.0
*/
public function handle_page_routing() {
global $post;
if ( ! is_page() || ! $post ) {
return;
}
$template = get_page_template_slug( $post->ID );
if ( ! in_array( $template, array( 'page-slide-dashboard.php', 'page-slide-preview.php' ), true ) ) {
return;
}
if ( ! $this->check_dashboard_access() ) {
$this->redirect_to_login();
}
}
/**
* Mark dashboard and preview as plugin tool pages for CSS.
*
* @since 1.1.5
* @param array $classes Body classes.
* @return array
*/
public function add_tool_page_body_class( $classes ) {
if ( ! is_page() ) {
return $classes;
}
$template = get_page_template_slug();
if ( in_array( $template, array( 'page-slide-dashboard.php', 'page-slide-preview.php' ), true ) ) {
$classes[] = 'hssm-tool-page';
}
return $classes;
}
/**
* Check if user has access to the dashboard
*
* @since 1.0.0
* @param bool $check_upload Whether to also check upload capabilities
* @return bool Access granted
*/
private function check_dashboard_access( $check_upload = false ) {
// Check if user is logged in
if ( ! is_user_logged_in() ) {
hssm_debug_log( 'Dashboard access denied: User not logged in' );
return false;
}
$user_id = get_current_user_id();
hssm_debug_log( 'Checking dashboard access for user ID: ' . $user_id );
// Check Members plugin capability FIRST
if ( function_exists( 'members_user_can' ) && members_user_can( $user_id, 'hssm_manage_slides' ) ) {
hssm_debug_log( 'Members plugin check: PASS (hssm_manage_slides)' );
if ( $check_upload ) {
$can_upload = current_user_can( 'upload_files' );
hssm_debug_log( 'Upload files capability check: ' . ($can_upload ? 'PASS' : 'FAIL') );
return $can_upload;
}
return true;
}
// Fallback to WordPress capabilities - be more permissive
$can_edit = current_user_can( 'edit_posts' ) || current_user_can( 'publish_posts' ) || current_user_can( 'manage_options' );
hssm_debug_log( 'WordPress capability check (edit_posts/publish_posts/manage_options): ' . ($can_edit ? 'PASS' : 'FAIL') );
if ( $check_upload ) {
$can_upload = current_user_can( 'upload_files' );
hssm_debug_log( 'Upload files capability check: ' . ($can_upload ? 'PASS' : 'FAIL') );
return $can_edit && $can_upload;
}
return $can_edit;
}
/**
* Redirect to login page
*
* @since 1.0.0
*/
private function redirect_to_login() {
$redirect = home_url( add_query_arg( array() ) );
wp_safe_redirect( wp_login_url( $redirect ) );
exit;
}
/**
* Enqueue frontend assets
*
* @since 1.0.0
*/
public function enqueue_assets() {
global $post;
// Only load on our dashboard or preview pages
if ( ! is_page() ) {
return;
}
$template_slug = get_page_template_slug( $post->ID );
$is_dashboard = ( $template_slug === 'page-slide-dashboard.php' );
$is_preview = ( $template_slug === 'page-slide-preview.php' );
if ( ! $is_dashboard && ! $is_preview ) {
return;
}
if ( ! $this->check_dashboard_access() ) {
return;
}
wp_enqueue_style( 'dashicons' );
// Enqueue styles (both pages need dashboard CSS)
wp_enqueue_style(
'hssm-dashboard',
HSSM_PLUGIN_URL . 'assets/css/dashboard.css',
array(),
HSSM_VERSION
);
// Dashboard page needs full JS; preview page only needs styles
if ( $is_dashboard ) {
// Enqueue scripts
wp_enqueue_script( 'jquery-ui-sortable' );
wp_enqueue_media(); // For image uploads
wp_enqueue_script(
'hssm-dashboard',
HSSM_PLUGIN_URL . 'assets/js/dashboard.js',
array( 'jquery', 'jquery-ui-sortable' ),
HSSM_VERSION,
true
);
// Localize script
wp_localize_script( 'hssm-dashboard', 'hssmDashboard', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'hssm_dashboard_nonce' ),
'previewUrl' => hssm_get_page_url_by_template( 'page-slide-preview.php', '/slide-preview/' ),
'strings' => array(
'save_success' => __( 'Slide saved successfully!', 'hi-school-slide-manager' ),
'save_error' => __( 'Error saving slide. Please try again.', 'hi-school-slide-manager' ),
'delete_confirm' => __( 'Are you sure you want to delete this slide?', 'hi-school-slide-manager' ),
'delete_success' => __( 'Slide deleted successfully!', 'hi-school-slide-manager' ),
'required_fields' => __( 'Please fill in all required fields.', 'hi-school-slide-manager' ),
'max_forms' => __( 'Maximum 5 slide forms allowed at once.', 'hi-school-slide-manager' ),
'preview_save_first' => __( 'Save the slide first, then preview it as a banner.', 'hi-school-slide-manager' ),
'approve_confirm' => __( 'Approve this slide?', 'hi-school-slide-manager' ),
'approve_success' => __( 'Slide approved.', 'hi-school-slide-manager' ),
),
) );
// Add sites data for JavaScript
wp_localize_script( 'hssm-dashboard', 'hssmSites', $this->get_available_sites() );
}
// Preview page specific JS
if ( $is_preview ) {
wp_enqueue_script( 'jquery-ui-datepicker' );
// Enqueue jQuery UI CSS (using a reliable CDN or local if available,
// but WP doesn't ship with a default jQuery UI theme CSS)
wp_enqueue_style(
'jquery-ui-style',
'https://code.jquery.com/ui/1.13.2/themes/smoothness/jquery-ui.css',
array(),
'1.13.2'
);
wp_enqueue_script(
'hssm-preview',
HSSM_PLUGIN_URL . 'assets/js/preview.js',
array( 'jquery', 'jquery-ui-datepicker' ),
HSSM_VERSION,
true
);
// Localize script for preview page too
wp_localize_script( 'hssm-preview', 'hssmPreview', array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'hssm_dashboard_nonce' ),
'strings' => array(
'approve_confirm' => __( 'Are you sure you want to approve this slide?', 'hi-school-slide-manager' ),
'approve_success' => __( 'Slide approved successfully!', 'hi-school-slide-manager' ),
'approve_error' => __( 'Error approving slide. Please try again.', 'hi-school-slide-manager' ),
),
) );
}
}
/**
* Sanitize slide data from form
*
* @since 1.0.0
* @param array $data Raw form data
* @return array Sanitized data
*/
private function sanitize_slide_data( $data ) {
hssm_debug_log( 'Raw form data received: ' . print_r( $data, true ) );
// Handle target_sites - it might come as an array or need to be extracted
$target_sites = array();
if ( isset( $data['target_sites'] ) ) {
if ( is_array( $data['target_sites'] ) ) {
$target_sites = $data['target_sites'];
} else {
// If it's a single value, make it an array
$target_sites = array( $data['target_sites'] );
}
}
// Handle buttons - decode JSON if it's a string
$buttons = $data['buttons'] ?? array();
if ( is_string( $buttons ) ) {
$decoded = json_decode( stripslashes( $buttons ), true );
if ( is_array( $decoded ) ) {
$buttons = $decoded;
}
}
$sanitized = array(
'title' => sanitize_text_field( $data['title'] ?? '' ),
'subtitle' => sanitize_text_field( $data['subtitle'] ?? '' ),
'content' => wp_kses_post( $data['content'] ?? '' ),
'buttons' => $this->sanitize_buttons( $buttons ),
'target_sites' => array_map( 'sanitize_text_field', $target_sites ),
'start_date' => sanitize_text_field( $data['start_date'] ?? '' ),
'end_date' => sanitize_text_field( $data['end_date'] ?? '' ),
'post_status' => in_array( $data['post_status'] ?? 'draft', array( 'draft', 'publish', 'future' ), true ) ? ( $data['post_status'] ?? 'draft' ) : 'draft',
'auto_publish' => ! empty( $data['auto_publish'] ),
'auto_unpublish' => ! empty( $data['auto_unpublish'] ),
'order' => intval( $data['order'] ?? 0 ),
'featured_image_id' => intval( $data['featured_image_id'] ?? 0 ),
'hide_all_text' => ! empty( $data['hide_all_text'] ) && $data['hide_all_text'] === '1',
);
hssm_debug_log( 'Sanitized slide data: ' . print_r( $sanitized, true ) );
hssm_debug_log( 'Target sites count: ' . count( $sanitized['target_sites'] ) );
return $sanitized;
}
/**
* Sanitize buttons array
*
* @since 1.0.0
* @param array $buttons Raw buttons data
* @return array Sanitized buttons
*/
private function sanitize_buttons( $buttons ) {
$sanitized = array();
if ( is_array( $buttons ) ) {
foreach ( $buttons as $button ) {
if ( ! empty( $button['text'] ) || ! empty( $button['url'] ) ) {
$sanitized[] = array(
'text' => sanitize_text_field( $button['text'] ?? 'Learn More' ),
'url' => esc_url_raw( $button['url'] ?? '' ),
);
}
}
}
// Ensure at least one button
if ( empty( $sanitized ) ) {
$sanitized[] = array( 'text' => 'Learn More', 'url' => '' );
}
return array_slice( $sanitized, 0, 5 ); // Max 5 buttons
}
/**
* Validate slide data
*
* @since 1.0.0
* @param array $data Sanitized slide data
* @return bool Is valid
*/
private function validate_slide_data( $data ) {
hssm_debug_log( 'Validating slide data: ' . print_r( $data, true ) );
// Required fields
if ( empty( $data['title'] ) ) {
hssm_debug_log( 'Validation failed: Missing title' );
return false;
}
// Validate target sites
if ( ! empty( $data['target_sites'] ) && is_array( $data['target_sites'] ) ) {
// Check if each site slug exists as a taxonomy term
foreach ( $data['target_sites'] as $site_slug ) {
$term = get_term_by( 'slug', $site_slug, 'hssm_site' );
if ( ! $term || is_wp_error( $term ) ) {
hssm_debug_log( 'Validation failed: Invalid site slug (not a taxonomy term): ' . $site_slug );
return false;
}
}
} else {
hssm_debug_log( 'Validation failed: No target sites selected or target_sites is not an array' );
hssm_debug_log( 'target_sites value: ' . print_r( $data['target_sites'] ?? 'NOT SET', true ) );
return false;
}
hssm_debug_log( 'Validation passed' );
return true;
}
/**
* Save slide meta data
*
* @since 1.0.0
* @param int $slide_id Slide ID
* @param array $data Slide data
*/
private function save_slide_meta( $slide_id, $data ) {
$meta_fields = array(
'_hssm_subtitle' => $data['subtitle'],
'_hssm_buttons' => $data['buttons'],
'_hssm_start_date' => $data['start_date'],
'_hssm_end_date' => $data['end_date'],
'_hssm_auto_publish' => $data['auto_publish'] ? '1' : '0',
'_hssm_auto_unpublish' => $data['auto_unpublish'] ? '1' : '0',
'_hssm_order' => $data['order'],
'_hssm_hide_all_text' => $data['hide_all_text'] ? '1' : '0',
);
foreach ( $meta_fields as $key => $value ) {
update_post_meta( $slide_id, $key, $value );
}
}
/**
* Save slide taxonomies
*
* @since 1.0.0
* @param int $slide_id Slide ID
* @param array $data Slide data
*/
private function save_slide_taxonomies( $slide_id, $data ) {
// Set target sites
if ( ! empty( $data['target_sites'] ) && is_array( $data['target_sites'] ) ) {
hssm_debug_log( 'Saving target sites for slide ' . $slide_id . ': ' . print_r( $data['target_sites'], true ) );
// Ensure we're using term slugs, not IDs
$term_slugs = array();
foreach ( $data['target_sites'] as $site_slug ) {
// Check if term exists, if not create it
$term = get_term_by( 'slug', $site_slug, 'hssm_site' );
if ( ! $term || is_wp_error( $term ) ) {
// Try to create the term if it doesn't exist
$term_result = wp_insert_term( $site_slug, 'hssm_site', array( 'slug' => $site_slug ) );
if ( ! is_wp_error( $term_result ) ) {
$term_slugs[] = $site_slug;
}
} else {
$term_slugs[] = $site_slug;
}
}
if ( ! empty( $term_slugs ) ) {
$result = wp_set_post_terms( $slide_id, $term_slugs, 'hssm_site' );
hssm_debug_log( 'wp_set_post_terms result: ' . print_r( $result, true ) );
} else {
hssm_debug_log( 'No valid term slugs to save' );
}
} else {
hssm_debug_log( 'No target sites to save for slide ' . $slide_id );
}
}
/**
* Format slide data for frontend display
*
* @since 1.0.0
* @param int $slide_id Slide ID
* @return array Formatted slide data
*/
private function format_slide_for_frontend( $slide_id ) {
$slide = get_post( $slide_id );
return array(
'id' => $slide->ID,
'title' => $slide->post_title,
'subtitle' => get_post_meta( $slide_id, '_hssm_subtitle', true ),
'content' => $slide->post_content,
'buttons' => get_post_meta( $slide_id, '_hssm_buttons', true ),
'featured_image' => get_the_post_thumbnail_url( $slide_id, 'medium' ),
'target_sites' => wp_get_post_terms( $slide_id, 'hssm_site', array( 'fields' => 'slugs' ) ),
'status' => $slide->post_status,
'created' => $slide->post_date,
);
}
/**
* AJAX handler for getting slides
*
* @since 1.0.0
*/
public function ajax_get_slides() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) ) {
wp_send_json_error( array( 'message' => 'Nonce verification failed' ) );
return;
}
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => 'You do not have permission to view slides.' ) );
return;
}
// Get pagination parameters
$page = isset( $_POST['page'] ) ? intval( $_POST['page'] ) : 1;
$per_page = isset( $_POST['per_page'] ) ? intval( $_POST['per_page'] ) : 25;
$offset = ( $page - 1 ) * $per_page;
// Get filter parameters
$status_filter = isset( $_POST['status_filter'] ) ? sanitize_text_field( $_POST['status_filter'] ) : '';
if ( 'scheduled' === $status_filter ) {
$status_filter = 'future';
}
$site_filter = isset( $_POST['site_filter'] ) ? sanitize_text_field( $_POST['site_filter'] ) : '';
$month_filter = isset( $_POST['month_filter'] ) ? sanitize_text_field( $_POST['month_filter'] ) : ''; // Format: YYYY-MM
$year_filter = isset( $_POST['year_filter'] ) ? intval( $_POST['year_filter'] ) : 0;
// Build query args
$args = array(
'post_type' => 'hssm_slide',
'post_status' => array( 'publish', 'draft', 'pending', 'future' ), // Include all statuses
'posts_per_page' => $per_page,
'offset' => $offset,
'orderby' => 'date',
'order' => 'DESC',
'tax_query' => array(),
'date_query' => array()
);
// Add status filter
if ( ! empty( $status_filter ) ) {
$args['post_status'] = array( $status_filter );
}
// Add site filter using taxonomy query
if ( ! empty( $site_filter ) ) {
// Get site term by slug
$site_term = get_term_by( 'slug', $site_filter, 'hssm_site' );
if ( $site_term && ! is_wp_error( $site_term ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'hssm_site',
'field' => 'term_id',
'terms' => $site_term->term_id,
);
}
}
// Add month/year filter using date query
if ( ! empty( $month_filter ) ) {
// Format: YYYY-MM
$date_parts = explode( '-', $month_filter );
if ( count( $date_parts ) === 2 && strlen( $date_parts[0] ) === 4 && strlen( $date_parts[1] ) === 2 ) {
$year = intval( $date_parts[0] );
$month = intval( $date_parts[1] );
if ( $year > 0 && $month >= 1 && $month <= 12 ) {
$args['date_query'][] = array(
'year' => $year,
'month' => $month,
);
}
}
} elseif ( $year_filter > 0 ) {
// If only year is specified
$args['date_query'][] = array(
'year' => $year_filter,
);
}
// Set relation if we have multiple taxonomy queries
if ( count( $args['tax_query'] ) > 1 ) {
$args['tax_query']['relation'] = 'AND';
} elseif ( empty( $args['tax_query'] ) ) {
// Remove empty tax_query to avoid unnecessary query conditions
unset( $args['tax_query'] );
}
// Remove empty date_query if no date filters were applied
if ( empty( $args['date_query'] ) ) {
unset( $args['date_query'] );
}
// Get total count for pagination
$count_args = $args;
$count_args['posts_per_page'] = -1;
$count_args['fields'] = 'ids';
unset( $count_args['offset'] );
$total_slides = count( get_posts( $count_args ) );
// Get slides
$slides = get_posts( $args );
$formatted_slides = array();
foreach ( $slides as $slide ) {
$formatted = $this->format_slide_for_dashboard( $slide );
if ( $formatted ) {
$formatted_slides[] = $formatted;
}
}
wp_send_json_success( array(
'slides' => $formatted_slides,
'total' => $total_slides,
'page' => $page,
'per_page' => $per_page,
'total_pages' => ceil( $total_slides / $per_page ),
'has_more' => ( $page * $per_page ) < $total_slides
) );
}
/**
* AJAX handler for loading a single slide into the dashboard edit form.
*
* Used when the slide is not already in the in-memory list (pagination, filters, deep link).
*
* @since 1.1.2
*/
public function ajax_get_slide_for_edit() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) ) {
wp_send_json_error( array( 'message' => 'Nonce verification failed' ) );
return;
}
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => 'You do not have permission to edit slides.' ) );
return;
}
if ( ! isset( $_POST['slide_id'] ) || '' === trim( (string) $_POST['slide_id'] ) ) {
wp_send_json_error( array( 'message' => 'Slide ID is required' ) );
return;
}
$slide_id = absint( $_POST['slide_id'] );
$slide = get_post( $slide_id );
if ( ! $slide || 'hssm_slide' !== $slide->post_type ) {
wp_send_json_error( array( 'message' => 'Slide not found' ) );
return;
}
if ( ! current_user_can( 'edit_post', $slide_id ) ) {
wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
return;
}
$formatted = $this->format_slide_for_dashboard( $slide );
if ( ! $formatted ) {
wp_send_json_error( array( 'message' => 'Slide not found' ) );
return;
}
wp_send_json_success( array( 'slide' => $formatted ) );
}
/**
* Format a slide for the dashboard list and edit form.
*
* @since 1.1.2
* @param int|WP_Post $slide Slide ID or post object.
* @return array|null Formatted slide data.
*/
private function format_slide_for_dashboard( $slide ) {
if ( is_numeric( $slide ) ) {
$slide = get_post( $slide );
}
if ( ! $slide instanceof WP_Post || 'hssm_slide' !== $slide->post_type ) {
return null;
}
$slide_meta = get_post_meta( $slide->ID );
$site_terms = wp_get_post_terms( $slide->ID, 'hssm_site', array( 'fields' => 'all' ) );
$site_names = array();
$site_slugs = array();
if ( ! empty( $site_terms ) && ! is_wp_error( $site_terms ) ) {
foreach ( $site_terms as $site_term ) {
$site_names[] = $site_term->name;
$site_slugs[] = $site_term->slug;
}
}
$status_display = ucfirst( $slide->post_status );
if ( 'pending' === $slide->post_status ) {
$status_display = 'Pending Approval';
} elseif ( 'future' === $slide->post_status ) {
$status_display = 'Scheduled';
}
$start_date = isset( $slide_meta['_hssm_start_date'][0] ) ? $slide_meta['_hssm_start_date'][0] : '';
$end_date = isset( $slide_meta['_hssm_end_date'][0] ) ? $slide_meta['_hssm_end_date'][0] : '';
return array(
'id' => $slide->ID,
'title' => $slide->post_title,
'subtitle' => isset( $slide_meta['_hssm_subtitle'][0] ) ? $slide_meta['_hssm_subtitle'][0] : '',
'content' => $slide->post_content,
'status' => $slide->post_status,
'status_display' => $status_display,
'sites' => $site_names,
'site_slugs' => $site_slugs,
'target_sites' => $site_names,
'start_date' => $this->format_dashboard_date( $start_date ),
'end_date' => $this->format_dashboard_date( $end_date ),
'auto_publish' => isset( $slide_meta['_hssm_auto_publish'][0] ) ? $slide_meta['_hssm_auto_publish'][0] : '',
'auto_unpublish' => isset( $slide_meta['_hssm_auto_unpublish'][0] ) ? $slide_meta['_hssm_auto_unpublish'][0] : '',
'featured_image' => (int) get_post_thumbnail_id( $slide->ID ),
'featured_image_url' => get_the_post_thumbnail_url( $slide->ID, 'thumbnail' ),
'created' => $slide->post_date,
'modified' => $slide->post_modified,
'buttons' => isset( $slide_meta['_hssm_buttons'][0] ) ? maybe_unserialize( $slide_meta['_hssm_buttons'][0] ) : array(),
'hide_all_text' => isset( $slide_meta['_hssm_hide_all_text'][0] ) && '1' === $slide_meta['_hssm_hide_all_text'][0],
);
}
/**
* Normalize stored dates for HTML date inputs (YYYY-MM-DD).
*
* @since 1.1.2
* @param string $value Stored date or datetime.
* @return string
*/
private function format_dashboard_date( $value ) {
if ( empty( $value ) ) {
return '';
}
if ( preg_match( '/^(\d{4}-\d{2}-\d{2})/', $value, $matches ) ) {
return $matches[1];
}
$timestamp = strtotime( $value );
if ( ! $timestamp ) {
return '';
}
return wp_date( 'Y-m-d', $timestamp );
}
/**
* AJAX handler for approving slides
*
* @since 1.0.0
*/
public function ajax_approve_slide() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) ) {
wp_send_json_error( array( 'message' => 'Nonce verification failed' ) );
return;
}
if ( ! isset( $_POST['slide_id'] ) || '' === trim( (string) $_POST['slide_id'] ) ) {
wp_send_json_error( array( 'message' => 'Slide ID is required' ) );
return;
}
$slide_id = intval( $_POST['slide_id'] );
$approve_action = isset( $_POST['approve_action'] ) ? sanitize_text_field( $_POST['approve_action'] ) : 'publish';
// Check if slide exists
$slide = get_post( $slide_id );
if ( ! $slide || $slide->post_type !== 'hssm_slide' ) {
wp_send_json_error( array( 'message' => 'Slide not found' ) );
return;
}
if ( ! $this->check_dashboard_access() ) {
wp_send_json_error( array( 'message' => 'Permission denied' ) );
return;
}
$slide_meta = get_post_meta( $slide_id );
$start_date = isset( $slide_meta['_hssm_start_date'][0] ) ? $slide_meta['_hssm_start_date'][0] : '';
if ( $approve_action === 'schedule' && ! empty( $start_date ) ) {
// Schedule for future publication
$post_data = array(
'ID' => $slide_id,
'post_status' => 'future',
'post_date' => $start_date . ' 00:00:00'
);
} else {
// Publish immediately
$post_data = array(
'ID' => $slide_id,
'post_status' => 'publish'
);
}
$result = wp_update_post( $post_data );
if ( $result ) {
hssm_debug_log( "Slide $slide_id approved with action: $approve_action" );
$notifications = new HSSM_Notifications();
$notifications->notify_creator_slide_approved( $slide_id );
$notifications->notify_itc_slide_approved( $slide_id );
wp_send_json_success( array(
'message' => 'Slide approved successfully',
'new_status' => $post_data['post_status']
) );
} else {
wp_send_json_error( array( 'message' => 'Failed to approve slide' ) );
}
}
/**
* AJAX handler for deleting slides
*
* @since 1.0.0
*/
public function ajax_delete_slide() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) ) {
wp_send_json_error( array( 'message' => 'Nonce verification failed' ) );
return;
}
if ( ! isset( $_POST['slide_id'] ) || '' === trim( (string) $_POST['slide_id'] ) ) {
wp_send_json_error( array( 'message' => 'Slide ID is required' ) );
return;
}
$slide_id = intval( $_POST['slide_id'] );
// Check if slide exists and user can delete it
$slide = get_post( $slide_id );
if ( ! $slide || $slide->post_type !== 'hssm_slide' ) {
wp_send_json_error( array( 'message' => 'Slide not found' ) );
}
if ( ! current_user_can( 'delete_post', $slide_id ) ) {
wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
}
// Delete the slide
$result = wp_delete_post( $slide_id, true );
if ( $result ) {
wp_send_json_success( array( 'message' => 'Slide deleted successfully!' ) );
} else {
wp_send_json_error( array( 'message' => 'Failed to delete slide' ) );
}
}
/**
* AJAX handler for updating slides
*
* @since 1.0.0
*/
public function ajax_update_slide() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) ) {
wp_send_json_error( array( 'message' => 'Nonce verification failed' ) );
return;
}
if ( ! isset( $_POST['slide_id'] ) || '' === trim( (string) $_POST['slide_id'] ) ) {
wp_send_json_error( array( 'message' => 'Slide ID is required' ) );
return;
}
$slide_id = intval( $_POST['slide_id'] );
// Check if slide exists and user can edit it
$slide = get_post( $slide_id );
if ( ! $slide || $slide->post_type !== 'hssm_slide' ) {
wp_send_json_error( array( 'message' => 'Slide not found' ) );
}
if ( ! current_user_can( 'edit_post', $slide_id ) ) {
wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
}
// Sanitize and validate input
$slide_data = $this->sanitize_slide_data( $_POST );
if ( ! $this->validate_slide_data( $slide_data ) ) {
wp_send_json_error( array( 'message' => 'Invalid slide data' ) );
}
// Get post status from request (default to draft if not specified)
$post_status = isset( $_POST['post_status'] ) ? sanitize_text_field( $_POST['post_status'] ) : 'draft';
// Validate post status
$allowed_statuses = array( 'draft', 'pending', 'publish', 'future' );
if ( ! in_array( $post_status, $allowed_statuses, true ) ) {
$post_status = 'draft';
}
// Update the slide post
$post_args = array(
'ID' => $slide_id,
'post_title' => $slide_data['title'],
'post_content' => $slide_data['content'],
'post_excerpt' => $slide_data['content'],
'post_status' => $post_status,
);
// If scheduling for future and start date is set, use that as post_date
if ( $post_status === 'future' && ! empty( $slide_data['start_date'] ) ) {
$post_args['post_date'] = $slide_data['start_date'] . ' 00:00:00';
$post_args['post_date_gmt'] = get_gmt_from_date( $post_args['post_date'] );
}
$result = wp_update_post( $post_args );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => 'Failed to update slide' ) );
}
// Update meta data
$this->save_slide_meta( $slide_id, $slide_data );
// Update taxonomies
$this->save_slide_taxonomies( $slide_id, $slide_data );
// Update featured image if provided
if ( ! empty( $slide_data['featured_image_id'] ) ) {
set_post_thumbnail( $slide_id, $slide_data['featured_image_id'] );
}
// Send approval notification if status is pending
if ( $post_status === 'pending' ) {
// Trigger 1 (variant): Notification to Editor when updated slide is scheduled for approval
$notifications = new HSSM_Notifications();
$notifications->notify_editor_slide_created( $slide_id, 'scheduled' );
}
$status_message = ( $post_status === 'pending' )
? 'Slide updated and scheduled for approval!'
: 'Slide updated successfully!';
wp_send_json_success( array(
'message' => $status_message,
'slide_data' => $this->format_slide_for_frontend( $slide_id )
) );
}
/**
* AJAX handler for scheduling a slide for approval
*
* @since 1.0.0
*/
public function ajax_schedule_slide() {
hssm_debug_log( 'Schedule slide AJAX called' );
hssm_debug_log( 'POST data: ' . print_r( $_POST, true ) );
// Check nonce
if ( ! check_ajax_referer( 'hssm_dashboard_nonce', 'nonce', false ) ) {
hssm_debug_log( 'Nonce verification failed' );
wp_send_json_error( array( 'message' => 'Security check failed' ) );
}
// Check user permissions
if ( ! current_user_can( 'edit_posts' ) ) {
hssm_debug_log( 'User permissions check failed' );
wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
}
// Sanitize and validate input
$slide_data = $this->sanitize_slide_data( $_POST );
if ( ! $this->validate_slide_data( $slide_data ) ) {
hssm_debug_log( 'Slide data validation failed' );
wp_send_json_error( array( 'message' => 'Invalid slide data' ) );
}
// Create slide with 'pending' status for approval
$post_args = array(
'post_title' => $slide_data['title'],
'post_content' => $slide_data['content'],
'post_excerpt' => $slide_data['content'],
'post_type' => 'hssm_slide',
'post_status' => 'pending',
'meta_input' => array(
'_hssm_scheduled_for_approval' => current_time( 'timestamp' ),
'_hssm_submitted_by' => get_current_user_id(),
)
);
$slide_id = wp_insert_post( $post_args );
if ( is_wp_error( $slide_id ) || ! $slide_id ) {
wp_send_json_error( array( 'message' => 'Failed to schedule slide' ) );
}
// Save meta data and taxonomies
$this->save_slide_meta( $slide_id, $slide_data );
$this->save_slide_taxonomies( $slide_id, $slide_data );
// Set featured image
if ( ! empty( $slide_data['featured_image_id'] ) ) {
set_post_thumbnail( $slide_id, $slide_data['featured_image_id'] );
}
// Send approval email notifications
// Trigger 1 (variant): Notification to Editor when slide is scheduled for approval
$notifications = new HSSM_Notifications();
$notifications->notify_editor_slide_created( $slide_id, 'scheduled' );
wp_send_json_success( array(
'message' => 'Slide scheduled for approval. Email notifications sent.',
'slide_id' => $slide_id
) );
}
/**
* AJAX handler for publishing a slide immediately
*
* @since 1.0.0
*/
public function ajax_publish_slide() {
// Check nonce
if ( ! check_ajax_referer( 'hssm_dashboard_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Security check failed' ) );
}
// Check user permissions
if ( ! current_user_can( 'publish_posts' ) ) {
wp_send_json_error( array( 'message' => 'Insufficient permissions' ) );
}
// Sanitize and validate input
$slide_data = $this->sanitize_slide_data( $_POST );
if ( ! $this->validate_slide_data( $slide_data ) ) {
wp_send_json_error( array( 'message' => 'Invalid slide data' ) );
}
// Create slide with 'publish' status
$post_args = array(
'post_title' => $slide_data['title'],
'post_content' => $slide_data['content'],
'post_excerpt' => $slide_data['content'],
'post_type' => 'hssm_slide',
'post_status' => 'publish',
'meta_input' => array(
'_hssm_published_immediately' => current_time( 'timestamp' ),
'_hssm_published_by' => get_current_user_id(),
)
);
$slide_id = wp_insert_post( $post_args );
if ( is_wp_error( $slide_id ) || ! $slide_id ) {
wp_send_json_error( array( 'message' => 'Failed to publish slide' ) );
}
// Save meta data and taxonomies
$this->save_slide_meta( $slide_id, $slide_data );
$this->save_slide_taxonomies( $slide_id, $slide_data );
// Set featured image
if ( ! empty( $slide_data['featured_image_id'] ) ) {
set_post_thumbnail( $slide_id, $slide_data['featured_image_id'] );
}
// Send approval/notification emails
// Trigger 1: Notification to Editor when slide is scheduled or published
$notifications = new HSSM_Notifications();
$notifications->notify_editor_slide_created( $slide_id, 'published' );
wp_send_json_success( array(
'message' => 'Slide published successfully. Approval notifications sent.',
'slide_id' => $slide_id
) );
}
/**
* Send email notifications for slide approval workflow
*
* @since 1.0.0
* @param int $slide_id The slide ID
* @param string $action The action taken ('scheduled' or 'published')
*/
private function send_approval_notification( $slide_id, $action ) {
// Use the new Notification class
$notifications = new HSSM_Notifications();
$notifications->notify_editor_slide_created( $slide_id, $action );
}
/**
* Get formatted text of target sites for a slide
*
* @since 1.0.0
* @param int $slide_id The slide ID
* @return string Formatted target sites text
*/
private function get_slide_target_sites_text( $slide_id ) {
$target_sites = get_post_meta( $slide_id, '_hssm_target_sites', true );
if ( empty( $target_sites ) || ! is_array( $target_sites ) ) {
return 'None selected';
}
$site_names = array();
foreach ( $target_sites as $site_id ) {
// Get site name - this could be enhanced to use actual site data
$site_names[] = ucfirst( str_replace( '-', ' ', $site_id ) );
}
return implode( ', ', $site_names );
}
/**
* Get available sites for targeting
*
* @since 1.0.0
* @return array Available sites data
*/
private function get_available_sites() {
// Use centralized method from HSSM_Main if available
if ( class_exists( 'HSSM_Main' ) && method_exists( 'HSSM_Main', 'get_companion_sites' ) ) {
$sites = HSSM_Main::get_companion_sites();
$formatted_sites = array();
foreach ( $sites as $site ) {
$formatted_sites[] = array(
'id' => $site['slug'],
'name' => $site['name']
);
}
return $formatted_sites;
}
// Fallback for safety
return array(
array(
'id' => 'hi-school',
'name' => 'Hi-School Pharmacy'
),
array(
'id' => 'one-stop',
'name' => 'One Stop Hardware'
),
array(
'id' => 'ace',
'name' => 'Ace Hardware'
)
);
}
/**
* Nest preview and confirmation pages under the slide dashboard.
*
* @since 1.1.3
*/
public function maybe_nest_plugin_pages() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$dashboard_query = new WP_Query(
array(
'post_type' => 'page',
'post_status' => array( 'publish', 'private' ),
'posts_per_page' => 1,
'no_found_rows' => true,
'meta_key' => '_wp_page_template',
'meta_value' => 'page-slide-dashboard.php',
)
);
if ( ! $dashboard_query->have_posts() ) {
wp_reset_postdata();
return;
}
$parent_id = (int) $dashboard_query->posts[0]->ID;
wp_reset_postdata();
$this->maybe_create_confirmation_page( $parent_id );
$child_templates = array( 'page-slide-preview.php', 'page-slide-confirmation.php' );
foreach ( $child_templates as $template ) {
$pages_query = new WP_Query(
array(
'post_type' => 'page',
'post_status' => array( 'publish', 'private' ),
'posts_per_page' => 20,
'no_found_rows' => true,
'meta_key' => '_wp_page_template',
'meta_value' => $template,
)
);
if ( ! $pages_query->have_posts() ) {
wp_reset_postdata();
continue;
}
foreach ( $pages_query->posts as $page ) {
if ( (int) $page->ID === $parent_id ) {
continue;
}
if ( (int) $page->post_parent !== $parent_id ) {
wp_update_post(
array(
'ID' => $page->ID,
'post_parent' => $parent_id,
)
);
}
}
wp_reset_postdata();
}
}
/**
* Create the ITC confirmation page when it is missing.
*
* @since 1.1.3
* @param int $parent_id Dashboard page ID.
*/
private function maybe_create_confirmation_page( $parent_id ) {
$existing = new WP_Query(
array(
'post_type' => 'page',
'post_status' => array( 'publish', 'private' ),
'posts_per_page' => 1,
'no_found_rows' => true,
'meta_key' => '_wp_page_template',
'meta_value' => 'page-slide-confirmation.php',
)
);
if ( $existing->have_posts() ) {
wp_reset_postdata();
return;
}
wp_reset_postdata();
wp_insert_post(
array(
'post_title' => 'Slide Confirmation',
'post_name' => 'slide-confirmation',
'post_status' => 'publish',
'post_type' => 'page',
'post_parent' => absint( $parent_id ),
'meta_input' => array(
'_wp_page_template' => 'page-slide-confirmation.php',
),
)
);
}
/**
* AJAX handler for duplicating slides
*
* @since 1.0.0
*/
public function ajax_duplicate_slide() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) || ! $this->check_dashboard_access() ) {
wp_send_json_error( array( 'message' => 'Unauthorized' ) );
return;
}
if ( ! isset( $_POST['slide_id'] ) || '' === trim( (string) $_POST['slide_id'] ) ) {
wp_send_json_error( array( 'message' => 'Slide ID is required' ) );
return;
}
$original_id = intval( $_POST['slide_id'] );
$original_post = get_post( $original_id );
if ( ! $original_post || $original_post->post_type !== 'hssm_slide' ) {
wp_send_json_error( array( 'message' => 'Slide not found' ) );
}
// Create new post
$new_post_data = array(
'post_title' => $original_post->post_title . ' (Copy)',
'post_content' => $original_post->post_content,
'post_status' => 'draft',
'post_type' => 'hssm_slide',
'post_author' => get_current_user_id(),
);
$new_id = wp_insert_post( $new_post_data );
if ( is_wp_error( $new_id ) ) {
wp_send_json_error( array( 'message' => 'Failed to duplicate slide' ) );
}
// Copy all meta fields
$meta_fields = get_post_meta( $original_id );
foreach ( $meta_fields as $key => $values ) {
foreach ( $values as $value ) {
add_post_meta( $new_id, $key, maybe_unserialize( $value ) );
}
}
wp_send_json_success( array( 'message' => 'Slide duplicated successfully', 'new_id' => $new_id ) );
}
/**
* AJAX handler for toggling slide status
*
* @since 1.0.0
*/
public function ajax_toggle_slide_status() {
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'hssm_dashboard_nonce' ) || ! $this->check_dashboard_access() ) {
wp_send_json_error( array( 'message' => 'Unauthorized' ) );
return;
}
if ( ! isset( $_POST['slide_id'] ) || '' === trim( (string) $_POST['slide_id'] ) ) {
wp_send_json_error( array( 'message' => 'Slide ID is required' ) );
return;
}
$slide_id = intval( $_POST['slide_id'] );
$post = get_post( $slide_id );
if ( ! $post || $post->post_type !== 'hssm_slide' ) {
wp_send_json_error( array( 'message' => 'Slide not found' ) );
}
// Toggle status
$new_status = ( $post->post_status === 'publish' ) ? 'draft' : 'publish';
$updated = wp_update_post( array(
'ID' => $slide_id,
'post_status' => $new_status
) );
if ( is_wp_error( $updated ) ) {
wp_send_json_error( array( 'message' => 'Failed to update slide status' ) );
}
wp_send_json_success( array(
'message' => 'Slide status updated successfully',
'new_status' => $new_status
) );
}
/**
* AJAX handler to get notification settings
*/
public function ajax_get_notification_settings() {
if ( ! check_ajax_referer( 'hssm_dashboard_nonce', 'nonce', false ) || ! $this->check_dashboard_access() ) {
wp_send_json_error( array( 'message' => 'Unauthorized' ) );
}
$settings = HSSM_Notifications::get_settings();
wp_send_json_success( $settings );
}
/**
* AJAX handler to save notification settings
*/
public function ajax_save_notification_settings() {
if ( ! check_ajax_referer( 'hssm_dashboard_nonce', 'nonce', false ) || ! $this->check_dashboard_access() ) {
wp_send_json_error( array( 'message' => 'Unauthorized' ) );
}
// Pass raw values to the class method which handles sanitization
$settings = array(
'editor_emails' => isset($_POST['editor_emails']) ? wp_unslash( $_POST['editor_emails'] ) : '',
'itc_emails' => isset($_POST['itc_emails']) ? wp_unslash( $_POST['itc_emails'] ) : ''
);
if ( HSSM_Notifications::save_settings( $settings ) ) {
wp_send_json_success( array( 'message' => 'Settings saved' ) );
} else {
// It might fail if no changes were made, which is fine, but we'll return success anyway or handle accordingly
wp_send_json_success( array( 'message' => 'Settings saved (or no changes)' ) );
}
}
/**
* Filter Slides Query on Preview Page
* We use pre_get_posts because it's more reliable at catching all queries.
*
* @since 1.0.0
* @param WP_Query $query The query object.
*/
public function filter_preview_slides_query( $query ) {
if ( is_admin() ) {
return;
}
// --- DEBUG ---
// hssm_debug_log('HSSM Query Filter Check: is_main=' . ($query->is_main_query()?'Y':'N') . ' post_type=' . print_r($query->get('post_type'), true));
// Only on preview page
$obj_id = get_queried_object_id();
if ( ! $obj_id ) {
global $post;
$obj_id = $post->ID ?? 0;
}
$template = get_page_template_slug( $obj_id );
if ( $template !== 'page-slide-preview.php' ) {
return;
}
// Don't filter the main query (the page itself)
if ( $query->is_main_query() ) {
return;
}
// Check if this query is intended for our slides.
// We look for queries that either already target hssm_slide
// OR queries from Divi loops that might need redirection.
$post_type = $query->get( 'post_type' );
// If it's a menu or something else we recognize, bail
if ( $query->get( 'post_type' ) === 'nav_menu_item' ) {
return;
}
// Force the query to slides for the preview
$query->set( 'post_type', 'hssm_slide' );
$query->set( 'posts_per_page', -1 );
$query->set( 'post_status', array( 'publish', 'future', 'pending' ) );
// Filter parameters from URL
$date_param = isset( $_GET['hssm_date'] ) ? sanitize_text_field( $_GET['hssm_date'] ) : current_time( 'Y-m-d' );
$site_param = isset( $_GET['hssm_site'] ) ? sanitize_text_field( $_GET['hssm_site'] ) : '';
// Apply Meta Query for Date (evergreen: no start/end = always show)
$meta_query = array(
'relation' => 'AND',
array(
'relation' => 'OR',
array(
'key' => '_hssm_start_date',
'value' => '',
'compare' => '=',
),
array(
'key' => '_hssm_start_date',
'value' => $date_param,
'compare' => '<=',
),
array(
'key' => '_hssm_start_date',
'compare' => 'NOT EXISTS',
),
),
array(
'relation' => 'OR',
array(
'key' => '_hssm_end_date',
'value' => $date_param,
'compare' => '>=',
),
array(
'key' => '_hssm_end_date',
'compare' => 'NOT EXISTS',
),
array(
'key' => '_hssm_end_date',
'value' => '',
'compare' => '=',
),
),
);
$query->set( 'meta_query', $meta_query );
// Apply Tax Query for Site
if ( ! empty( $site_param ) ) {
$tax_query = array(
array(
'taxonomy' => 'hssm_site',
'field' => 'slug',
'terms' => $site_param,
)
);
$query->set( 'tax_query', $tax_query );
}
// Ensure ordering matches
$query->set( 'orderby', 'meta_value_num' );
$query->set( 'meta_key', '_hssm_order' );
$query->set( 'order', 'ASC' );
$query->set( 'ignore_sticky_posts', true );
}
}