'notice', 'priority' => 3, 'title' => __( 'Finish Setting Up Charitable', 'charitable' ), 'message' => '

' . esc_html__( "Looks like you started the setup wizard but didn't finish. Pick up where you left off and we'll have you ready to accept donations in just a couple more steps.", 'charitable' ) . '

', 'custom_css' => 'charitable-notification-type-notice', 'button_url' => add_query_arg( array( 'resume' => 'true' ), charitable_get_onboarding_url() ), 'button_text' => __( 'Resume Setup Wizard', 'charitable' ), ); update_option( 'charitable_dashboard_notifications', $notifications ); return; } if ( $present && ! $started ) { unset( $notifications['resume_setup_wizard'] ); update_option( 'charitable_dashboard_notifications', $notifications ); } } /** * Render the dashboard new page. * * @since 1.8.8 * @version 1.8.8.1 */ public function render_dashboard_new() { ?>

render_dashboard_header(); ?> render_dashboard_notifications(); ?>
render_stats_section(); ?> render_tabs_section(); ?> render_upgrade_section(); ?> render_quick_access_section(); } ?>
render_enhance_campaign_section(); ?> render_latest_updates_section(); ?> render_quick_access_section(); } ?>
render_dashboard_scripts(); ?>

1 ) : ?>
$notification ) : ?> array( 'href' => array(), 'title' => array(), ), 'strong' => array(), 'p' => array(), 'ol' => array(), 'ul' => array(), 'li' => array(), 'br' => array(), 'h1' => array(), 'h2' => array(), 'h3' => array(), 'h4' => array(), 'h5' => array(), ) ); $notification_type = ! empty( $notification['type'] ) ? $notification['type'] : 'notice'; $type_icons = array( 'notice' => 'dashicons-info', 'warning' => 'dashicons-warning', 'error' => 'dashicons-dismiss', ); $icon_class = isset( $type_icons[ $notification_type ] ) ? $type_icons[ $notification_type ] : 'dashicons-info'; $button_url = ! empty( $notification['button_url'] ) ? esc_url( $notification['button_url'] ) : ''; $button_text = ! empty( $notification['button_text'] ) ? sanitize_text_field( $notification['button_text'] ) : ''; ?>

