Files
2026-04-28 15:13:50 +02:00

1103 lines
40 KiB
PHP

<?php
/**
* WPConsent Admin Page Dashboard.
*
* @package WPConsent
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Dashboard page class.
*/
class WPConsent_Admin_Page_Dashboard extends WPConsent_Admin_Page {
use WPConsent_Banner_Preview;
/**
* Cached basic compliance items.
*
* @var array|null
*/
private $cached_basic_items;
/**
* Cached advanced compliance items.
*
* @var array|null
*/
protected $cached_advanced_items;
/**
* Cached docs slug map.
*
* @var array|null
*/
protected $cached_docs_by_slug;
/**
* Page slug.
*
* @var string
*/
public $page_slug = 'wpconsent';
/**
* Call this just to set the page title translatable.
*/
public function __construct() {
$this->page_title = __( 'Dashboard', 'wpconsent-cookies-banner-privacy-suite' );
parent::__construct();
}
/**
* Page hooks.
*
* @return void
*/
public function page_hooks() {
add_filter( 'wpconsent_admin_js_data', array( $this, 'banner_preview_scripts' ) );
}
/**
* Page content — prototype with placeholder data.
*
* @return void
*/
public function output_content() {
$this->alert_bar();
// Count actionable pending items (not upsells) to decide docs widget placement.
$all_items = $this->get_basic_items();
$advanced_items = $this->get_advanced_items();
$pending_count = 0;
foreach ( array_merge( $all_items, $advanced_items ) as $item ) {
if ( ! $item['earned'] && empty( $item['upsell'] ) ) {
$pending_count++;
}
}
// If 5+ actionable pending items, the score widget is tall — put docs on the right.
$docs_on_right = $pending_count >= 5;
?>
<div class="wpconsent-dashboard-grid">
<div class="wpconsent-dashboard-grid-col">
<?php $this->compliance_score_widget(); ?>
<?php if ( ! $docs_on_right ) : ?>
<?php $this->docs_widget(); ?>
<?php endif; ?>
</div>
<div class="wpconsent-dashboard-grid-col">
<?php wpconsent()->recommended_plugins->recommended_plugins_widget(); ?>
<?php $this->blog_feed_widget(); ?>
<?php $this->banner_preview_box(); ?>
<?php if ( $docs_on_right ) : ?>
<?php $this->docs_widget(); ?>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Alert bar — only shows time-sensitive conditions.
*
* @return void
*/
public function alert_bar() {
$alerts = $this->get_alerts();
if ( empty( $alerts ) ) {
return;
}
$visible = array_slice( $alerts, 0, 3 );
$overflow = count( $alerts ) - 3;
?>
<div class="wpconsent-alert-bar">
<?php foreach ( $visible as $alert ) : ?>
<div class="wpconsent-alert-bar-item" data-alert-key="<?php echo esc_attr( $alert['key'] ); ?>">
<span class="wpconsent-alert-bar-message"><?php echo esc_html( $alert['message'] ); ?></span>
<a href="<?php echo esc_url( $alert['url'] ); ?>" class="wpconsent-button wpconsent-button-small wpconsent-button-primary">
<?php echo esc_html( $alert['action'] ); ?>
</a>
<button type="button" class="wpconsent-alert-bar-dismiss" aria-label="<?php esc_attr_e( 'Dismiss', 'wpconsent-cookies-banner-privacy-suite' ); ?>">&times;</button>
</div>
<?php endforeach; ?>
<?php if ( $overflow > 0 ) : ?>
<div class="wpconsent-alert-bar-overflow">
<?php
/* translators: %d: number of additional alerts. */
printf( esc_html__( 'and %d more...', 'wpconsent-cookies-banner-privacy-suite' ), $overflow );
?>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Build the list of active alerts.
*
* Only time-sensitive and state-change conditions — not things
* already visible as pending score items.
*
* @return array
*/
protected function get_alerts() {
$alerts = array();
// Unreviewed inspector findings.
$pending = wpconsent()->inspector->get_pending_cookies();
if ( ! empty( $pending ) ) {
$alerts[] = array(
'key' => 'inspector_pending',
/* translators: %d: number of cookies awaiting review. */
'message' => sprintf( __( 'Inspector found %d cookies awaiting review.', 'wpconsent-cookies-banner-privacy-suite' ), count( $pending ) ),
'action' => __( 'Review', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-scanner&view=inspector' ),
);
}
// Scan overdue (>30 days).
$scan_data = wpconsent()->scanner->get_scan_data();
if ( ! empty( $scan_data['date'] ) ) {
$days_ago = (int) floor( ( time() - strtotime( $scan_data['date'] ) ) / DAY_IN_SECONDS );
if ( $days_ago > 30 ) {
$alerts[] = array(
'key' => 'scan_overdue',
/* translators: %d: number of days since last scan. */
'message' => sprintf( __( 'Your last scan was %d days ago. Your site may have changed.', 'wpconsent-cookies-banner-privacy-suite' ), $days_ago ),
'action' => __( 'Scan Now', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-scanner' ),
);
}
}
return $alerts;
}
/**
* Get the basic compliance items with real data.
*
* @return array
*/
protected function get_basic_items() {
if ( null !== $this->cached_basic_items ) {
return $this->cached_basic_items;
}
// Consent banner enabled (20 pts).
$banner_enabled = (bool) wpconsent()->settings->get_option( 'enable_consent_banner' );
// Script blocking enabled (20 pts).
$script_blocking = (bool) wpconsent()->settings->get_option( 'enable_script_blocking' );
$all_scripts = wpconsent()->script_blocker->get_all_scripts();
$scripts_count = 0;
foreach ( $all_scripts as $scripts ) {
$scripts_count += count( $scripts );
}
// Cookie policy configured (10 pts).
$policy_page_id = (int) wpconsent()->settings->get_option( 'cookie_policy_page' );
$policy_exists = $policy_page_id > 0 && 'publish' === get_post_status( $policy_page_id );
// Cookie categories configured (10 pts).
// Count user-configured cookies, excluding the auto-created wpconsent_preferences cookie.
$cookie_query = new WP_Query( array(
'post_type' => 'wpconsent_cookie',
'post_status' => 'publish',
'posts_per_page' => 1,
'fields' => 'ids',
'meta_query' => array(
'relation' => 'OR',
array(
'key' => 'wpconsent_cookie_id',
'value' => 'wpconsent_preferences',
'compare' => '!=',
),
array(
'key' => 'wpconsent_cookie_id',
'compare' => 'NOT EXISTS',
),
),
) );
$cookie_count = $cookie_query->found_posts;
$has_categories = $cookie_count > 0;
// Website scanned in last 30 days (5 pts).
$scan_data = wpconsent()->scanner->get_scan_data();
$scan_recent = false;
$scan_days = 0;
$scan_time = 0;
if ( ! empty( $scan_data['date'] ) ) {
$scan_time = strtotime( $scan_data['date'] );
$scan_days = (int) floor( ( time() - $scan_time ) / DAY_IN_SECONDS );
$scan_recent = $scan_days <= 30;
}
// Inspector run at least once (5 pts).
$inspector_run = (bool) wpconsent()->settings->get_option( 'inspector_completed', false );
// Scan findings reviewed (5 pts).
$pending_cookies = wpconsent()->inspector->get_pending_cookies();
$all_reviewed = empty( $pending_cookies );
// Google Consent Mode (5 pts) — only shown when Google services are detected.
$has_google_services = $this->has_google_services();
$gcm_enabled = (bool) wpconsent()->settings->get_option( 'google_consent_mode', true );
// Content blocking enabled (5 pts) — only shown when content-blockable services exist.
$has_embeddable_services = $this->has_embeddable_services();
$content_blocking = (bool) wpconsent()->settings->get_option( 'enable_content_blocking' );
$items = array(
array(
'label' => __( 'Consent banner enabled', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 20,
'earned' => $banner_enabled,
'detail' => $banner_enabled
? __( 'Your consent banner is active and showing to visitors.', 'wpconsent-cookies-banner-privacy-suite' )
: __( 'Enable a consent banner to inform visitors about cookies.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-banner' ),
'action_type' => 'toggle',
'action_key' => 'enable_consent_banner',
),
array(
'label' => __( 'Script blocking enabled', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 20,
'earned' => $script_blocking,
'detail' => $script_blocking
/* translators: %d: number of scripts being managed. */
? sprintf( __( '%d scripts managed.', 'wpconsent-cookies-banner-privacy-suite' ), $scripts_count )
: __( 'Block tracking scripts until visitors give consent.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-cookies' ),
'action_type' => 'toggle',
'action_key' => 'enable_script_blocking',
),
array(
'label' => __( 'Cookie policy configured', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 10,
'earned' => $policy_exists,
'detail' => $policy_exists
/* translators: %s: the cookie policy page title. */
? sprintf( __( 'Linked to "%s".', 'wpconsent-cookies-banner-privacy-suite' ), get_the_title( $policy_page_id ) )
: __( 'Set a cookie policy page so visitors can learn more.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-cookies' ) . '#cookie-policy-input',
'action_type' => 'generate_policy',
'action_key' => 'cookie_policy',
),
array(
'label' => __( 'Cookie categories configured', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 10,
'earned' => $has_categories,
'detail' => $has_categories
/* translators: %d: number of cookies documented. */
? sprintf( __( '%d cookies documented.', 'wpconsent-cookies-banner-privacy-suite' ), $cookie_count )
: __( 'Document cookies so visitors know what data is collected.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-cookies&view=cookies' ),
'action_type' => 'configure_cookies',
'action_key' => '',
'has_scan' => ! empty( $scan_data['date'] ),
),
array(
'label' => __( 'Website scanned recently', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 5,
'earned' => $scan_recent,
'detail' => $scan_recent
/* translators: %s: formatted date of last scan. */
? sprintf( __( 'Last scan: %s.', 'wpconsent-cookies-banner-privacy-suite' ), date_i18n( get_option( 'date_format' ), $scan_time ) )
: ( ! empty( $scan_data['date'] )
/* translators: %d: days since last scan. */
? sprintf( __( 'Last scan was %d days ago. Run a new scan to check for changes.', 'wpconsent-cookies-banner-privacy-suite' ), $scan_days )
: __( 'Scan your website to detect cookies and tracking scripts.', 'wpconsent-cookies-banner-privacy-suite' )
),
'url' => admin_url( 'admin.php?page=wpconsent-scanner' ),
'action_type' => 'link',
'action_key' => '',
),
array(
'label' => __( 'Inspector completed', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 5,
'earned' => $inspector_run,
'detail' => $inspector_run
? __( 'Inspector has been run to verify cookie detection.', 'wpconsent-cookies-banner-privacy-suite' )
: __( 'Run the Cookie Inspector to detect cookies in real-time.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-scanner&view=inspector' ),
'action_type' => 'link',
'action_key' => '',
),
array(
'label' => __( 'All findings reviewed', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 5,
'earned' => $all_reviewed && $inspector_run,
'detail' => $all_reviewed
? __( 'No pending findings to review.', 'wpconsent-cookies-banner-privacy-suite' )
/* translators: %d: number of cookies pending review. */
: sprintf( __( '%d cookies need categorization.', 'wpconsent-cookies-banner-privacy-suite' ), count( $pending_cookies ) ),
'url' => admin_url( 'admin.php?page=wpconsent-scanner&view=inspector' ),
'action_type' => 'link',
'action_key' => '',
),
);
// Content blocking — only add when embeddable services are registered.
if ( $has_embeddable_services ) {
$items[] = array(
'label' => __( 'Content blocking enabled', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 5,
'earned' => $content_blocking,
'detail' => $content_blocking
? __( 'Embedded content is blocked until visitors give consent.', 'wpconsent-cookies-banner-privacy-suite' )
: __( 'Block iframes and embeds like YouTube and Google Maps until consent is given.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-cookies' ) . '#enable_content_blocking',
'action_type' => 'toggle',
'action_key' => 'enable_content_blocking',
);
}
// Google Consent Mode — only add when Google services are present.
if ( $has_google_services ) {
$items[] = array(
'label' => __( 'Google Consent Mode', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 5,
'earned' => $gcm_enabled,
'detail' => $gcm_enabled
? __( 'Google services respect visitor consent choices.', 'wpconsent-cookies-banner-privacy-suite' )
: __( 'Let Google Analytics and Ads respect consent before tracking.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => admin_url( 'admin.php?page=wpconsent-cookies' ) . '#google_consent_mode',
'action_type' => 'toggle',
'action_key' => 'google_consent_mode',
);
}
$this->cached_basic_items = $items;
return $items;
}
/**
* Check if the site has Google services registered.
*
* Uses the same logic as the frontend: checks service taxonomy terms
* for slugs containing "google". Result is cached in a transient.
*
* @return bool
*/
protected function has_google_services() {
$cached = get_transient( 'wpconsent_has_google_services' );
if ( false !== $cached ) {
return 'yes' === $cached;
}
$services = get_terms( array(
'taxonomy' => 'wpconsent_category',
'hide_empty' => false,
'number' => 0,
) );
if ( is_wp_error( $services ) ) {
return false;
}
foreach ( $services as $service ) {
if ( false !== strpos( $service->slug, 'google' ) ) {
set_transient( 'wpconsent_has_google_services', 'yes', DAY_IN_SECONDS );
return true;
}
}
set_transient( 'wpconsent_has_google_services', 'no', DAY_IN_SECONDS );
return false;
}
/**
* Check if the site has services with embeddable content (iframes, videos, maps).
*
* Cross-references content-blocking providers from the script library with
* services actually registered in the taxonomy. Result is cached in a transient.
*
* @return bool
*/
protected function has_embeddable_services() {
$cached = get_transient( 'wpconsent_has_embeddable_services' );
if ( false !== $cached ) {
return 'yes' === $cached;
}
// Get provider keys that support content blocking (e.g., youtube, google-maps, vimeo).
$providers = wpconsent()->script_blocker->get_content_blocking_providers();
if ( empty( $providers ) ) {
set_transient( 'wpconsent_has_embeddable_services', 'no', DAY_IN_SECONDS );
return false;
}
// Check if any of these providers are registered as services on this site.
$services = get_terms( array(
'taxonomy' => 'wpconsent_category',
'hide_empty' => false,
'number' => 0,
) );
if ( is_wp_error( $services ) ) {
return false;
}
$provider_keys = array_keys( $providers );
foreach ( $services as $service ) {
if ( in_array( $service->slug, $provider_keys, true ) ) {
set_transient( 'wpconsent_has_embeddable_services', 'yes', DAY_IN_SECONDS );
return true;
}
}
set_transient( 'wpconsent_has_embeddable_services', 'no', DAY_IN_SECONDS );
return false;
}
/**
* Get value stats to show below the score label.
*
* @return array Array of stat strings.
*/
protected function get_value_stats() {
// Cookies documented.
$total_cookies = (int) wp_count_posts( 'wpconsent_cookie' )->publish;
// Days since activation.
$activated_data = get_option( 'wpconsent_activated', array() );
$activated = ! empty( $activated_data['wpconsent'] ) ? (int) $activated_data['wpconsent'] : time();
$days = (int) floor( ( time() - $activated ) / DAY_IN_SECONDS );
$stats = array(
/* translators: %d: number of cookies documented. */
sprintf( __( '%d cookies documented', 'wpconsent-cookies-banner-privacy-suite' ), $total_cookies ),
);
if ( $days > 0 ) {
/* translators: %d: number of days since plugin activation. */
$stats[] = sprintf( __( 'Active for %d days', 'wpconsent-cookies-banner-privacy-suite' ), $days );
}
return $stats;
}
/**
* Compliance score widget.
*
* @return void
*/
public function compliance_score_widget() {
$all_items = $this->get_basic_items();
$advanced_items = $this->get_advanced_items();
// Split into earned and pending for both sections.
$pending_basics = array_filter( $all_items, function ( $item ) {
return ! $item['earned'];
} );
$earned_basics = array_filter( $all_items, function ( $item ) {
return $item['earned'];
} );
$pending_advanced = array_filter( $advanced_items, function ( $item ) {
return ! $item['earned'];
} );
$earned_advanced = array_filter( $advanced_items, function ( $item ) {
return $item['earned'];
} );
// Combine all earned for the toggle count.
$all_earned = array_merge( $earned_basics, $earned_advanced );
$all_total = array_merge( $all_items, $advanced_items );
$earned_count = count( $all_earned );
$total_count = count( $all_total );
// Calculate score.
// Lite: score is based on base items only, scaled to 75% max.
// Pro: score is based on all items out of 100%.
$has_upsells = false;
foreach ( $advanced_items as $item ) {
if ( ! empty( $item['upsell'] ) ) {
$has_upsells = true;
break;
}
}
if ( $has_upsells ) {
// Lite: only base items count. Upsell items appear in the UI but don't affect the score.
// Scaling to 75 means a fully configured Lite user always reaches exactly 75%.
$base_earned = 0;
$base_total = 0;
foreach ( $all_items as $item ) {
$base_total += $item['points'];
if ( $item['earned'] ) {
$base_earned += $item['points'];
}
}
$percentage = $base_total > 0 ? (int) round( ( $base_earned / $base_total ) * 75 ) : 0;
} else {
// Pro: all items count toward a 100% score.
$earned_points = 0;
$total_points = 0;
foreach ( array_merge( $all_items, $advanced_items ) as $item ) {
$total_points += $item['points'];
if ( $item['earned'] ) {
$earned_points += $item['points'];
}
}
$percentage = $total_points > 0 ? (int) round( ( $earned_points / $total_points ) * 100 ) : 0;
}
// Status label and color.
if ( $percentage >= 90 ) {
$label = __( 'Excellent. All recommended features enabled.', 'wpconsent-cookies-banner-privacy-suite' );
$color = 'green';
} elseif ( $percentage >= 70 ) {
$label = __( 'Good. Core setup done, more features available.', 'wpconsent-cookies-banner-privacy-suite' );
$color = 'blue';
} elseif ( $percentage >= 50 ) {
$label = __( 'Getting there. A few steps to complete.', 'wpconsent-cookies-banner-privacy-suite' );
$color = 'yellow';
} else {
$label = __( 'Just getting started. Let\'s set things up.', 'wpconsent-cookies-banner-privacy-suite' );
$color = 'orange';
}
?>
<div class="wpconsent-dashboard-box wpconsent-compliance-score">
<div class="wpconsent-dashboard-box-title">
<h2><?php esc_html_e( 'Site Consent Health', 'wpconsent-cookies-banner-privacy-suite' ); ?></h2>
</div>
<div class="wpconsent-dashboard-box-content">
<div class="wpconsent-score-header">
<div class="wpconsent-score-ring wpconsent-score-ring-<?php echo esc_attr( $color ); ?>" style="--score-pct: <?php echo esc_attr( $percentage ); ?>">
<span class="wpconsent-score-ring-value"><?php echo esc_html( $percentage ); ?>%</span>
</div>
<div class="wpconsent-score-header-text">
<p class="wpconsent-score-label"><?php echo esc_html( $label ); ?></p>
<?php
$stats = $this->get_value_stats();
if ( ! empty( $stats ) ) :
?>
<p class="wpconsent-score-stats">
<?php
$stat_parts = array();
foreach ( $stats as $stat ) {
if ( is_array( $stat ) && ! empty( $stat['url'] ) ) {
$stat_parts[] = '<a href="' . esc_url( $stat['url'] ) . '">' . esc_html( $stat['text'] ) . '</a>';
} else {
$stat_parts[] = esc_html( is_array( $stat ) ? $stat['text'] : $stat );
}
}
echo implode( ' · ', $stat_parts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- each part is escaped above.
?>
</p>
<?php endif; ?>
</div>
</div>
<?php if ( $percentage < 50 ) : ?>
<div class="wpconsent-wizard-cta">
<p><?php esc_html_e( 'The quickest way to get set up is our guided wizard. It takes about two minutes.', 'wpconsent-cookies-banner-privacy-suite' ); ?></p>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpconsent-onboarding' ) ); ?>" class="wpconsent-button wpconsent-button-primary">
<?php esc_html_e( 'Run the Setup Wizard', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</a>
</div>
<?php endif; ?>
<?php if ( ! empty( $pending_basics ) || ! empty( $pending_advanced ) ) : ?>
<h3 class="wpconsent-score-section-title"><?php esc_html_e( 'To improve your score', 'wpconsent-cookies-banner-privacy-suite' ); ?></h3>
<ul class="wpconsent-score-items">
<?php
foreach ( $pending_basics as $item ) :
$this->render_score_item( $item );
endforeach;
foreach ( $pending_advanced as $item ) :
$this->render_score_item( $item, true );
endforeach;
?>
</ul>
<?php endif; ?>
<?php
$suggestions = $this->get_suggestions();
if ( ! empty( $suggestions ) ) :
?>
<div class="wpconsent-suggestions">
<h3 class="wpconsent-score-section-title wpconsent-score-section-suggestions"><?php esc_html_e( 'Suggestions', 'wpconsent-cookies-banner-privacy-suite' ); ?></h3>
<ul class="wpconsent-suggestion-list">
<?php foreach ( $suggestions as $suggestion ) : ?>
<li class="wpconsent-suggestion-item">
<span class="wpconsent-suggestion-icon">&#128161;</span>
<div class="wpconsent-suggestion-content">
<div class="wpconsent-suggestion-header">
<span class="wpconsent-suggestion-label"><?php echo esc_html( $suggestion['label'] ); ?></span>
<button type="button" class="wpconsent-suggestion-dismiss" data-suggestion="<?php echo esc_attr( $suggestion['key'] ); ?>" aria-label="<?php esc_attr_e( 'Dismiss', 'wpconsent-cookies-banner-privacy-suite' ); ?>">&times;</button>
</div>
<span class="wpconsent-suggestion-detail"><?php echo esc_html( $suggestion['detail'] ); ?></span>
<a href="<?php echo esc_url( $suggestion['url'] ); ?>" class="wpconsent-score-item-action"><?php esc_html_e( 'Set up', 'wpconsent-cookies-banner-privacy-suite' ); ?> &rarr;</a>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php
$dismissed_suggestions = $this->get_dismissed_suggestions();
$has_details = ! empty( $all_earned ) || ! empty( $dismissed_suggestions );
?>
<?php if ( $has_details ) : ?>
<?php
/* translators: %1$d: earned count, %2$d: total count. */
$show_text = sprintf( esc_html__( '%1$d of %2$d items complete · Show details', 'wpconsent-cookies-banner-privacy-suite' ), $earned_count, $total_count );
/* translators: %1$d: earned count, %2$d: total count. */
$hide_text = sprintf( esc_html__( '%1$d of %2$d items complete · Hide details', 'wpconsent-cookies-banner-privacy-suite' ), $earned_count, $total_count );
?>
<div class="wpconsent-score-earned-toggle">
<button type="button" class="wpconsent-score-toggle-btn" data-show-text="<?php echo esc_attr( $show_text ); ?>" data-hide-text="<?php echo esc_attr( $hide_text ); ?>">
<span class="wpconsent-score-toggle-text"><?php echo esc_html( $show_text ); ?></span>
</button>
</div>
<div class="wpconsent-score-details-content" style="display: none;">
<?php if ( ! empty( $all_earned ) ) : ?>
<ul class="wpconsent-score-items wpconsent-score-items-earned">
<?php
foreach ( $earned_basics as $item ) :
$this->render_score_item( $item );
endforeach;
foreach ( $earned_advanced as $item ) :
$this->render_score_item( $item, true );
endforeach;
?>
</ul>
<?php endif; ?>
<div class="wpconsent-dismissed-suggestions-area" <?php echo empty( $dismissed_suggestions ) ? 'style="display:none;"' : ''; ?>>
<h3 class="wpconsent-score-section-title wpconsent-score-section-dismissed"><?php esc_html_e( 'Dismissed suggestions', 'wpconsent-cookies-banner-privacy-suite' ); ?></h3>
<ul class="wpconsent-suggestion-list wpconsent-suggestion-list-dismissed">
<?php foreach ( $dismissed_suggestions as $suggestion ) : ?>
<li class="wpconsent-suggestion-item wpconsent-suggestion-item-dismissed">
<span class="wpconsent-suggestion-icon">&#128161;</span>
<div class="wpconsent-suggestion-content">
<div class="wpconsent-suggestion-header">
<span class="wpconsent-suggestion-label"><?php echo esc_html( $suggestion['label'] ); ?></span>
</div>
<span class="wpconsent-suggestion-detail"><?php echo esc_html( $suggestion['detail'] ); ?></span>
<a href="<?php echo esc_url( $suggestion['url'] ); ?>" class="wpconsent-score-item-action"><?php esc_html_e( 'Set up', 'wpconsent-cookies-banner-privacy-suite' ); ?> &rarr;</a>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
/**
* Get the advanced compliance items.
*
* In Lite, these are upsell items. Pro overrides this method
* to return actionable items that count toward the score.
*
* @return array
*/
protected function get_advanced_items() {
if ( null !== $this->cached_advanced_items ) {
return $this->cached_advanced_items;
}
$items = array(
array(
'earned' => false,
'label' => __( 'Geolocation rules', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 0,
'detail' => __( 'Show the right consent experience per region.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => wpconsent_utm_url( 'https://wpconsent.com/lite/', 'dashboard', 'geolocation' ),
'upsell' => true,
),
array(
'earned' => false,
'label' => __( 'Consent logging enabled', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 15,
'detail' => __( 'Keep a log of visitor consent choices.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => wpconsent_utm_url( 'https://wpconsent.com/lite/', 'dashboard', 'consent-logging' ),
'upsell' => true,
),
array(
'earned' => false,
'label' => __( 'Auto-scanning enabled', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 10,
'detail' => __( 'Automatically detect new cookies as your site changes.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => wpconsent_utm_url( 'https://wpconsent.com/lite/', 'dashboard', 'auto-scanning' ),
'upsell' => true,
),
);
// Conditional: show multilanguage upsell if a translation plugin is active.
if ( $this->has_translation_plugin() ) {
$items[] = array(
'earned' => false,
'label' => __( 'Automatic translations', 'wpconsent-cookies-banner-privacy-suite' ),
'points' => 0,
'detail' => __( 'Automatically translate your consent banner for multilingual visitors.', 'wpconsent-cookies-banner-privacy-suite' ),
'url' => wpconsent_utm_url( 'https://wpconsent.com/lite/', 'dashboard', 'multilanguage' ),
'upsell' => true,
);
}
$this->cached_advanced_items = $items;
return $items;
}
/**
* Check if a supported translation plugin is active.
*
* @return bool
*/
protected function has_translation_plugin() {
return function_exists( 'pll_current_language' )
|| defined( 'ICL_SITEPRESS_VERSION' )
|| class_exists( 'TRP_Translate_Press' );
}
/**
* Get suggestion items. Suggestions are dismissible and don't count toward score.
*
* Lite returns empty — suggestions are Pro features.
* Pro overrides this to return IAB TCF, Do Not Sell, etc.
*
* @return array
*/
protected function get_suggestions() {
return array();
}
/**
* Get dismissed suggestion items for display in the details toggle.
*
* Lite returns empty. Pro overrides this.
*
* @return array
*/
protected function get_dismissed_suggestions() {
return array();
}
/**
* Render a single compliance score item.
*
* @param array $item The item data.
* @param bool $is_advanced Whether this is in the advanced section.
*
* @return void
*/
protected function render_score_item( $item, $is_advanced = false ) {
$is_upsell = ! empty( $item['upsell'] );
$classes = 'wpconsent-score-item';
$classes .= $item['earned'] ? ' wpconsent-score-item-earned' : ' wpconsent-score-item-pending';
if ( $is_upsell ) {
$classes .= ' wpconsent-score-item-pro';
}
?>
<li class="<?php echo esc_attr( $classes ); ?>">
<span class="wpconsent-score-item-check"><?php echo $item['earned'] ? '&#10003;' : '&#9675;'; ?></span>
<div class="wpconsent-score-item-content">
<div class="wpconsent-score-item-header">
<span class="wpconsent-score-item-label"><?php echo esc_html( $item['label'] ); ?></span>
<?php if ( ! $is_upsell ) : ?>
<span class="wpconsent-score-item-points">
<?php echo esc_html( $item['points'] . ' ' . __( 'pts', 'wpconsent-cookies-banner-privacy-suite' ) ); ?>
</span>
<?php endif; ?>
</div>
<span class="wpconsent-score-item-detail"><?php echo esc_html( $item['detail'] ); ?></span>
<?php if ( ! $item['earned'] ) : ?>
<?php
$action_type = isset( $item['action_type'] ) ? $item['action_type'] : 'link';
$action_key = isset( $item['action_key'] ) ? $item['action_key'] : '';
?>
<div class="wpconsent-score-item-actions">
<?php if ( 'toggle' === $action_type && ! empty( $action_key ) ) : ?>
<button type="button" class="wpconsent-score-action-toggle" data-setting="<?php echo esc_attr( $action_key ); ?>">
<?php esc_html_e( 'Enable', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</button>
<a href="<?php echo esc_url( $item['url'] ); ?>" class="wpconsent-score-item-action">
<?php esc_html_e( 'Settings', 'wpconsent-cookies-banner-privacy-suite' ); ?> &rarr;
</a>
<?php elseif ( 'generate_policy' === $action_type ) : ?>
<button type="button" class="wpconsent-score-action-generate-policy">
<?php esc_html_e( 'Generate page', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</button>
<a href="<?php echo esc_url( $item['url'] ); ?>" class="wpconsent-score-item-action">
<?php esc_html_e( 'Choose page', 'wpconsent-cookies-banner-privacy-suite' ); ?> &rarr;
</a>
<?php elseif ( 'configure_cookies' === $action_type ) : ?>
<?php if ( ! empty( $item['has_scan'] ) ) : ?>
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpconsent-scanner#wpconsent-scan-detailed-report' ) ); ?>" class="wpconsent-score-action-btn">
<?php esc_html_e( 'Configure from scan', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</a>
<?php endif; ?>
<a href="<?php echo esc_url( $item['url'] ); ?>" class="wpconsent-score-item-action">
<?php esc_html_e( 'Configure manually', 'wpconsent-cookies-banner-privacy-suite' ); ?> &rarr;
</a>
<?php else : ?>
<a href="<?php echo esc_url( $item['url'] ); ?>" class="wpconsent-score-item-action" <?php echo $is_upsell ? 'target="_blank" rel="noopener noreferrer"' : ''; ?>>
<?php
if ( $is_upsell ) {
esc_html_e( 'Upgrade to Pro', 'wpconsent-cookies-banner-privacy-suite' );
} else {
esc_html_e( 'Set up', 'wpconsent-cookies-banner-privacy-suite' );
}
?>
&rarr;
</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</li>
<?php
}
/**
* Banner preview widget.
*
* @return void
*/
public function banner_preview_box() {
$banner_enabled = (bool) wpconsent()->settings->get_option( 'enable_consent_banner' );
?>
<div class="wpconsent-dashboard-box wpconsent-banner-preview-widget">
<div class="wpconsent-dashboard-box-title">
<h2><?php esc_html_e( 'Your Consent Banner', 'wpconsent-cookies-banner-privacy-suite' ); ?></h2>
</div>
<div class="wpconsent-dashboard-box-content">
<?php if ( ! $banner_enabled ) : ?>
<p class="wpconsent-banner-preview-disabled"><?php esc_html_e( 'Banner is not active.', 'wpconsent-cookies-banner-privacy-suite' ); ?></p>
<?php endif; ?>
<div class="wpconsent-banner-preview-container <?php echo ! $banner_enabled ? 'wpconsent-banner-preview-faded' : ''; ?>">
<div class="wpconsent-banner-preview-wrapper">
<?php wpconsent()->banner->output_banner(); ?>
</div>
</div>
</div>
<div class="wpconsent-dashboard-box-actions">
<a href="<?php echo esc_url( $this->get_page_url( 'wpconsent-banner' ) ); ?>" class="wpconsent-button wpconsent-button-primary">
<?php $banner_enabled ? esc_html_e( 'Customize Banner', 'wpconsent-cookies-banner-privacy-suite' ) : esc_html_e( 'Enable Banner', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</a>
</div>
</div>
<?php
}
/**
* Blog feed widget showing recent announcements.
*
* @return void
*/
public function blog_feed_widget() {
$articles = wpconsent()->dashboard->get_blog_articles();
?>
<div class="wpconsent-dashboard-box wpconsent-blog-feed-widget">
<div class="wpconsent-dashboard-box-title">
<h2><?php esc_html_e( "What's New", 'wpconsent-cookies-banner-privacy-suite' ); ?></h2>
</div>
<div class="wpconsent-dashboard-box-content">
<?php if ( ! empty( $articles ) ) : ?>
<ul class="wpconsent-blog-feed-list">
<?php foreach ( $articles as $article ) : ?>
<li class="wpconsent-blog-feed-item">
<?php wpconsent_icon( 'announcement', 20, 20, '0 0 24 24' ); ?>
<div class="wpconsent-blog-feed-item-content">
<a href="<?php echo esc_url( wpconsent_utm_url( $article['link'], 'dashboard', 'blog-announcements' ) ); ?>" target="_blank" rel="noopener noreferrer" class="wpconsent-blog-feed-title">
<?php echo esc_html( wp_trim_words( $article['title'], 10 ) ); ?>
</a>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php else : ?>
<p>
<a href="<?php echo esc_url( wpconsent_utm_url( 'https://wpconsent.com/blog/', 'dashboard', 'blog-announcements' ) ); ?>" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'Visit our blog', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</a>
</p>
<?php endif; ?>
</div>
<?php if ( ! empty( $articles ) ) : ?>
<div class="wpconsent-dashboard-box-actions">
<a href="<?php echo esc_url( wpconsent_utm_url( 'https://wpconsent.com/blog/', 'dashboard', 'blog-announcements' ) ); ?>" target="_blank" rel="noopener noreferrer" class="wpconsent-button wpconsent-button-secondary">
<?php esc_html_e( 'View all articles', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</a>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Get contextual documentation articles based on plugin state.
*
* Matches article slugs from the docs JSON against current configuration gaps.
* Contextual articles appear first, backfilled with fallback articles up to 5 total.
* Pro overrides this to add Pro-specific contextual rules.
*
* @return array Array of articles with 'title' and 'url' keys.
*/
protected function get_doc_articles() {
$by_slug = $this->get_docs_by_slug();
if ( empty( $by_slug ) ) {
return array();
}
// Contextual rules: map unconfigured states to article slugs.
$contextual_slugs = array();
$banner_enabled = (bool) wpconsent()->settings->get_option( 'enable_consent_banner' );
if ( ! $banner_enabled ) {
$contextual_slugs[] = 'banner-content-and-text';
$contextual_slugs[] = 'banner-layout-options';
}
$script_blocking = (bool) wpconsent()->settings->get_option( 'enable_script_blocking' );
if ( ! $script_blocking ) {
$contextual_slugs[] = 'automatic-script-blocking';
}
$scan_data = wpconsent()->scanner->get_scan_data();
if ( empty( $scan_data['date'] ) ) {
$contextual_slugs[] = 'scanning-your-website';
}
$policy_page_id = (int) wpconsent()->settings->get_option( 'cookie_policy_page' );
$policy_exists = $policy_page_id > 0 && 'publish' === get_post_status( $policy_page_id );
if ( ! $policy_exists ) {
$contextual_slugs[] = 'cookie-policy-page';
}
$cookie_count = (int) wp_count_posts( 'wpconsent_cookie' )->publish;
if ( 0 === $cookie_count ) {
$contextual_slugs[] = 'managing-cookie-categories';
$contextual_slugs[] = 'managing-services';
}
// Fallback articles for when everything is configured.
$fallback_slugs = array(
'banner-styling',
'google-consent-mode-v2',
'content-blocking-and-placeholders',
'how-to-customize-the-wpconsent-banner-with-css-variables',
'troubleshooting-guide',
);
// Build the final article list: contextual first, backfill with fallbacks.
$articles = array();
$used = array();
foreach ( $contextual_slugs as $slug ) {
if ( count( $articles ) >= 5 ) {
break;
}
if ( isset( $by_slug[ $slug ] ) && ! isset( $used[ $slug ] ) ) {
$articles[] = $by_slug[ $slug ];
$used[ $slug ] = true;
}
}
foreach ( $fallback_slugs as $slug ) {
if ( count( $articles ) >= 5 ) {
break;
}
if ( isset( $by_slug[ $slug ] ) && ! isset( $used[ $slug ] ) ) {
$articles[] = $by_slug[ $slug ];
$used[ $slug ] = true;
}
}
return $articles;
}
/**
* Build a slug-to-doc lookup from the docs JSON data.
*
* @return array Associative array of slug => doc data.
*/
protected function get_docs_by_slug() {
if ( null !== $this->cached_docs_by_slug ) {
return $this->cached_docs_by_slug;
}
$docs = new WPConsent_Docs();
$all = $docs->get_docs();
if ( empty( $all ) ) {
return array();
}
$by_slug = array();
foreach ( $all as $doc ) {
if ( empty( $doc['url'] ) ) {
continue;
}
$slug = trim( wp_parse_url( $doc['url'], PHP_URL_PATH ), '/' );
$parts = explode( '/', $slug );
$slug = end( $parts );
if ( ! empty( $slug ) ) {
$by_slug[ $slug ] = $doc;
}
}
$this->cached_docs_by_slug = $by_slug;
return $by_slug;
}
/**
* Documentation widget with contextual article recommendations.
*
* @return void
*/
public function docs_widget() {
$articles = $this->get_doc_articles();
?>
<div class="wpconsent-dashboard-box wpconsent-docs-widget">
<div class="wpconsent-dashboard-box-title">
<h2><?php esc_html_e( 'Help & Documentation', 'wpconsent-cookies-banner-privacy-suite' ); ?></h2>
</div>
<div class="wpconsent-dashboard-box-content">
<?php if ( ! empty( $articles ) ) : ?>
<h3 class="wpconsent-docs-section-title"><?php esc_html_e( 'Recommended for you', 'wpconsent-cookies-banner-privacy-suite' ); ?></h3>
<ul class="wpconsent-docs-list">
<?php foreach ( $articles as $article ) : ?>
<li class="wpconsent-docs-item">
<a href="<?php echo esc_url( wpconsent_utm_url( $article['url'], 'dashboard', 'docs-widget' ) ); ?>" class="wpconsent-docs-link" data-doc-url="<?php echo esc_url( $article['url'] ); ?>">
<?php echo esc_html( $article['title'] ); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php else : ?>
<p>
<?php esc_html_e( 'Browse our documentation for guides, tutorials, and troubleshooting.', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</p>
<?php endif; ?>
</div>
<div class="wpconsent-dashboard-box-actions">
<a href="#" class="wpconsent-button wpconsent-button-secondary wpconsent-show-help">
<?php esc_html_e( 'Browse all documentation', 'wpconsent-cookies-banner-privacy-suite' ); ?>
</a>
</div>
</div>
<?php
}
}