*/ ?> get_date_range_for_period( $time_period ); ?>
render_stats_row(); ?>
get_dashboard_stats( $time_period ); ?>
%
%
clear_dashboard_stats_cache(); } // TEMPORARY TESTING: Return empty stats if testing mode is enabled if ( self::SHOW_EMPTY_DASHBOARD ) { // Decode HTML entities in currency-formatted strings to prevent double-encoding in JSON. $empty_total = html_entity_decode( charitable_format_money( 0 ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $empty_avg = html_entity_decode( charitable_format_money( 0 ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $empty_refunds = html_entity_decode( charitable_format_money( 0 ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); return array( 'total_donations' => $empty_total, 'donations_change' => '0%', 'avg_donations' => $empty_avg, 'avg_change' => '0%', 'total_donors' => 0, 'total_refunds' => $empty_refunds, 'total_refunds_count' => 0 ); } // Calculate dates based on time period $date_range = $this->get_date_range_for_period( $time_period ); $start_date = $date_range['start_date']; $end_date = $date_range['end_date']; $days = $date_range['days']; // Initialize Charitable Reports $charitable_reports = Charitable_Reports::get_instance(); $args = array( 'start_date' => $start_date, 'end_date' => $end_date, 'days' => $days, ); $charitable_reports->init_with_array( 'dashboard', $args ); // Get donations data $donations = $charitable_reports->get_donations(); $total_count_donations = count( $donations ); $total_amount_donations = $charitable_reports->get_donations_total(); $donation_average = ( $total_amount_donations > 0 && $total_count_donations > 0 ) ? ( charitable_format_money( $total_amount_donations / $total_count_donations, 2, true ) ) : charitable_format_money( 0 ); $donation_total = charitable_format_money( $total_amount_donations ); // Get donors count $total_count_donors = $charitable_reports->get_donors_count(); // Get refunds data $refunds_data = $charitable_reports->get_refunds(); $total_amount_refunds = charitable_format_money( $refunds_data['total_amount'] ); $total_count_refunds = $refunds_data['total_count']; // Decode HTML entities in currency-formatted strings to prevent double-encoding in JSON. // charitable_format_money() returns HTML entities (e.g., $ for $), so we decode them // before sending via AJAX to avoid double-encoding issues. $donation_total = html_entity_decode( $donation_total, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $donation_average = html_entity_decode( $donation_average, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $total_amount_refunds = html_entity_decode( $total_amount_refunds, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); // Calculate percentage changes $changes = $this->calculate_percentage_changes( $time_period, $total_amount_donations, $donation_average ); $donations_change = $changes['donations_change']; $avg_change = $changes['avg_change']; // Defensive guard: if current average is zero, hide avg change if ( 0.0 === (float) $this->parse_money_amount( $donation_average ) ) { $avg_change = ''; } // Allow filtering to disable percentage displays completely $show_percentage_changes = apply_filters( 'charitable_dashboard_show_percentage_changes', true ); if ( ! $show_percentage_changes ) { $donations_change = ''; $avg_change = ''; } return array( 'total_donations' => $donation_total, 'donations_change' => $donations_change, 'avg_donations' => $donation_average, 'avg_change' => $avg_change, 'total_donors' => $total_count_donors, 'total_refunds' => $total_amount_refunds, 'total_refunds_count' => $total_count_refunds ); } /** * Get date range for a given time period. * * @since 1.8.8 * @param string $time_period The time period. * @return array */ public function get_date_range_for_period( $time_period ) { /* * Site-local, not UTC: this range feeds Charitable_Reports::get_data(), * which filters on p.post_date (site-local time), so a bound built from * bare UTC excludes part of today's donations on any site whose * timezone is ahead of UTC. Every case below is anchored to the same * site-local "now" so the range is internally consistent. * * @since 1.8.13 */ $now = current_time( 'timestamp', 0 ); $end_date = gmdate( 'Y/m/d', $now ); switch ( $time_period ) { case 'last-7-days': $start_date = gmdate( 'Y/m/d', strtotime( '-7 days', $now ) ); $days = 7; break; case 'last-14-days': $start_date = gmdate( 'Y/m/d', strtotime( '-14 days', $now ) ); $days = 14; break; case 'last-30-days': $start_date = gmdate( 'Y/m/d', strtotime( '-30 days', $now ) ); $days = 30; break; case 'this-month': $start_date = gmdate( 'Y/m/d', strtotime( 'first day of this month', $now ) ); $days = gmdate( 'j', $now ); // Current day of month break; case 'last-3-months': $start_date = gmdate( 'Y/m/d', strtotime( '-3 months', $now ) ); $days = 90; break; case 'last-6-months': $start_date = gmdate( 'Y/m/d', strtotime( '-6 months', $now ) ); $days = 180; break; case 'last-year': $start_date = gmdate( 'Y/m/d', strtotime( '-1 year', $now ) ); $days = 365; break; default: $start_date = gmdate( 'Y/m/d', strtotime( '-7 days', $now ) ); $days = 7; break; } return array( 'start_date' => $start_date, 'end_date' => $end_date, 'days' => $days ); } /** * Calculate percentage changes for donations and average. * * @since 1.8.8 * @param string $time_period Current time period. * @param float $current_total Current total donations. * @param string $current_avg Current average donation. * @return array */ private function calculate_percentage_changes( $time_period, $current_total, $current_avg ) { $debug_no_cache = ( defined( 'CHARITABLE_DEBUG' ) && true === constant( 'CHARITABLE_DEBUG' ) ); // Get cache duration from filter (default 4 hours) $cache_duration = apply_filters( 'charitable_dashboard_stats_cache_duration', 4 * HOUR_IN_SECONDS ); // Create cache key $cache_key = 'charitable_dashboard_stats_changes_' . $time_period . '_' . gmdate( 'Y-m-d' ); // Try to get from cache first (unless debugging) if ( ! $debug_no_cache ) { $cached_changes = get_transient( $cache_key ); if ( false !== $cached_changes ) { return $cached_changes; } } // Calculate previous period date range $previous_period_range = $this->get_previous_period_date_range( $time_period ); if ( false === $previous_period_range ) { // If we can't calculate previous period, return no change $changes = array( 'donations_change' => '', 'avg_change' => '' ); if ( ! $debug_no_cache ) { set_transient( $cache_key, $changes, $cache_duration ); } return $changes; } // Get previous period data $previous_data = $this->get_previous_period_data( $previous_period_range ); if ( false === $previous_data ) { // If we can't get previous data, return no change $changes = array( 'donations_change' => '', 'avg_change' => '' ); if ( ! $debug_no_cache ) { set_transient( $cache_key, $changes, $cache_duration ); } return $changes; } // Calculate percentage changes $donations_change = $this->calculate_percentage_change( $previous_data['total'], $current_total ); $avg_change = $this->calculate_percentage_change( $previous_data['avg'], $this->parse_money_amount( $current_avg ) ); $changes = array( 'donations_change' => $donations_change, 'avg_change' => $avg_change ); // Cache the results unless debugging if ( ! $debug_no_cache ) { set_transient( $cache_key, $changes, $cache_duration ); } return $changes; } /** * Get previous period date range for comparison. * * @since 1.8.8 * @param string $time_period Current time period. * @return array|false */ private function get_previous_period_date_range( $time_period ) { $current_range = $this->get_date_range_for_period( $time_period ); $current_start = $current_range['start_date']; $current_days = $current_range['days']; // Calculate previous period dates $previous_end = gmdate( 'Y/m/d', strtotime( $current_start . ' -1 day' ) ); $previous_start = gmdate( 'Y/m/d', strtotime( $previous_end . ' -' . ( $current_days - 1 ) . ' days' ) ); return array( 'start_date' => $previous_start, 'end_date' => $previous_end, 'days' => $current_days ); } /** * Get previous period donation data. * * @since 1.8.8 * @param array $date_range Previous period date range. * @return array|false */ private function get_previous_period_data( $date_range ) { try { // Initialize Charitable Reports for previous period $charitable_reports = Charitable_Reports::get_instance(); $args = array( 'start_date' => $date_range['start_date'], 'end_date' => $date_range['end_date'], 'days' => $date_range['days'], ); $charitable_reports->init_with_array( 'dashboard', $args ); // Get donations data $donations = $charitable_reports->get_donations(); $total_count_donations = count( $donations ); $total_amount_donations = $charitable_reports->get_donations_total(); // Calculate average $donation_average = ( $total_amount_donations > 0 && $total_count_donations > 0 ) ? ( $total_amount_donations / $total_count_donations ) : 0; return array( 'total' => $total_amount_donations, 'avg' => $donation_average ); } catch ( Exception $e ) { // Log error and return false if ( charitable_is_debug() ) { error_log( 'Charitable Dashboard: Error getting previous period data - ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } return false; } } /** * Calculate percentage change between two values. * * @since 1.8.8 * @param float $previous Previous value. * @param float $current Current value. * @return string */ private function calculate_percentage_change( $previous, $current ) { // Handle edge cases: if either value is zero or negative, do not show a percentage change if ( $previous <= 0 || $current <= 0 ) { return ''; } $percentage = ( ( $current - $previous ) / $previous ) * 100; // Round to 1 decimal place $percentage = round( $percentage, 1 ); // Handle very large percentages if ( $percentage > 99.9 ) { return '+99.9%'; } if ( $percentage < -99.9 ) { return '-99.9%'; } // Format with + or - sign $sign = $percentage > 0 ? '+' : ''; return $sign . $percentage . '%'; } /** * Parse money amount from formatted string. * * @since 1.8.8 * @param string $money_formatted Formatted money string. * @return float */ private function parse_money_amount( $money_formatted ) { // Decode HTML entities first (e.g., $ becomes $) $money_formatted = html_entity_decode( $money_formatted, ENT_QUOTES, 'UTF-8' ); // Remove currency symbols and formatting $amount = preg_replace( '/[^\d.,]/', '', $money_formatted ); // Handle different decimal separators if ( strpos( $amount, ',' ) !== false && strpos( $amount, '.' ) !== false ) { // Both comma and dot present - assume comma is thousands separator $amount = str_replace( ',', '', $amount ); } elseif ( strpos( $amount, ',' ) !== false ) { // Only comma - could be decimal separator $amount = str_replace( ',', '.', $amount ); } return (float) $amount; } /** * Clear dashboard stats cache. * * @since 1.8.8 * @version 1.8.8.4 - added blog posts cache clearing */ public function clear_dashboard_stats_cache() { // Delete all dashboard stats cache transients global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( '_transient_charitable_dashboard_stats_changes_' ) . '%' ) ); // Also delete the transient timeout entries // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( '_transient_timeout_charitable_dashboard_stats_changes_' ) . '%' ) ); // Clear blog posts cache and Latest Updates feed cache. delete_transient( 'charitable_blog_posts_cache' ); delete_transient( 'charitable_latest_updates_cache' ); } /** * Get the default tab from URL parameter. * * @since 1.8.8 * @return string */ private function get_default_tab() { $valid_tabs = array( 'top-campaigns', 'latest-donations', 'top-donors', 'comments' ); // phpcs:disable WordPress.Security.NonceVerification.Recommended $default_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'top-campaigns'; // phpcs:enable WordPress.Security.NonceVerification.Recommended // Validate the tab parameter if ( ! in_array( $default_tab, $valid_tabs, true ) ) { $default_tab = 'top-campaigns'; } return $default_tab; } /** * Check if a tab should be active. * * @since 1.8.8 * @param string $tab_name The tab name to check. * @return bool */ private function is_tab_active( $tab_name ) { return $this->get_default_tab() === $tab_name; } /** * Render the tabs section with campaigns, donations, donors, and comments. * * @since 1.8.8 */ public function render_tabs_section() { ?>
render_top_campaigns_tab(); ?> render_latest_donations_tab(); ?> render_top_donors_tab(); ?> render_comments_tab(); ?>
get_top_campaigns(); ?>
Campaign Title Goal Raised Status Progress
%
apply_filters( 'charitable_dashboard_top_campaigns_amount', 5 ) ); $top_campaigns = Charitable_Campaigns::ordered_by_amount( $args ); $campaigns = array(); if ( ! empty( $top_campaigns->posts ) ) { foreach ( $top_campaigns->posts as $campaign ) { $_campaign = charitable_get_campaign( $campaign->ID ); if ( ! $_campaign ) { continue; } // Get campaign data (same as legacy) $donated_amount = $_campaign->get_donated_amount(); $donation_goal = $_campaign->get_goal(); $status = $_campaign->get_status(); $percent_donated = $_campaign->get_percent_donated_raw(); // Filter out draft campaigns if ( 'draft' === $status ) { continue; } // Cap percentage at 100% if ( $percent_donated > 100 ) { $percent_donated = 100; } // Map status to display format (same as legacy) $status_labels = array( 'active' => __( 'Active', 'charitable' ), 'completed' => __( 'Finished', 'charitable' ), 'failed' => __( 'Failed', 'charitable' ) ); $status_label = isset( $status_labels[ $status ] ) ? $status_labels[ $status ] : ucwords( $status ); // Map status for CSS class $status_class = $status; if ( 'completed' === $status ) { $status_class = 'finished'; } // Build campaign URL $url = charitable_disable_legacy_campaigns() ? admin_url( 'admin.php?page=charitable-campaign-builder&view=template&campaign_id=' . $campaign->ID ) : admin_url( 'post.php?post=' . $campaign->ID . '&action=edit' ); // Handle goal display for finished campaigns $goal_display = '-'; if ( $donation_goal ) { $goal_display = charitable_format_money( $donation_goal ); } elseif ( 'completed' === $status ) { // For finished campaigns without goals, show the total raised $goal_display = charitable_format_money( $donated_amount ); } $campaigns[] = array( 'title' => $campaign->post_title, 'url' => $url, 'goal' => $goal_display, 'reach' => charitable_format_money( $donated_amount ), 'status' => $status_class, 'status_label' => $status_label, 'progress' => round( $percent_donated ) ); } } // If no campaigns, return empty array return $campaigns; } /** * Render the latest donations tab content. * * @since 1.8.8 */ private function render_latest_donations_tab() { $donations = $this->get_latest_donations(); ?>
Donation Details Status
# Amount Donated: Method:
gmdate( 'Y/m/d', current_time( 'timestamp', 0 ) - ( 30 * DAY_IN_SECONDS ) ), 'end_date' => gmdate( 'Y/m/d', current_time( 'timestamp', 0 ) ), 'days' => 30, ); $charitable_reports->init_with_array( 'dashboard', $args ); $donations = $charitable_reports->get_donations(); $latest_donations = array(); $limit = apply_filters( 'charitable_dashboard_latest_donations_amount', 3 ); if ( ! empty( $donations ) ) { // Sort donations by date (newest first) usort( $donations, function( $a, $b ) { return strtotime( $b->post_date ) - strtotime( $a->post_date ); }); // Get valid donation statuses for mapping $statuses = charitable_get_valid_donation_statuses(); $count = 0; foreach ( $donations as $donation ) { if ( $count >= $limit ) { break; } $donation_id = intval( $donation->donation_id ); $donor_name = trim( esc_html( $donation->first_name ) . ' ' . esc_html( $donation->last_name ) ); $payment_method = strtolower( $donation->payment_gateway ); $donation_status = $donation->donation_status; $amount = charitable_format_money( $donation->amount ); $timestamp = human_time_diff( strtotime( $donation->post_date ), current_time( 'timestamp' ) ) . ' ago'; // Map status to display format $status_label = array_key_exists( $donation_status, $statuses ) ? $statuses[ $donation_status ] : ucwords( $donation_status ); // Map status for CSS class $status_class = $donation_status; if ( 'charitable-completed' === $donation_status ) { $status_class = 'paid'; } elseif ( strpos( $donation_status, 'charitable-' ) === 0 ) { $status_class = substr( $donation_status, 11 ); // Remove 'charitable-' prefix } // Determine payment method CSS class $method_class = ''; if ( 'stripe' === $payment_method ) { $method_class = 'donation-method-stripe'; } elseif ( 'paypal' === $payment_method ) { $method_class = 'donation-method-paypal'; } // Build donation URL $url = admin_url( 'post.php?post=' . $donation_id . '&action=edit' ); $latest_donations[] = array( 'id' => $donation_id, 'url' => $url, 'donor_name' => $donor_name, 'timestamp' => $timestamp, 'amount' => $amount, 'method' => ucwords( $payment_method ), 'method_class' => $method_class, 'status' => $status_class, 'status_label' => $status_label ); $count++; } } return $latest_donations; } /** * Render the top donors tab content. * * @since 1.8.8 */ private function render_top_donors_tab() { $donors = $this->get_top_donors(); ?>
Name Total Donations Status
<?php echo esc_attr( $donor['name'] ); ?>
$num_posts ) { if ( 'trash' === $status ) { continue; } if ( (int) $num_posts > 0 ) { $has_non_trashed_campaign = true; break 2; } } } } ?>
false, 'end_date' => false, 'days' => false, ); $charitable_reports->init_with_array( 'dashboard', $args ); // Get donations data first $charitable_reports->get_donations(); // Get top donors data (get more than needed to account for filtering) $top_donors = $charitable_reports->get_top_donors_overview( $limit * 2 ); if ( empty( $top_donors ) || ! is_array( $top_donors ) ) { return array(); } $donors = array(); $unique_donors = array(); $count = 0; foreach ( $top_donors as $donor_data ) { // Skip if we've already processed this donor if ( in_array( $donor_data->donor_id, $unique_donors, true ) ) { continue; } // Stop if we've reached the limit if ( $count >= $limit ) { break; } $unique_donors[] = $donor_data->donor_id; $count++; // Get donor name $donor_name = ! empty( $donor_data->donor_name ) ? $donor_data->donor_name : __( 'Unknown', 'charitable' ); // Get donor email and truncate if needed $donor_email = ! empty( $donor_data->donor_email ) ? $donor_data->donor_email : ''; $donor_email_display = $donor_email; if ( strlen( $donor_email ) > 30 ) { $donor_email_display = substr( $donor_email, 0, 30 ) . '...'; } // Get avatar (WordPress default) $donor_avatar = ! empty( $donor_data->donor_avatar_url ) ? $donor_data->donor_avatar_url : false; if ( false === $donor_avatar && function_exists( 'get_avatar_url' ) && ! empty( $donor_data->donor_email ) ) { $donor_avatar = get_avatar_url( $donor_data->donor_email, array( 'size' => 40, ) ); } // Determine donor type and badge $donation_parent = false; if ( ! empty( $donor_data->donation_parent_id ) ) { $donation = charitable_get_donation( $donor_data->donation_parent_id ); $donation_parent = ( $donation && is_object( $donation ) ) ? $donation->get( 'donation_type' ) : false; } $display_count = ! empty( $donor_data->total_donation_count ) ? intval( $donor_data->total_donation_count ) : 1; $badge_css = strtolower( $donation_parent ) === 'recurring' ? 'recurring' : 'one-time'; $badge_label = strtolower( $donation_parent ) === 'recurring' ? esc_html__( 'Recurring', 'charitable' ) : esc_html__( 'One-Time', 'charitable' ); // Override for multiple donations if ( $display_count > 1 ) { $badge_css = 'multiple'; $badge_label = esc_html__( 'Multiple', 'charitable' ); } // Format total amount $display_amount = ! empty( $donor_data->total_donation_amount ) ? charitable_format_money( $donor_data->total_donation_amount, 2, true ) : charitable_format_money( $donor_data->amount, 2, true ); $donors[] = array( 'name' => $donor_name, 'email' => $donor_email, 'email_display' => $donor_email_display, 'avatar' => $donor_avatar, 'total' => $display_amount, 'type' => $badge_css, 'type_label' => $badge_label, 'url' => admin_url( 'admin.php?page=charitable-donors' ) ); } return $donors; } /** * Render the comments tab content. * * @since 1.8.8 */ private function render_comments_tab() { ?>
render_comments_content(); ?>
get_donor_comments_addon_status(); switch ( $addon_status ) { case 'active': $this->render_comments_table(); break; case 'installed_inactive': $this->render_addon_inactive_message(); break; case 'not_installed_pro': $this->render_addon_not_installed_pro_message(); break; case 'not_installed_lite': default: $this->render_addon_not_installed_lite_message(); break; } } /** * Get donor comments addon status. * * @since 1.8.8 * @return string Status: 'active', 'installed_inactive', 'not_installed_pro', 'not_installed_lite' */ private function get_donor_comments_addon_status() { // Check if addon is active if ( class_exists( 'Charitable_Donor_Comments' ) ) { return 'active'; } // Check if addon is installed but not active $plugin_file = 'charitable-donor-comments/charitable-donor-comments.php'; if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin_file ) ) { return charitable_is_pro() ? 'installed_inactive' : 'not_installed_lite'; } // Addon not installed return charitable_is_pro() ? 'not_installed_pro' : 'not_installed_lite'; } /** * Render comments table with actual data. * * @since 1.8.8 */ private function render_comments_table() { $comments = $this->get_comments(); ?>
Comment Details Status
""
Approved

Charitable Pro to access this feature.', 'charitable' ), array( 'strong' => array() ) ); ?>

'charitable_comment', 'status' => 'all', // Include pending and approved 'number' => $limit, 'orderby' => 'comment_date', 'order' => 'DESC', 'post_type' => Charitable::CAMPAIGN_POST_TYPE, ) ); $formatted_comments = array(); if ( ! empty( $comments ) ) { foreach ( $comments as $comment ) { // Get comment author (handle anonymous) $author = get_comment_author( $comment ); if ( empty( $author ) ) { $author = __( 'Anonymous', 'charitable' ); } // Get comment text and sanitize it $text = get_comment_text( $comment ); if ( empty( $text ) ) { continue; // Skip comments without text } // Strip HTML tags except for bold and italic, then convert to proper formatting $allowed_tags = array( 'strong' => array(), 'b' => array(), 'em' => array(), 'i' => array(), ); $text = wp_kses( $text, $allowed_tags ); // Convert HTML tags to proper bold/italic formatting $text = str_replace( array( '', '', '', '' ), array( '', '', '', '' ), $text ); $text = str_replace( array( '', '', '', '' ), array( '', '', '', '' ), $text ); // Determine status $status = $comment->comment_approved ? 'approved' : 'pending'; $formatted_comments[] = array( 'id' => $comment->comment_ID, 'name' => $author, 'text' => $text, 'status' => $status, 'date' => $comment->comment_date, ); } } return $formatted_comments; } /** * AJAX handler for activating addons. * * @since 1.8.8 */ public function ajax_activate_addon() { // Verify nonce // phpcs:disable WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable-admin' ) ) { wp_die( esc_html( __( 'Security check failed', 'charitable' ) ) ); } // Check user capabilities if ( ! current_user_can( 'activate_plugins' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to activate plugins.', 'charitable' ) ) ); } $plugin = isset( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( empty( $plugin ) ) { wp_send_json_error( array( 'message' => __( 'Plugin not specified.', 'charitable' ) ) ); } // Check if plugin exists if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) { wp_send_json_error( array( 'message' => __( 'Plugin file not found.', 'charitable' ) ) ); } // Activate the plugin $result = activate_plugin( $plugin ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( array( 'message' => __( 'Addon activated successfully!', 'charitable' ) ) ); } /** * AJAX handler for approving comments. * * @since 1.8.8 */ public function ajax_approve_comment() { // Verify nonce // phpcs:disable WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable-admin' ) ) { wp_die( esc_html( __( 'Security check failed', 'charitable' ) ) ); } // Check user capabilities if ( ! current_user_can( 'moderate_comments' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to moderate comments.', 'charitable' ) ) ); } $comment_id = isset( $_POST['comment_id'] ) ? intval( wp_unslash( $_POST['comment_id'] ) ) : 0; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( empty( $comment_id ) ) { wp_send_json_error( array( 'message' => __( 'Comment ID not specified.', 'charitable' ) ) ); } // Check if comment exists $comment = get_comment( $comment_id ); if ( ! $comment ) { wp_send_json_error( array( 'message' => __( 'Comment not found.', 'charitable' ) ) ); } // Check if it's a charitable comment if ( 'charitable_comment' !== $comment->comment_type ) { wp_send_json_error( array( 'message' => __( 'Invalid comment type.', 'charitable' ) ) ); } // Approve the comment $result = wp_set_comment_status( $comment_id, 'approve' ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( array( 'message' => __( 'Comment approved successfully!', 'charitable' ) ) ); } /** * AJAX handler for deleting comments. * * @since 1.8.8 */ public function ajax_delete_comment() { // Verify nonce // phpcs:disable WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable-admin' ) ) { wp_die( esc_html( __( 'Security check failed', 'charitable' ) ) ); } // Check user capabilities if ( ! current_user_can( 'moderate_comments' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to moderate comments.', 'charitable' ) ) ); } $comment_id = isset( $_POST['comment_id'] ) ? intval( wp_unslash( $_POST['comment_id'] ) ) : 0; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( empty( $comment_id ) ) { wp_send_json_error( array( 'message' => __( 'Comment ID not specified.', 'charitable' ) ) ); } // Check if comment exists $comment = get_comment( $comment_id ); if ( ! $comment ) { wp_send_json_error( array( 'message' => __( 'Comment not found.', 'charitable' ) ) ); } // Check if it's a charitable comment if ( 'charitable_comment' !== $comment->comment_type ) { wp_send_json_error( array( 'message' => __( 'Invalid comment type.', 'charitable' ) ) ); } // Delete the comment (force delete) $result = wp_delete_comment( $comment_id, true ); if ( ! $result ) { wp_send_json_error( array( 'message' => __( 'Failed to delete comment.', 'charitable' ) ) ); } wp_send_json_success( array( 'message' => __( 'Comment deleted successfully!', 'charitable' ) ) ); } /** * Render the upgrade section. * * @since 1.8.8 */ public function render_upgrade_section() { // Don't show upgrade section if user already has Pro license if ( charitable_is_pro() ) { return; } ?>

render_upgrade_features_left(); ?>
render_upgrade_features_right(); ?>
Site Analysis tab, where the user runs their free Fundraising Score analysis. * * @since 1.8.12 * * @return void */ public function render_site_analysis_section() { $charitable_sa_url = admin_url( 'admin.php?page=charitable-reports&tab=site-analysis' ); $charitable_sa_nonce = wp_create_nonce( 'charitable_site_analysis_click' ); ?>

track_recommendation_click( 'clicked', 'site-analysis', 'feature', 'dashboard_site_analysis' ); wp_send_json_success(); } /** * Render the enhance campaign section. * * @since 1.8.8 */ public function render_enhance_campaign_section() { // Hide section if no items to show if ( ! $this->should_show_enhance_section() ) { return; } ?>

render_enhance_grid_items(); ?>
get_enhance_items(); foreach ( $items as $item ) : $button_attributes = ''; if ( isset( $item['button_action'] ) ) { $button_attributes = 'data-action="' . esc_attr( $item['button_action'] ) . '"'; $button_attributes .= ' data-slug="' . esc_attr( $item['slug'] ) . '"'; $button_attributes .= ' data-type="' . esc_attr( $item['type'] ) . '"'; // Add basename for third-party plugins and charitable addons if ( isset( $item['basename'] ) ) { $button_attributes .= ' data-basename="' . esc_attr( $item['basename'] ) . '"'; } // Add setup URL for third-party plugins (always available for setup state) if ( $item['type'] === 'third_party' && isset( $item['setup_url'] ) ) { $button_attributes .= ' data-setup-url="' . esc_url( $item['setup_url'] ) . '"'; } // Add upgrade URL for charitable addons if ( $item['type'] === 'charitable_addon' && isset( $item['upgrade_url'] ) ) { $button_attributes .= ' data-upgrade-url="' . esc_url( $item['upgrade_url'] ) . '"'; } // Add disabled state if ( isset( $item['button_disabled'] ) && $item['button_disabled'] ) { $button_attributes .= ' disabled'; } } ?>

get_curated_third_party_plugins(); $charitable_addons = $this->get_curated_charitable_addons(); // Get third-party plugin data $all_plugins = $this->get_third_party_plugin_data(); // Get first 3 available third-party plugins $available_third_party = 0; foreach ( $third_party_plugins as $slug ) { if ( $available_third_party >= 3 ) { break; } if ( isset( $all_plugins[ $slug ] ) ) { $plugin_data = $all_plugins[ $slug ]; if ( ! $plugin_data['active'] ) { // Show if not active (installed or not) $items[] = $this->format_plugin_item( $plugin_data, 'third_party' ); $available_third_party++; } } } // Fill remaining slots with Charitable addons $remaining_slots = 4 - count( $items ); $available_charitable = 0; foreach ( $charitable_addons as $slug ) { if ( $available_charitable >= $remaining_slots ) { break; } $addon_data = $this->get_charitable_addon_data( $slug ); if ( $addon_data && ! ( $addon_data['installed'] && $addon_data['active'] ) ) { $items[] = $this->format_addon_item( $addon_data ); $available_charitable++; } } return $items; } /** * AJAX handler for installing Charitable addons. * * @since 1.8.8 */ public function ajax_install_charitable_addon() { // Security check // phpcs:disable WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable-admin' ) ) { wp_send_json_error( 'Invalid nonce.' ); return; } // Permission check if ( ! current_user_can( 'install_plugins' ) ) { wp_send_json_error( 'Insufficient permissions.' ); return; } $slug = isset( $_POST['slug'] ) ? sanitize_text_field( wp_unslash( $_POST['slug'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( empty( $slug ) ) { wp_send_json_error( 'Addon slug is required.' ); return; } // Get addon data $addon_data = $this->get_charitable_addon_data( $slug ); if ( ! $addon_data ) { wp_send_json_error( 'Addon not found.' ); return; } // Check if already installed if ( $addon_data['installed'] ) { wp_send_json_error( 'Addon is already installed.' ); return; } // Check if download URL is available if ( ! isset( $addon_data['download_url'] ) || empty( $addon_data['download_url'] ) ) { wp_send_json_error( 'Download URL not available for this addon.' ); return; } // Download and install the addon $result = $this->install_charitable_addon_from_url( $addon_data['download_url'], $slug ); if ( is_wp_error( $result ) ) { wp_send_json_error( $result->get_error_message() ); return; } wp_send_json_success( array( 'message' => __( 'Addon installed successfully.', 'charitable' ), 'button_text' => __( 'Activate', 'charitable' ), 'button_class' => 'charitable-dashboard-v2-activate-button', 'button_action' => 'activate_addon' ) ); } /** * AJAX handler for activating Charitable addons. * * @since 1.8.8 */ public function ajax_activate_charitable_addon() { // Security check // phpcs:disable WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), 'charitable-admin' ) ) { wp_send_json_error( 'Invalid nonce.' ); return; } // Permission check if ( ! current_user_can( 'activate_plugins' ) ) { wp_send_json_error( 'Insufficient permissions.' ); return; } $slug = isset( $_POST['slug'] ) ? sanitize_text_field( wp_unslash( $_POST['slug'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( empty( $slug ) ) { wp_send_json_error( 'Addon slug is required.' ); return; } // Get addon data $addon_data = $this->get_charitable_addon_data( $slug ); if ( ! $addon_data ) { wp_send_json_error( 'Addon not found.' ); return; } // Check if installed if ( ! $addon_data['installed'] ) { wp_send_json_error( 'Addon is not installed.' ); return; } // Check if already active if ( $addon_data['active'] ) { wp_send_json_error( 'Addon is already active.' ); return; } // Activate the plugin $result = activate_plugin( $addon_data['basename'] ); if ( is_wp_error( $result ) ) { wp_send_json_error( 'Failed to activate addon: ' . $result->get_error_message() ); return; } wp_send_json_success( array( 'message' => __( 'Addon activated successfully.', 'charitable' ), 'button_text' => __( 'Installed', 'charitable' ), 'button_class' => 'charitable-dashboard-v2-installed-button', 'button_action' => 'installed', 'button_disabled' => true ) ); } /** * Get curated third-party plugins list. * * @since 1.8.8 * @return array */ private function get_curated_third_party_plugins() { return array( 'monsterinsights', // Analytics 'wpforms-lite', // Forms 'aioseo', // SEO 'duplicator', // Backup 'pushengage', // Notifications 'uncanny-automator', // Automation 'envira-gallery', // Gallery // Add more as you review ); } /** * Get custom SVG icons for plugins. * * @since 1.8.8 * @return array */ private function get_custom_plugin_icons() { return array( 'pushengage' => '', 'all-in-one-seo-pack' => ' ', 'duplicator' => ' ', // Add more custom SVG icons as needed ); } /** * Get curated Charitable addons list. * * @since 1.8.8 * @return array */ private function get_curated_charitable_addons() { return array( 'charitable-recurring', 'charitable-donortrust', 'charitable-gift-aid', // Add more as you review ); } /** * Get custom title overrides for Charitable addons. * * @since 1.8.8 * @return array */ private function get_charitable_addon_title_overrides() { return array( 'charitable-recurring' => __( 'Recurring Donations', 'charitable' ), 'charitable-donortrust' => __( 'DonorTrust', 'charitable' ), 'charitable-gift-aid' => __( 'Gift Aid', 'charitable' ), 'charitable-ambassadors' => __( 'Ambassadors', 'charitable' ), 'charitable-anonymous-donations' => __( 'Anonymous Donations', 'charitable' ), 'charitable-fee-relief' => __( 'Fee Relief', 'charitable' ), // Add more overrides as needed ); } /** * Get third-party plugin data. * * @since 1.8.8 * @return array */ private function get_third_party_plugin_data() { $plugins_class = Charitable_Admin_Plugins_Third_Party::get_instance(); return $plugins_class->get_plugins( false ); } /** * Get Charitable addon data. * * @since 1.8.8 * @param string $slug Addon slug. * @return array|false */ private function get_charitable_addon_data( $slug ) { // Debug: Output current plan and license info $addons_directory = Charitable_Addons_Directory::get_instance(); $current_plan = $addons_directory->get_current_plan_slug(); $licenses = charitable_get_licenses(); $is_pro = charitable_is_pro(); // First check if the addon is installed as a WordPress plugin $plugin_file = $slug . '/' . $slug . '.php'; $is_installed = file_exists( WP_PLUGIN_DIR . '/' . $plugin_file ); $is_active = is_plugin_active( $plugin_file ); // If not installed, try to get addon data from directory if ( ! $is_installed ) { $addons_directory = Charitable_Addons_Directory::get_instance(); $addon = $addons_directory->get_addon( $slug ); if ( ! $addon ) { // Check if user has access to this addon by checking if it's in the recommended addons $all_addons = $addons_directory->get_addons(); $has_access = false; // Check if addon is in recommended, licensed, or unlicensed categories foreach ( $all_addons as $category => $addons ) { foreach ( $addons as $addon_data ) { if ( isset( $addon_data['slug'] ) && $addon_data['slug'] === $slug ) { $has_access = true; $addon = $addon_data; break 2; } } } if ( ! $has_access ) { return false; } // Process the raw addon data to add status, action, and plugin_allow fields // This mimics what get_addon() does but for raw data from get_addons() $addon = $this->process_raw_addon_data( $addon, $slug ); } } else { // Create addon data for installed plugin $addon = array( 'name' => ucwords( str_replace( '-', ' ', $slug ) ), 'status' => $is_active ? 'active' : 'installed', 'action' => $is_active ? 'deactivate' : 'activate', 'path' => $plugin_file, 'plugin_allow' => true, 'upgrade_url' => '', // Installed plugin may not have directory metadata; default license to empty 'license' => array(), ); } // Get title with overrides $title_overrides = $this->get_charitable_addon_title_overrides(); if ( isset( $title_overrides[ $slug ] ) ) { $title = $title_overrides[ $slug ]; } else { $title = isset( $addon['name'] ) ? $addon['name'] : ucwords( str_replace( '-', ' ', $slug ) ); $title = preg_replace( '/^Charitable\s+/i', '', $title ); } // Get local icon $icon = $this->get_charitable_addon_icon( $slug ); $final_data = array( 'title' => $title, 'description' => $this->get_charitable_addon_description( $slug ), 'icon' => $icon, 'installed' => $addon['status'] === 'installed' || $addon['status'] === 'active', 'active' => $addon['status'] === 'active', 'download_url' => isset( $addon['download_link'] ) ? $addon['download_link'] : '', 'slug' => $slug, 'type' => 'charitable_addon', 'basename' => $addon['path'], 'status' => $addon['status'], 'action' => $addon['action'], 'plugin_allow' => $addon['plugin_allow'], 'upgrade_url' => $addon['upgrade_url'] ?? '', 'license' => isset( $addon['license'] ) ? (array) $addon['license'] : array(), ); return $final_data; } /** * Get Charitable addon description. * * @since 1.8.8.4 * @param string $slug Addon slug. * @return string Description. */ public function get_charitable_addon_description( $slug ) { $descriptions = array( // Charitable official addons 'charitable-recurring' => __( 'Let donors give automatically on a daily, weekly, monthly, or yearly schedule.', 'charitable' ), 'charitable-donortrust' => __( 'Build Trust and Boost Donations with Real-Time Social Proof.', 'charitable' ), 'charitable-gift-aid' => __( 'Allow UK donors to add Gift Aid and boost eligible donations by 25%.', 'charitable' ), 'charitable-anonymous-donations' => __( 'Give supporters the option to hide their name from public donations.', 'charitable' ), 'charitable-fee-relief' => __( 'Let donors optionally cover processing fees.', 'charitable' ), // Add more addon descriptions here as they are surfaced in the dashboard 'google-analytics-for-wordpress' => __( 'Get detailed insights into your donation traffic and campaign performance.', 'charitable' ), 'wpforms-lite' => __( 'Create custom donation forms and surveys to engage with supporters.', 'charitable' ), 'all-in-one-seo-pack' => __( 'Optimize your campaigns for search engines to reach more potential donors.', 'charitable' ), 'duplicator' => __( 'Safely backup and migrate your donation data and campaign content.', 'charitable' ), 'pushengage' => __( 'Send targeted push notifications to re-engage donors and drive contributions.', 'charitable' ), 'uncanny-automator' => __( 'Automate your donation workflows and connect with other fundraising tools.', 'charitable' ), 'envira-gallery' => __( 'Create beautiful photo galleries to showcase your charitable impact.', 'charitable' ), ); if ( isset( $descriptions[ $slug ] ) ) { return $descriptions[ $slug ]; } // Sensible default if an addon is missing a curated description return __( 'Enhance your campaign with powerful fundraising features.', 'charitable' ); } /** * Get Charitable addon icon from local assets. * * @since 1.8.8 * @param string $slug Addon slug. * @return string Icon URL or generic icon. */ private function get_charitable_addon_icon( $slug ) { // Map addon slugs to their base filenames (without extension) $icon_mapping = array( 'charitable-donortrust' => 'donortrust', 'charitable-gift-aid' => 'gift-aid', 'charitable-recurring' => 'recurring-donations', 'charitable-anonymous-donations' => 'anonymous-donations', 'charitable-fee-relief' => 'fee-relief', ); // Check if we have a local icon for this addon if ( isset( $icon_mapping[ $slug ] ) ) { $base_filename = $icon_mapping[ $slug ]; $base_path = charitable()->get_path( 'assets' ) . 'images/plugins/charitable/' . $base_filename; // First try SVG $svg_path = $base_path . '.svg'; if ( file_exists( $svg_path ) ) { $svg_content = file_get_contents( $svg_path ); return $svg_content ? $svg_content : $this->get_generic_plugin_icon(); } // Fallback to PNG $png_path = $base_path . '.png'; if ( file_exists( $png_path ) ) { $png_url = charitable()->get_path( 'directory', false ) . 'assets/images/plugins/charitable/' . $base_filename . '.png'; return '' . esc_attr( $base_filename ) . ''; } } // Fall back to generic puzzle piece icon return $this->get_generic_plugin_icon(); } /** * Get generic plugin icon SVG. * * @since 1.8.8 * @return string Generic icon SVG. */ private function get_generic_plugin_icon() { return ' '; } /** * Format plugin item for display. * * @since 1.8.8 * @param array $plugin_data Plugin data. * @param string $type Plugin type. * @return array */ private function format_plugin_item( $plugin_data, $type ) { $button_state = $this->get_button_state( $plugin_data, $type ); $item = array( 'title' => $plugin_data['title'], 'description' => $this->get_charitable_addon_description( $plugin_data['slug'] ), 'button_text' => $button_state['text'], 'button_class' => $button_state['class'], 'button_action' => $button_state['action'], 'icon' => $this->get_plugin_icon( $plugin_data ), 'slug' => $plugin_data['slug'], 'basename' => $plugin_data['basename'], 'type' => $type ); // Always add setup URL if available (for when button changes to setup state) if ( ! empty( $plugin_data['setup'] ) ) { $item['setup_url'] = $plugin_data['setup']; } return $item; } /** * Format addon item for display. * * @since 1.8.8 * @param array $addon_data Addon data. * @return array */ private function format_addon_item( $addon_data ) { $button_state = $this->get_button_state( $addon_data, 'charitable_addon' ); $item = array( 'title' => $addon_data['title'], 'description' => isset( $addon_data['description'] ) ? $addon_data['description'] : $this->get_charitable_addon_description( $addon_data['slug'] ), 'button_text' => $button_state['text'], 'button_class' => $button_state['class'], 'button_action' => $button_state['action'], 'icon' => $addon_data['icon'], 'slug' => $addon_data['slug'], 'type' => 'charitable_addon' ); // Add upgrade URL for upgrade buttons if ( $button_state['action'] === 'upgrade' && isset( $button_state['upgrade_url'] ) ) { $item['upgrade_url'] = $button_state['upgrade_url']; } // Add disabled state for installed buttons if ( isset( $button_state['disabled'] ) && $button_state['disabled'] ) { $item['button_disabled'] = true; } return $item; } /** * Install a Charitable addon from download URL. * * @param string $download_url The download URL for the addon. * @param string $slug The addon slug. * @return bool|WP_Error True on success, WP_Error on failure. */ private function install_charitable_addon_from_url( $download_url, $slug ) { // Include WordPress filesystem API if ( ! function_exists( 'download_url' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! function_exists( 'unzip_file' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } // Download the addon zip file $temp_file = download_url( $download_url ); if ( is_wp_error( $temp_file ) ) { return new WP_Error( 'download_failed', 'Failed to download addon: ' . $temp_file->get_error_message() ); } // Get the WordPress filesystem $wp_filesystem = $this->get_wp_filesystem(); if ( ! $wp_filesystem ) { unlink( $temp_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink return new WP_Error( 'filesystem_error', 'Unable to access WordPress filesystem.' ); } // Create the plugin directory $plugin_dir = WP_PLUGIN_DIR . '/' . $slug; if ( ! $wp_filesystem->is_dir( $plugin_dir ) ) { $wp_filesystem->mkdir( $plugin_dir ); } // Unzip the file $result = unzip_file( $temp_file, WP_PLUGIN_DIR ); // Clean up temp file unlink( $temp_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink if ( is_wp_error( $result ) ) { return new WP_Error( 'unzip_failed', 'Failed to extract addon: ' . $result->get_error_message() ); } return true; } /** * Get WordPress filesystem instance. * * @return WP_Filesystem_Base|false */ private function get_wp_filesystem() { global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } return $wp_filesystem; } /** * Process raw addon data to add status, action, and plugin_allow fields. * This mimics what get_addon() does but for raw data from get_addons(). * * @version 1.8.8.4 * * @param array $raw_addon The raw addon data from get_addons(). * @param string $slug The addon slug. * @return array The processed addon data. */ private function process_raw_addon_data( $raw_addon, $slug ) { // Check if plugin is installed $plugin_file = $slug . '/' . $slug . '.php'; $is_installed = file_exists( WP_PLUGIN_DIR . '/' . $plugin_file ); $is_active = $is_installed ? is_plugin_active( $plugin_file ) : false; $is_pro = charitable_is_pro(); $current_plan = Charitable_Addons_Directory::get_current_plan_slug(); $required_plans = isset( $raw_addon['license'] ) ? (array) $raw_addon['license'] : array(); $plan_allows = empty( $required_plans ) ? false : in_array( $current_plan, $required_plans, true ); // Determine status and action based on installation state and license if ( $is_active ) { $status = 'active'; $action = 'deactivate'; $plugin_allow = true; } elseif ( $is_installed ) { $status = 'installed'; $action = 'activate'; $plugin_allow = true; } else { $status = 'uninstalled'; // Check if user has a valid license and required plan before allowing install if ( $is_pro && $plan_allows ) { $action = 'install'; $plugin_allow = true; } else { $action = 'upgrade'; $plugin_allow = false; // Provide upgrade URL so the button can link to pricing $addons_directory = Charitable_Addons_Directory::get_instance(); if ( method_exists( $addons_directory, 'charitable_get_upgrade_link' ) ) { $raw_addon['upgrade_url'] = $addons_directory->charitable_get_upgrade_link(); } } } // Add the processed fields to the raw addon data $raw_addon['status'] = $status; $raw_addon['action'] = $action; $raw_addon['plugin_allow'] = $plugin_allow; $raw_addon['path'] = $plugin_file; return $raw_addon; } /** * Get button state for plugin/addon. * * @since 1.8.8 * @param array $item_data Item data. * @param string $type Item type. * @return array */ private function get_button_state( $item_data, $type = 'third_party' ) { if ( $type === 'third_party' ) { if ( ! $item_data['installed'] ) { return array( 'text' => __( 'Install & Activate', 'charitable' ), 'action' => 'install', 'class' => '' // Same as hardcoded ); } elseif ( ! $item_data['active'] ) { return array( 'text' => __( 'Activate', 'charitable' ), 'action' => 'activate', 'class' => 'charitable-dashboard-v2-activate-button' // Same as hardcoded ); } else { return array( 'text' => __( 'Setup', 'charitable' ), 'action' => 'setup', 'class' => 'charitable-dashboard-v2-setup-button' // New class for setup ); } } else { // Charitable addon // Use the action from addons directory switch ( $item_data['action'] ) { case 'upgrade': case 'license': return array( 'text' => __( 'Upgrade', 'charitable' ), 'action' => 'upgrade', 'class' => 'charitable-dashboard-v2-upgrade-button', 'upgrade_url' => $item_data['upgrade_url'] ); case 'install': return array( 'text' => 'Install & Activate', 'action' => 'install_addon', 'class' => '' ); case 'activate': return array( 'text' => __( 'Activate', 'charitable' ), 'action' => 'activate_addon', 'class' => 'charitable-dashboard-v2-activate-button' ); case 'deactivate': return array( 'text' => __( 'Installed', 'charitable' ), 'action' => 'installed', 'class' => 'charitable-dashboard-v2-installed-button', 'disabled' => true ); default: return array( 'text' => __( 'Upgrade', 'charitable' ), 'action' => 'upgrade', 'class' => 'charitable-dashboard-v2-upgrade-button', 'upgrade_url' => $item_data['upgrade_url'] ); } } } /** * Get plugin icon. * * @since 1.8.8 * @param array $plugin_data Plugin data. * @return string */ private function get_plugin_icon( $plugin_data ) { // Step 1: Try PNG icon from plugin data (check if file exists) if ( ! empty( $plugin_data['icon'] ) ) { $icon_url = $plugin_data['icon']; if ( filter_var( $icon_url, FILTER_VALIDATE_URL ) ) { // Convert URL to server path for file existence check $icon_path = $this->url_to_server_path( $icon_url ); if ( $icon_path && file_exists( $icon_path ) ) { return '' . esc_attr( $plugin_data['title'] ) . ''; } } } // Step 2: Try custom SVG icon $custom_icons = $this->get_custom_plugin_icons(); if ( isset( $custom_icons[ $plugin_data['slug'] ] ) ) { return $custom_icons[ $plugin_data['slug'] ]; } // Step 3: Try the 'icon' from plugin data even if file doesn't exist // (might be a different format or external URL) if ( ! empty( $plugin_data['icon'] ) ) { $icon_url = $plugin_data['icon']; if ( filter_var( $icon_url, FILTER_VALIDATE_URL ) ) { // Check if the URL actually exists $response = wp_remote_head( $icon_url, array( 'timeout' => 5 ) ); if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) { return '' . esc_attr( $plugin_data['title'] ) . ''; } } } // Step 4: Default puzzle piece icon return ' '; } /** * Convert URL to server file path. * * @since 1.8.8 * @param string $url The URL to convert. * @return string|false The server path or false if conversion fails. */ private function url_to_server_path( $url ) { // Get the site URL and uploads directory $site_url = get_site_url(); $upload_dir = wp_upload_dir(); // Check if it's a local URL if ( strpos( $url, $site_url ) === 0 ) { // Remove site URL from the beginning $relative_path = str_replace( $site_url, '', $url ); // Get the absolute path $absolute_path = ABSPATH . ltrim( $relative_path, '/' ); return $absolute_path; } // Check if it's a plugin URL $plugin_url = plugins_url(); if ( strpos( $url, $plugin_url ) === 0 ) { // Remove plugin URL from the beginning $relative_path = str_replace( $plugin_url, '', $url ); // Get the absolute path $absolute_path = WP_PLUGIN_DIR . ltrim( $relative_path, '/' ); return $absolute_path; } // Check if it's a theme URL $theme_url = get_template_directory_uri(); if ( strpos( $url, $theme_url ) === 0 ) { // Remove theme URL from the beginning $relative_path = str_replace( $theme_url, '', $url ); // Get the absolute path $absolute_path = get_template_directory() . ltrim( $relative_path, '/' ); return $absolute_path; } // Check if it's a content URL $content_url = content_url(); if ( strpos( $url, $content_url ) === 0 ) { // Remove content URL from the beginning $relative_path = str_replace( $content_url, '', $url ); // Get the absolute path $absolute_path = WP_CONTENT_DIR . ltrim( $relative_path, '/' ); return $absolute_path; } return false; } /** * Track recommendation interaction. * * @since 1.8.8 * @param string $action Action performed. * @param string $plugin_slug Plugin slug. * @param string $plugin_type Plugin type. * @param string $context Context of interaction. * @return void */ private function track_recommendation_click( $action, $plugin_slug, $plugin_type, $context = 'dashboard_enhance' ) { if ( ! charitable_get_usage_tracking_setting() ) { return; } $tracked_data = get_option( 'charitable_recommended_plugins_' . $action, array() ); $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $tracked_data[] = array( 'plugin' => $plugin_slug, 'type' => $plugin_type, 'context' => $context, 'timestamp' => time(), 'user_agent' => $user_agent, 'referrer' => wp_get_referer() ?: 'direct' ); // Keep only last 100 entries if ( count( $tracked_data ) > 100 ) { $tracked_data = array_slice( $tracked_data, -100 ); } update_option( 'charitable_recommended_plugins_' . $action, $tracked_data ); } /** * Check if enhance section should be shown. * * @since 1.8.8 * @return bool */ private function should_show_enhance_section() { $items = $this->get_enhance_items(); return count( $items ) > 0; // Hide if no items to show } /** * Render the latest updates section. * * @since 1.8.8 */ public function render_latest_updates_section() { // Try v2 feed first, fall back to legacy blog posts. $updates = $this->get_latest_updates(); $has_posts = ! empty( $updates ); if ( ! $has_posts ) { $posts = $this->get_blog_posts(); $has_posts = ! empty( $posts ) && ! $this->has_blog_posts_error( $posts ); } $footer_link_text = __( 'See More Updates', 'charitable' ); // Threshold resolves in order: feed meta.behind_threshold → WP filter → hardcoded default (3). $updates_behind = $this->get_updates_behind(); $threshold = 3; // isset (not ! empty) so a feed-supplied 0 correctly overrides to "always alert". if ( isset( $this->version_info['behind_threshold'] ) ) { $threshold = (int) $this->version_info['behind_threshold']; } /** * Filter the "behind versions" threshold above which the big update alert renders. * * @since 1.8.12 * * @param int $threshold Default 3, or value supplied by the feed. */ $threshold = (int) apply_filters( 'charitable_latest_updates_behind_threshold', $threshold ); $show_version_update = $updates_behind > $threshold; // Version-state pill resolution (mutually exclusive with the big alert). @since 1.8.12 // - on_latest : remote latest is known AND user is 0 behind. // - mildly_behind : 1+ behind but not past the threshold (shows a compact amber nudge). $latest_known_version = $this->get_latest_version(); $on_latest = ! empty( $latest_known_version ) && 0 === $updates_behind; $mildly_behind = ! empty( $latest_known_version ) && $updates_behind > 0 && ! $show_version_update; // If blog posts can't load and there's no version update to show, don't display the section at all if ( ! $has_posts && ! $show_version_update ) { return; } $section_class = 'charitable-dashboard-v2-section'; if ( ! $show_version_update ) { $section_class .= ' no-update-alert'; } ?>

Latest Updates

%s).", 'charitable' ), esc_html( charitable()->get_version() ) ), array( 'strong' => array() ) ); ?>
%1$s. Update now', 'charitable' ), esc_html( $versions_label ), esc_url( admin_url( 'plugins.php?s=charitable' ) ) ), array( 'strong' => array(), 'a' => array( 'href' => array() ) ) ); ?>

%s', esc_html( sprintf( _n( '%d version', '%d versions', $updates_behind, 'charitable' ), $updates_behind ) ) ); $update_link = sprintf( '%s', esc_url( admin_url( 'plugins.php?s=charitable' ) ), esc_html__( 'Update now', 'charitable' ) ); printf( wp_kses( /* translators: 1: versions-behind label (e.g. "4 versions"), 2: "Update now" link */ __( 'You are currently %1$s behind. Please upgrade Charitable to take advantage of the features and addons below. %2$s', 'charitable' ), array( 'strong' => array(), 'a' => array( 'href' => array() ) ) ), $version_label, $update_link ); ?>

render_latest_updates_items(); ?>
publish ) ? (int) $count_campaigns->publish : 0; $count_donations = wp_count_posts( 'donation' ); $total_donations = isset( $count_donations->{'charitable-completed'} ) ? (int) $count_donations->{'charitable-completed'} : 0; if ( $days < apply_filters( 'charitable_days_since_activated', 14 ) || $total_campaigns < 1 || $total_donations < 1 ) { return; } // Use the same transient as the top notice so dismissing either dismisses both. if ( get_transient( 'charitable_five-star-review_banner' ) ) { return; } } $feedback_url = 'https://wpcharitable.com/charitable-admin-review/'; $rating_url = 'https://wpcharitable.com/charitable-admin-review/'; ?>

site_url( '/_promo_videos/charitable_dashboard_updates_video_mini-widget.mp4' ), ); foreach ( $map as $keyword => $url ) { if ( false !== stripos( $title, $keyword ) ) { return $url; } } return ''; } /** * Return a local thumbnail URL for a given post title, if one exists. * * @param string $title Post title from the feed. * @return string URL or empty string. */ private function get_local_thumbnail_url( $title ) { $base = charitable()->get_path( 'assets', false ) . 'images/dashboard/'; $map = array( 'Mini Donation Widget' => site_url( '/_promo_videos/mini-widget-thumbnail.svg' ), 'Donations Feed' => site_url( '/_promo_videos/donations-feed-thumbnail.svg' ), 'Campaign Showcase' => $base . 'thumb-campaign-showcase.png', 'Campaign Updates' => $base . 'thumb-campaign-updates.svg', ); foreach ( $map as $keyword => $url ) { if ( false !== stripos( $title, $keyword ) ) { return $url; } } return ''; } /** * Return a local badge type for a given post title, if one should be overridden. * Values: 'pro', 'addon', or empty string. * * @param string $title Post title from the feed. * @return string Badge type or empty string. */ private function get_local_badge( $title ) { $pro_keywords = array( 'Mini Donation Widget', 'Donations Feed', 'Campaign Showcase' ); foreach ( $pro_keywords as $keyword ) { if ( false !== stripos( $title, $keyword ) ) { return 'pro'; } } $addon_keywords = array( 'Campaign Updates Addon', 'Campaigns Update Addon' ); foreach ( $addon_keywords as $keyword ) { if ( false !== stripos( $title, $keyword ) ) { return 'addon'; } } return ''; } /** * Fetch and return the v2 Latest Updates feed. * * @since 1.8.12 * @return array Array of update items, or empty array on failure. */ private function get_latest_updates() { $cached = get_transient( 'charitable_latest_updates_cache' ); if ( false !== $cached ) { if ( isset( $cached['version_info'] ) ) { $this->version_info = $cached['version_info']; } return $cached['updates'] ?? []; } $response = wp_remote_get( self::LATEST_UPDATES_SOURCE_URL, [ 'timeout' => 10, 'sslverify' => false, ] ); if ( is_wp_error( $response ) ) { return []; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return []; } $decoded = json_decode( $body, true ); if ( json_last_error() !== JSON_ERROR_NONE || empty( $decoded['updates'] ) ) { return []; } // Store version info from meta. if ( isset( $decoded['meta'] ) && is_array( $decoded['meta'] ) ) { $this->version_info = [ 'current_charitable_version_lite' => ! empty( $decoded['meta']['current_charitable_version_lite'] ) ? sanitize_text_field( $decoded['meta']['current_charitable_version_lite'] ) : '', 'current_charitable_version_pro' => ! empty( $decoded['meta']['current_charitable_version_pro'] ) ? sanitize_text_field( $decoded['meta']['current_charitable_version_pro'] ) : '', ]; } // Sanitize each update item. $updates = []; foreach ( $decoded['updates'] as $item ) { if ( empty( $item['title'] ) || empty( $item['url'] ) ) { continue; } $updates[] = [ 'id' => sanitize_key( $item['id'] ?? '' ), 'title' => sanitize_text_field( $item['title'] ), 'description' => sanitize_text_field( $item['description'] ?? '' ), 'url' => esc_url_raw( $item['url'] ), 'date' => sanitize_text_field( $item['date'] ?? '' ), 'badge' => sanitize_key( $item['badge'] ?? '' ), 'is_new' => ! empty( $item['is_new'] ), 'image' => ! empty( $item['image'] ) ? esc_url_raw( $item['image'] ) : '', ]; } // Cache for 24 hours. set_transient( 'charitable_latest_updates_cache', [ 'updates' => $updates, 'version_info' => $this->version_info, ], DAY_IN_SECONDS ); return $updates; } /** * Render the v2 Latest Updates items. * * @since 1.8.12 */ private function render_latest_updates_items() { $updates = $this->get_latest_updates(); if ( empty( $updates ) ) { // Fall back to legacy blog posts. $this->render_blog_posts(); return; } ?>
= 4 ) break; $count++; ?>

→

get_blog_posts(); // phpcs:disable WordPress.Security.NonceVerification.Recommended $is_simulating = isset( $_GET['charitable_simulate_api_failure'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['charitable_simulate_api_failure'] ) ); $clear_cache = isset( $_GET['charitable_clear_cache'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['charitable_clear_cache'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended ?>
Unable to load blog posts. Please check back later.
<?php echo esc_attr( $post['title'] ); ?>
10, 'sslverify' => false, 'user-agent' => charitable_get_default_user_agent(), ]; $response = wp_remote_get( self::BLOG_POSTS_SOURCE_URL, $args ); if ( is_wp_error( $response ) ) { return false; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return false; } $decoded = json_decode( $body, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { return false; } return $this->verify_blog_posts( $decoded ); } /** * Calculate relative time from a date string. * * @since 1.8.8.4 * * @param string $date_string Date string in format 'Y-m-d H:i:s'. * * @return string Relative time string (e.g., '2 days ago', '1 week ago'). */ private function calculate_relative_time( $date_string ) { if ( empty( $date_string ) ) { return ''; } $post_date = strtotime( $date_string ); $current_time = current_time( 'timestamp' ); $time_diff = $current_time - $post_date; // If the date is in the future, return empty string if ( $time_diff < 0 ) { return ''; } // Calculate relative time $minutes = floor( $time_diff / 60 ); $hours = floor( $time_diff / 3600 ); $days = floor( $time_diff / 86400 ); $weeks = floor( $time_diff / 604800 ); $months = floor( $time_diff / 2592000 ); $years = floor( $time_diff / 31536000 ); if ( $years > 0 ) { /* translators: %s: number of years */ return $years === 1 ? _x( '1 year ago', 'relative time', 'charitable' ) : sprintf( _nx( '%s year ago', '%s years ago', $years, 'relative time', 'charitable' ), number_format_i18n( $years ) ); } elseif ( $months > 0 ) { /* translators: %s: number of months */ return $months === 1 ? _x( '1 month ago', 'relative time', 'charitable' ) : sprintf( _nx( '%s month ago', '%s months ago', $months, 'relative time', 'charitable' ), number_format_i18n( $months ) ); } elseif ( $weeks > 0 ) { /* translators: %s: number of weeks */ return $weeks === 1 ? _x( '1 week ago', 'relative time', 'charitable' ) : sprintf( _nx( '%s week ago', '%s weeks ago', $weeks, 'relative time', 'charitable' ), number_format_i18n( $weeks ) ); } elseif ( $days > 0 ) { /* translators: %s: number of days */ return $days === 1 ? _x( '1 day ago', 'relative time', 'charitable' ) : sprintf( _nx( '%s day ago', '%s days ago', $days, 'relative time', 'charitable' ), number_format_i18n( $days ) ); } elseif ( $hours > 0 ) { /* translators: %s: number of hours */ return $hours === 1 ? _x( '1 hour ago', 'relative time', 'charitable' ) : sprintf( _nx( '%s hour ago', '%s hours ago', $hours, 'relative time', 'charitable' ), number_format_i18n( $hours ) ); } elseif ( $minutes > 0 ) { /* translators: %s: number of minutes */ return $minutes === 1 ? _x( '1 minute ago', 'relative time', 'charitable' ) : sprintf( _nx( '%s minute ago', '%s minutes ago', $minutes, 'relative time', 'charitable' ), number_format_i18n( $minutes ) ); } else { return _x( 'Just now', 'relative time', 'charitable' ); } } /** * Verify blog posts data before it is saved. * * @since 1.8.8 * * @param array $blog_posts Array of blog posts items to verify. * * @return array */ private function verify_blog_posts( $blog_posts ) { $data = []; if ( ! is_array( $blog_posts ) || empty( $blog_posts ) ) { return $data; } // Check if we have the expected structure if ( ! isset( $blog_posts['blog_posts'] ) || ! is_array( $blog_posts['blog_posts'] ) ) { return $data; } // Store version information if available if ( isset( $blog_posts['meta'] ) && is_array( $blog_posts['meta'] ) ) { $this->version_info = array( 'current_charitable_version_lite' => ! empty( $blog_posts['meta']['current_charitable_version_lite'] ) ? sanitize_text_field( $blog_posts['meta']['current_charitable_version_lite'] ) : '', 'current_charitable_version_pro' => ! empty( $blog_posts['meta']['current_charitable_version_pro'] ) ? sanitize_text_field( $blog_posts['meta']['current_charitable_version_pro'] ) : '', ); } foreach ( $blog_posts['blog_posts'] as $post ) { // Verify required fields if ( empty( $post['title'] ) || empty( $post['url'] ) ) { continue; } // Handle image data $image_data = array( 'placeholder' => true, 'svg' => '' ); if ( ! empty( $post['image'] ) && is_array( $post['image'] ) ) { // Handle placeholder as both boolean and string $placeholder_value = $post['image']['placeholder'] ?? true; if ( is_string( $placeholder_value ) ) { // Handle string values: 'false', '0', 'no' should be false $placeholder_value = in_array( strtolower( $placeholder_value ), ['false', '0', 'no', ''], true ) ? false : true; } $image_data['placeholder'] = (bool) $placeholder_value; if ( ! empty( $post['image']['img'] ) ) { $image_data['img'] = esc_url_raw( $post['image']['img'] ); } if ( ! empty( $post['image']['svg'] ) ) { $image_data['svg'] = $post['image']['svg']; // SVG is already sanitized HTML } } // Calculate timestamp from date field if available, otherwise use provided timestamp $calculated_timestamp = ''; if ( ! empty( $post['date'] ) ) { $calculated_timestamp = $this->calculate_relative_time( $post['date'] ); } elseif ( ! empty( $post['timestamp'] ) ) { $calculated_timestamp = sanitize_text_field( $post['timestamp'] ); } $data[] = array( 'title' => sanitize_text_field( $post['title'] ), 'url' => esc_url_raw( $post['url'] ), 'timestamp' => $calculated_timestamp, 'featured' => ! empty( $post['featured'] ) ? (bool) $post['featured'] : false, 'image' => $image_data, ); } return $data; } /** * Get blog posts from transient cache. * * @since 1.8.8 * * @return array */ private function get_blog_posts_from_cache() { $cached_data = get_transient( 'charitable_blog_posts_cache' ); if ( false === $cached_data ) { return []; } return (array) $cached_data; } /** * Cache blog posts data using transients. * * @since 1.8.8 * * @param array $blog_posts Blog posts data to cache. */ private function cache_blog_posts( $blog_posts ) { $data = [ 'update' => time(), 'blog_posts' => $blog_posts, 'version_info' => $this->version_info, ]; // Cache for 24 hours set_transient( 'charitable_blog_posts_cache', $data, DAY_IN_SECONDS ); } /** * Get blog posts data. * * @since 1.8.8 * @return array */ private function get_blog_posts() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $is_simulating = isset( $_GET['charitable_simulate_api_failure'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['charitable_simulate_api_failure'] ) ); $clear_cache = isset( $_GET['charitable_clear_cache'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['charitable_clear_cache'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended // If simulating API failure, always return error if ( $is_simulating ) { return $this->get_blog_posts_error(); } // Clear cache if requested if ( $clear_cache ) { $this->clear_blog_posts_cache(); } // Try to get from cache first $cached_data = $this->get_blog_posts_from_cache(); if ( ! empty( $cached_data ) && ! $clear_cache ) { // Restore version info from cache if ( isset( $cached_data['version_info'] ) ) { $this->version_info = $cached_data['version_info']; } return $cached_data['blog_posts'] ?? []; } // Fetch fresh data from API $blog_posts = $this->fetch_blog_posts_feed(); // If API fetch was successful, cache and return the data if ( false !== $blog_posts && ! empty( $blog_posts ) ) { $this->cache_blog_posts( $blog_posts ); return $blog_posts; } else { // API failed, show error return $this->get_blog_posts_error(); } } /** * Clear blog posts cache (for testing purposes). * * @since 1.8.8 */ private function clear_blog_posts_cache() { delete_transient( 'charitable_blog_posts_cache' ); } /** * Get blog posts error data when API fails and no cache is available. * * @since 1.8.8 * @return array */ private function get_blog_posts_error() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $is_simulating = isset( $_GET['charitable_simulate_api_failure'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['charitable_simulate_api_failure'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended $error_title = $is_simulating ? __( 'Unable to load blog posts (Simulated API Failure)', 'charitable' ) : __( 'Unable to load blog posts', 'charitable' ); return array( array( 'title' => $error_title, 'url' => '#', 'timestamp' => '', 'featured' => false, 'error' => true ) ); } /** * Check if blog posts contain an error. * * @since 1.8.8 * @param array $posts Blog posts array. * @return bool True if posts contain an error. */ private function has_blog_posts_error( $posts ) { if ( empty( $posts ) ) { return true; } foreach ( $posts as $post ) { if ( ! empty( $post['error'] ) ) { return true; } } return false; } /** * Calculate how many updates behind the user is. * * @since 1.8.8 * @return int Number of updates behind, or 0 if up to date or unable to determine. */ private function get_updates_behind() { // Get current installed version $current_version = charitable()->get_version(); // Get latest version from API $latest_version = $this->get_latest_version(); if ( empty( $latest_version ) ) { return 0; } return $this->compare_versions( $current_version, $latest_version ); } /** * Get the latest version from the API data. * * @since 1.8.8 * @return string Latest version or empty string if not available. */ private function get_latest_version() { if ( null === $this->version_info ) { // Try to get version info from cached data $cached_data = get_transient( 'charitable_blog_posts_cache' ); if ( false !== $cached_data && isset( $cached_data['version_info'] ) ) { $this->version_info = $cached_data['version_info']; } } if ( null === $this->version_info ) { return ''; } // For now, we're working with Lite version return ! empty( $this->version_info['current_charitable_version_lite'] ) ? $this->version_info['current_charitable_version_lite'] : ''; } /** * Compare two version strings and return how many updates behind. * * @since 1.8.8 * @param string $current Current version (e.g., "1.8.4"). * @param string $latest Latest version (e.g., "1.8.8"). * @return int Number of updates behind. */ private function compare_versions( $current, $latest ) { // Normalize versions by removing any non-numeric characters except dots $current = preg_replace( '/[^0-9.]/', '', $current ); $latest = preg_replace( '/[^0-9.]/', '', $latest ); // Split into parts $current_parts = array_map( 'intval', explode( '.', $current ) ); $latest_parts = array_map( 'intval', explode( '.', $latest ) ); // Ensure both arrays have the same length by padding with zeros $max_length = max( count( $current_parts ), count( $latest_parts ) ); $current_parts = array_pad( $current_parts, $max_length, 0 ); $latest_parts = array_pad( $latest_parts, $max_length, 0 ); // Compare each part for ( $i = 0; $i < $max_length; $i++ ) { if ( $latest_parts[ $i ] > $current_parts[ $i ] ) { // Calculate the difference $updates_behind = 0; for ( $j = $i; $j < $max_length; $j++ ) { $updates_behind += $latest_parts[ $j ] - $current_parts[ $j ]; } return $updates_behind; } elseif ( $latest_parts[ $i ] < $current_parts[ $i ] ) { // Current version is newer than latest (shouldn't happen) return 0; } } // Versions are equal return 0; } /** * Test version comparison logic (for debugging). * * @since 1.8.8 * @return void */ public function test_version_comparison() { // Test cases $test_cases = array( array( '1.8.4', '1.8.8', 4 ), array( '1.8.4.2', '1.8.8', 4 ), array( '1.8.7.5', '1.8.8', 1 ), array( '1.8.8', '1.8.8', 0 ), array( '1.8.9', '1.8.8', 0 ), array( '1.7.5', '1.8.2', 5 ), ); foreach ( $test_cases as $test ) { $result = $this->compare_versions( $test[0], $test[1] ); $status = ( $result === $test[2] ) ? 'PASS' : 'FAIL'; } } /** * Render the quick access section. * * @since 1.8.8 * @version 1.8.8.6 */ public function render_quick_access_section() { ?>

Quick Access

render_quick_access_items(); ?>
get_quick_access_items(); foreach ( $items as $item ) : ?>
array( 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, 'class' => true, 'aria-hidden' => true, 'role' => true, ), 'path' => array( 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin'=> true, ), 'rect' => array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'fill' => true, ), 'mask' => array( 'id' => true, 'style' => true, 'maskunits' => true, 'x' => true, 'y' => true, 'width' => true, 'height' => true, ), 'g' => array( 'mask' => true, ), ) ); ?>
__( 'Getting Started', 'charitable' ), 'url' => 'https://wpcharitable.com/start-here/?referrer=chariable-dashboard', 'icon' => ' ' ), array( 'title' => __( 'Support Ticket', 'charitable' ), 'url' => 'https://wpcharitable.com/support?referrer=chariable-dashboard', 'icon' => ' ' ), array( 'title' => __( 'Documentation', 'charitable' ), 'url' => 'https://wpcharitable.com/documentation?referrer=chariable-dashboard', 'icon' => ' ' ), array( 'title' => __( 'Upgrade To Pro', 'charitable' ), 'url' => 'https://wpcharitable.com/lite-vs-pro?referrer=chariable-dashboard', 'icon' => ' ' ) ); } /** * Render dashboard scripts. * * @since 1.8.8 */ public function render_dashboard_scripts() { $charitable_dashboard_legacy = Charitable_Dashboard_Legacy::get_instance(); $charitable_dashboard_legacy->generate_dashboard_report_html(); $donation_axis = $charitable_dashboard_legacy->get_donation_axis(); $date_axis = $charitable_dashboard_legacy->get_date_axis(); // Decode HTML entities in the donation axis data and extract numeric values if ( is_array( $donation_axis ) ) { $donation_axis = array_map( function( $value ) { $decoded = html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); if ( preg_match( '/(\d+\.?\d*)/', $decoded, $matches ) ) { return floatval( $matches[1] ); } return 0; }, $donation_axis ); } ?> get_currency_symbol(), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $decimal_count = $currency_helper->get_decimals(); ?>