*/ public static function definitions() { return array( array( 'kind' => 'stats', 'block' => 'causewise/overview', 'shortcode' => 'causewise_stats', 'title' => __( 'Analytics Overview', 'causewise-analytics' ), 'description' => __( 'Page views, sessions, and visitors for a date range.', 'causewise-analytics' ), 'limit' => false, ), array( 'kind' => 'traffic', 'block' => 'causewise/traffic', 'shortcode' => 'causewise_traffic', 'title' => __( 'Traffic Chart', 'causewise-analytics' ), 'description' => __( 'Event counts over the selected date range.', 'causewise-analytics' ), 'limit' => false, ), array( 'kind' => 'top-pages', 'block' => 'causewise/top-pages', 'shortcode' => 'causewise_top_pages', 'title' => __( 'Top Pages', 'causewise-analytics' ), 'description' => __( 'The most viewed paths.', 'causewise-analytics' ), 'limit' => true, ), array( 'kind' => 'referrers', 'block' => 'causewise/referrers', 'shortcode' => 'causewise_referrers', 'title' => __( 'Referrers', 'causewise-analytics' ), 'description' => __( 'Where visits came from.', 'causewise-analytics' ), 'limit' => true, ), array( 'kind' => 'goals', 'block' => 'causewise/goals', 'shortcode' => 'causewise_goals', 'title' => __( 'Goals', 'causewise-analytics' ), 'description' => __( 'Goal conversions from CauseWise.', 'causewise-analytics' ), 'limit' => false, ), array( 'kind' => 'events', 'block' => 'causewise/recent-events', 'shortcode' => 'causewise_events', 'title' => __( 'Recent Events', 'causewise-analytics' ), 'description' => __( 'Recent event names, types, and paths. Event properties are not shown.', 'causewise-analytics' ), 'limit' => true, ), array( 'kind' => 'dashboard', 'block' => 'causewise/dashboard', 'shortcode' => 'causewise_dashboard', 'title' => __( 'CauseWise Dashboard', 'causewise-analytics' ), 'description' => __( 'Overview, traffic, pages, referrers, goals, funnels, and recent events.', 'causewise-analytics' ), 'limit' => false, 'dashboard' => true, ), ); } /** * Section flags for the dashboard block. Keys match the shared layout. * * @return array */ public static function dashboard_toggles() { return array( 'stats' => array( 'showStats', 'show_stats' ), 'traffic' => array( 'showTraffic', 'show_traffic' ), 'top-pages' => array( 'showPages', 'show_pages', 'showTopPages', 'show_top_pages' ), 'referrers' => array( 'showReferrers', 'show_referrers' ), 'goals' => array( 'showGoals', 'show_goals' ), 'funnels' => array( 'showFunnels', 'show_funnels' ), 'events' => array( 'showEvents', 'show_events' ), ); } /** * @return array> */ public static function dashboard_toggle_schema() { $schema = array(); foreach ( self::dashboard_toggles() as $keys ) { $schema[ $keys[0] ] = array( 'type' => 'boolean', 'default' => true, ); } return $schema; } /** * Attributes for one block. The dashboard adds section visibility. * * @param array $definition Block definition. * @return array> */ public static function attributes_for( $definition ) { $schema = self::attribute_schema(); if ( ! empty( $definition['dashboard'] ) ) { $schema = array_merge( $schema, self::dashboard_toggle_schema(), self::dashboard_layout_schema() ); } return $schema; } /** * Alignment and custom width. Only the dashboard block registers these. * * @return array> */ public static function dashboard_layout_schema() { return array( 'align' => array( 'type' => 'string', 'default' => '', ), 'width' => array( 'type' => 'string', 'default' => '', ), 'instance' => array( 'type' => 'string', 'default' => '', ), ); } /** * @return array> */ public static function attribute_schema() { return array( 'range' => array( 'type' => 'string', 'default' => '7d', ), 'title' => array( 'type' => 'string', 'default' => '', ), 'limit' => array( 'type' => 'number', 'default' => 8, ), 'theme' => array( 'type' => 'string', 'default' => 'light', ), ); } /** * Register hooks. Block registration waits for init. */ public static function init() { add_action( 'init', array( __CLASS__, 'register' ) ); add_filter( 'block_categories_all', array( __CLASS__, 'categories' ) ); add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_editor' ) ); add_action( 'wp', array( __CLASS__, 'mark_private_cache' ) ); add_action( 'template_redirect', array( __CLASS__, 'send_private_headers' ) ); } /** * Register dynamic blocks and shortcodes. Both call markup(). */ public static function register() { foreach ( self::definitions() as $definition ) { $kind = $definition['kind']; add_shortcode( $definition['shortcode'], static function ( $atts ) use ( $kind ) { return Causewise_Embeds::markup( $kind, $atts ); } ); if ( function_exists( 'register_block_type' ) ) { register_block_type( $definition['block'], array( 'api_version' => 3, 'title' => $definition['title'], 'description' => $definition['description'], 'category' => 'causewise', 'attributes' => self::attributes_for( $definition ), 'supports' => self::block_supports( $definition ), 'render_callback' => static function ( $attributes, $content, $block ) use ( $kind ) { unset( $content, $block ); return Causewise_Embeds::markup( $kind, $attributes, true ); }, ) ); } } } /** * @param mixed $categories Existing block categories. * @return array> */ public static function categories( $categories ) { if ( ! is_array( $categories ) ) { $categories = array(); } foreach ( $categories as $category ) { if ( is_array( $category ) && isset( $category['slug'] ) && 'causewise' === $category['slug'] ) { return $categories; } } array_unshift( $categories, array( 'slug' => 'causewise', 'title' => __( 'CauseWise', 'causewise-analytics' ), 'icon' => null, ) ); return $categories; } /** * Same message for every visitor. It contains no analytics. * * @return string */ public static function private_note() { return __( 'Sign in with a WordPress account that can manage CauseWise to view these analytics.', 'causewise-analytics' ); } /** * @param mixed $kind Requested section. * @return string */ public static function sanitize_kind( $kind ) { $kind = strtolower( trim( (string) $kind ) ); foreach ( self::definitions() as $definition ) { if ( $definition['kind'] === $kind ) { return $kind; } } return ''; } /** * @param mixed $theme Requested theme. * @return string */ public static function sanitize_theme( $theme ) { $theme = strtolower( trim( (string) $theme ) ); if ( in_array( $theme, array( 'light', 'dark', 'system' ), true ) ) { return $theme; } return 'light'; } /** * @param mixed $limit Requested row count. * @return int */ public static function sanitize_limit( $limit ) { if ( null === $limit || '' === $limit || ! is_numeric( $limit ) ) { return 8; } $limit = (int) $limit; if ( $limit < 1 ) { return 8; } if ( $limit > 20 ) { return 20; } return $limit; } /** * Event pages are 10 or 20. The embed then shows only the requested rows. * * @param mixed $limit Display limit. * @return int */ public static function event_request_limit( $limit ) { return self::sanitize_limit( $limit ) <= 10 ? 10 : 20; } /** * Theme alignment for the dashboard. Other values stay in the normal content width. * * @param mixed $align Requested alignment. * @return string */ public static function sanitize_instance( $instance ) { $instance = strtolower( (string) preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $instance ) ); if ( strlen( $instance ) < 6 || strlen( $instance ) > 40 || false !== strpos( $instance, 'cw_api' ) ) { return ''; } return $instance; } /** * Stable id for one dashboard block. Generated when the saved block has none. * * @param mixed $attributes Block attributes or shortcode attributes. * @return string */ public static function dashboard_instance( $attributes ) { $attributes = is_array( $attributes ) ? $attributes : array(); $instance = self::sanitize_instance( isset( $attributes['instance'] ) ? $attributes['instance'] : '' ); if ( '' !== $instance ) { return $instance; } $post_id = function_exists( 'get_the_ID' ) ? (int) get_the_ID() : 0; if ( $post_id < 0 ) { $post_id = 0; } $seq = isset( $GLOBALS['causewise_dashboard_seq'] ) ? (int) $GLOBALS['causewise_dashboard_seq'] : 0; $seq++; $GLOBALS['causewise_dashboard_seq'] = $seq; return 'post' . $post_id . 'seq' . $seq; } /** * Host name used to scope a viewer's saved layout. No path or credentials. * * @return string */ public static function viewer_site_host() { if ( ! function_exists( 'home_url' ) || ! function_exists( 'wp_parse_url' ) ) { return ''; } $host = wp_parse_url( home_url(), PHP_URL_HOST ); $host = strtolower( (string) preg_replace( '/[^a-z0-9.-]/', '', (string) $host ) ); return $host; } /** * WordPress user id for the authorized viewer. Zero when the user is unknown. * * @return int */ public static function viewer_user_id() { if ( ! function_exists( 'get_current_user_id' ) ) { return 0; } $user_id = (int) get_current_user_id(); return $user_id > 0 ? $user_id : 0; } public static function sanitize_align( $align ) { $align = strtolower( trim( (string) $align ) ); if ( 'wide' === $align || 'full' === $align ) { return $align; } return ''; } /** * Dashboard width. Lengths become a max-width. wide and full use theme alignment. * * @param mixed $width Requested width. * @return string */ public static function sanitize_width( $width ) { $width = strtolower( trim( (string) $width ) ); $width = preg_replace( '/\s+/', '', $width ); if ( ! is_string( $width ) || '' === $width || in_array( $width, array( 'container', 'default', 'content' ), true ) ) { return ''; } if ( 'wide' === $width || 'full' === $width ) { return $width; } if ( preg_match( '/^(\d{1,3})%$/', $width, $percent ) ) { $number = (int) $percent[1]; if ( $number >= 1 && $number <= 100 ) { return $number . '%'; } return ''; } if ( preg_match( '/^(\d{1,4})(?:px)?$/', $width, $pixels ) ) { $number = (int) $pixels[1]; if ( $number >= 240 && $number <= 2000 ) { return $number . 'px'; } } return ''; } /** * @param array $definition Block definition. * @return array */ public static function block_supports( $definition ) { $supports = array( 'html' => false, 'anchor' => false, ); if ( ! empty( $definition['dashboard'] ) ) { $supports['align'] = array( 'wide', 'full' ); } return $supports; } /** * @param mixed $title Optional heading. * @return string */ public static function sanitize_embed_title( $title ) { $title = sanitize_text_field( (string) $title ); if ( '' === $title || false !== strpos( $title, 'cw_api_' ) ) { return ''; } if ( function_exists( 'mb_substr' ) ) { return mb_substr( $title, 0, 80 ); } return substr( $title, 0, 80 ); } /** * True only for the old default heading, which froze a plugin version into the label. * * A title the page author wrote is not matched, even when it contains a version number. * * @param mixed $title Saved or requested title. * @return bool */ public static function is_legacy_default_dashboard_title( $title ) { $title = self::sanitize_embed_title( $title ); return 1 === preg_match( '/^CauseWise Analytics WP \d+\.\d+\.\d+$/', $title ); } /** * Dashboard heading. Blank and the old versioned label use CauseWise Analytics. * * @param mixed $title Saved or requested title. * @return string */ public static function dashboard_heading( $title ) { $title = self::sanitize_embed_title( $title ); if ( '' === $title || self::is_legacy_default_dashboard_title( $title ) ) { return 'CauseWise Analytics'; } return $title; } /** * @param mixed $value Block or shortcode flag. * @return bool */ public static function is_enabled_flag( $value ) { if ( is_bool( $value ) ) { return $value; } if ( is_int( $value ) || is_float( $value ) ) { return 0 !== (int) $value; } $text = strtolower( trim( (string) $value ) ); return ! in_array( $text, array( '', '0', 'false', 'no', 'off' ), true ); } /** * Visible dashboard panels, in the default layout order. * * An empty `sections` allowlist hides every panel. Missing flags stay visible. * * @param mixed $attributes Block attributes or shortcode attributes. * @return string[] */ public static function visible_sections( $attributes ) { $attributes = is_array( $attributes ) ? $attributes : array(); $order = array_keys( self::dashboard_toggles() ); $visible = array(); foreach ( $order as $panel ) { $visible[ $panel ] = true; } if ( isset( $attributes['sections'] ) && is_string( $attributes['sections'] ) ) { foreach ( $visible as $panel => $shown ) { $visible[ $panel ] = false; } $tokens = array( 'stats' => 'stats', 'traffic' => 'traffic', 'pages' => 'top-pages', 'top-pages' => 'top-pages', 'referrers' => 'referrers', 'goals' => 'goals', 'funnels' => 'funnels', 'events' => 'events', 'recent-events' => 'events', ); foreach ( explode( ',', $attributes['sections'] ) as $part ) { $token = strtolower( trim( (string) $part ) ); if ( isset( $tokens[ $token ] ) ) { $visible[ $tokens[ $token ] ] = true; } } } foreach ( self::dashboard_toggles() as $panel => $keys ) { foreach ( $keys as $key ) { if ( array_key_exists( $key, $attributes ) ) { $visible[ $panel ] = self::is_enabled_flag( $attributes[ $key ] ); } } } $list = array(); foreach ( $order as $panel ) { if ( ! empty( $visible[ $panel ] ) ) { $list[] = $panel; } } return $list; } /** * True when a saved block's attributes match the registered schema. * * @param string $block_name Block name. * @param mixed $attributes Saved attributes. * @return bool */ public static function saved_attributes_match( $block_name, $attributes ) { if ( ! is_array( $attributes ) ) { return false; } foreach ( self::definitions() as $definition ) { if ( $definition['block'] !== $block_name ) { continue; } $schema = self::attributes_for( $definition ); foreach ( $attributes as $key => $value ) { if ( 'className' === $key && is_string( $value ) ) { continue; } if ( ! isset( $schema[ $key ]['type'] ) ) { return false; } $type = $schema[ $key ]['type']; if ( 'string' === $type && ! is_string( $value ) ) { return false; } if ( 'number' === $type && ! is_int( $value ) && ! is_float( $value ) ) { return false; } if ( 'boolean' === $type && ! is_bool( $value ) ) { return false; } } return true; } return false; } /** * @param mixed $attributes Block attributes or shortcode attributes. * @return array{range: string, title: string, limit: int, theme: string} */ public static function config_from_attributes( $attributes ) { $attributes = is_array( $attributes ) ? $attributes : array(); return array( 'range' => Causewise_Api::sanitize_range( isset( $attributes['range'] ) ? $attributes['range'] : '7d' ), 'title' => self::sanitize_embed_title( isset( $attributes['title'] ) ? $attributes['title'] : '' ), 'limit' => self::sanitize_limit( isset( $attributes['limit'] ) ? $attributes['limit'] : 8 ), 'theme' => self::sanitize_theme( isset( $attributes['theme'] ) ? $attributes['theme'] : 'light' ), ); } /** * Placeholder shared by blocks and shortcodes. It never includes analytics. * * @param string $kind Embed kind. * @param mixed $attributes Requested settings. * @param bool $block_wrapper True when Gutenberg is rendering the block. * @return string */ public static function markup( $kind, $attributes, $block_wrapper = false ) { $kind = self::sanitize_kind( $kind ); if ( '' === $kind ) { return ''; } $config = self::config_from_attributes( $attributes ); if ( 'dashboard' === $kind ) { $config['title'] = self::dashboard_heading( $config['title'] ); } self::prepare_viewer(); $classes = array( 'causewise-embed' ); if ( is_array( $attributes ) && isset( $attributes['className'] ) ) { $parts = preg_split( '/\s+/', (string) $attributes['className'] ); if ( is_array( $parts ) ) { foreach ( $parts as $class ) { $clean = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $class ); if ( is_string( $clean ) && '' !== $clean ) { $classes[] = $clean; } } } } $sections = ''; $style = ''; $section_list = ''; $instance = 'dashboard' === $kind ? self::dashboard_instance( $attributes ) : ''; if ( 'dashboard' === $kind ) { $section_list = implode( ',', self::visible_sections( $attributes ) ); if ( '' === $section_list ) { $section_list = 'none'; } $sections = ' data-sections="' . esc_attr( $section_list ) . '"'; $sections .= ' data-instance="' . esc_attr( $instance ) . '"'; $layout = self::dashboard_layout( $attributes ); if ( 'wide' === $layout['align'] ) { $classes[] = 'alignwide'; } elseif ( 'full' === $layout['align'] ) { $classes[] = 'alignfull'; } $classes[] = 'wp-block-causewise-dashboard'; if ( '' !== $layout['width'] ) { $style = ' style="' . esc_attr( 'width:100%;max-width:' . $layout['width'] ) . '"'; } } $note = esc_html( self::private_note() ); if ( $block_wrapper && function_exists( 'get_block_wrapper_attributes' ) ) { $extra = array( 'class' => implode( ' ', $classes ), 'data-causewise-embed' => $kind, 'data-range' => $config['range'], 'data-title' => $config['title'], 'data-limit' => (string) (int) $config['limit'], 'data-theme' => $config['theme'], ); if ( 'dashboard' === $kind ) { $extra['data-sections'] = $section_list; $extra['data-instance'] = $instance; } if ( '' !== $style ) { $extra['style'] = 'width:100%;max-width:' . self::dashboard_layout( $attributes )['width']; } return '

' . $note . '

'; } return sprintf( '

%9$s

', esc_attr( implode( ' ', $classes ) ), esc_attr( $kind ), esc_attr( $config['range'] ), esc_attr( $config['title'] ), (int) $config['limit'], esc_attr( $config['theme'] ), $sections, $style, $note ); } /** * Resolved dashboard alignment and maximum width. * * A custom length stays inside the parent. It is not combined with wide or full alignment. * * @param mixed $attributes Block attributes or shortcode attributes. * @return array{align: string, width: string} */ public static function dashboard_layout( $attributes ) { $attributes = is_array( $attributes ) ? $attributes : array(); $width = self::sanitize_width( isset( $attributes['width'] ) ? $attributes['width'] : '' ); $align = self::sanitize_align( isset( $attributes['align'] ) ? $attributes['align'] : '' ); if ( ( 'wide' === $width || 'full' === $width ) && '' === $align ) { $align = $width; } if ( 'wide' === $width || 'full' === $width || '' !== $align ) { $width = ''; } return array( 'align' => $align, 'width' => $width, ); } /** * Load one section through the existing CauseWise client and drop the rest. * * @param mixed $range Requested range. * @param string $kind Embed kind. * @param mixed $limit Display limit. * @return array */ public static function load( $range, $kind, $limit ) { $kind = self::sanitize_kind( $kind ); $range = Causewise_Api::sanitize_range( $range ); $limit = self::sanitize_limit( $limit ); if ( '' === $kind ) { return Causewise_Api::scrub( array( 'ok' => false, 'status' => 400, 'code' => 'invalid_section', 'message' => __( 'Choose a CauseWise analytics block.', 'causewise-analytics' ), ) ); } if ( 'dashboard' === $kind ) { $payload = Causewise_Api::load( $range, 'dashboard' ); } elseif ( 'events' === $kind ) { $payload = Causewise_Api::load_events( $range, 'all', '', self::event_request_limit( $limit ) ); } elseif ( 'stats' === $kind || 'goals' === $kind ) { $payload = Causewise_Api::load( $range, 'dashboard' ); } else { $payload = Causewise_Api::load( $range, 'overview' ); } return self::project( $kind, $payload, $limit ); } /** * Keep only the fields the matching React component renders. * * @param string $kind Embed kind. * @param array $payload API payload. * @param int $limit Display limit. * @return array */ public static function project( $kind, $payload, $limit ) { $kind = self::sanitize_kind( $kind ); $payload = Causewise_Api::scrub( is_array( $payload ) ? $payload : array() ); $limit = self::sanitize_limit( $limit ); if ( '' === $kind ) { return self::failure_payload( 400, 'invalid_section', __( 'Choose a CauseWise analytics block.', 'causewise-analytics' ) ); } if ( empty( $payload['ok'] ) ) { return self::failure_payload( isset( $payload['status'] ) ? (int) $payload['status'] : 502, isset( $payload['code'] ) ? (string) $payload['code'] : 'error', isset( $payload['message'] ) ? (string) $payload['message'] : __( 'Analytics could not be loaded.', 'causewise-analytics' ) ); } $result = array( 'ok' => true, 'status' => 200, 'range' => isset( $payload['range'] ) ? (string) $payload['range'] : '7d', ); if ( 'stats' === $kind ) { $stats = isset( $payload['stats'] ) && is_array( $payload['stats'] ) ? $payload['stats'] : array(); $result['stats'] = array( 'pageviews' => isset( $stats['pageviews'] ) ? $stats['pageviews'] : 0, 'sessions' => isset( $stats['sessions'] ) ? $stats['sessions'] : 0, 'sessionsNote' => isset( $stats['sessionsNote'] ) ? $stats['sessionsNote'] : '', 'visitors' => array_key_exists( 'visitors', $stats ) ? $stats['visitors'] : null, 'visitorsNote' => isset( $stats['visitorsNote'] ) ? $stats['visitorsNote'] : '', ); } elseif ( 'traffic' === $kind ) { $result['trend'] = self::public_trend( isset( $payload['trend'] ) ? $payload['trend'] : null ); } elseif ( 'top-pages' === $kind ) { $result['topPages'] = self::slice_rows( isset( $payload['topPages'] ) ? $payload['topPages'] : array(), $limit, array( 'path', 'count' ) ); } elseif ( 'referrers' === $kind ) { $result['referrers'] = self::slice_rows( isset( $payload['referrers'] ) ? $payload['referrers'] : array(), $limit, array( 'label', 'count' ) ); } elseif ( 'dashboard' === $kind ) { $result = self::project_dashboard( $payload, $limit ); return Causewise_Api::scrub( $result ); } elseif ( 'goals' === $kind ) { if ( ! isset( $payload['goals'] ) && ! empty( $payload['summaryError'] ) && is_array( $payload['summaryError'] ) ) { return self::failure_payload( isset( $payload['summaryError']['status'] ) ? (int) $payload['summaryError']['status'] : 502, isset( $payload['summaryError']['code'] ) ? (string) $payload['summaryError']['code'] : 'error', isset( $payload['summaryError']['message'] ) ? (string) $payload['summaryError']['message'] : __( 'Analytics could not be loaded.', 'causewise-analytics' ) ); } $result['goals'] = isset( $payload['goals'] ) ? $payload['goals'] : null; } else { $events = array(); if ( isset( $payload['events'] ) && is_array( $payload['events'] ) ) { $events = $payload['events']; } elseif ( isset( $payload['recentEvents'] ) && is_array( $payload['recentEvents'] ) ) { $events = $payload['recentEvents']; } $result['recentEvents'] = self::public_events( $events, $limit ); } return Causewise_Api::scrub( $result ); } /** * Fields the complete dashboard renders. Event properties stay out. * * @param array $payload Scrubbed dashboard payload. * @param int $limit Display limit for pages, referrers, and events. * @return array */ private static function project_dashboard( $payload, $limit ) { $site = isset( $payload['site'] ) && is_array( $payload['site'] ) ? $payload['site'] : array(); $period = isset( $payload['period'] ) && is_array( $payload['period'] ) ? $payload['period'] : array(); $stats = isset( $payload['stats'] ) && is_array( $payload['stats'] ) ? $payload['stats'] : array(); $advanced = isset( $payload['advancedUrl'] ) ? (string) $payload['advancedUrl'] : ''; if ( 0 !== strpos( $advanced, 'https://causewise.io/' ) && 'https://causewise.io' !== $advanced ) { $advanced = 'https://causewise.io/dashboard'; } $referrers = null; if ( isset( $payload['referrers'] ) && is_array( $payload['referrers'] ) ) { $referrers = self::slice_rows( $payload['referrers'], $limit, array( 'label', 'count' ) ); } return array( 'ok' => true, 'status' => 200, 'range' => isset( $payload['range'] ) ? (string) $payload['range'] : '7d', 'site' => array( 'id' => isset( $site['id'] ) ? (string) $site['id'] : '', 'name' => isset( $site['name'] ) ? (string) $site['name'] : '', 'domain' => isset( $site['domain'] ) ? (string) $site['domain'] : '', ), 'advancedUrl' => $advanced, 'period' => array( 'range' => isset( $period['range'] ) ? (string) $period['range'] : '', 'start' => isset( $period['start'] ) ? (string) $period['start'] : '', 'end' => isset( $period['end'] ) ? (string) $period['end'] : '', 'retentionDays' => isset( $period['retentionDays'] ) ? $period['retentionDays'] : null, ), 'stats' => array( 'pageviews' => isset( $stats['pageviews'] ) ? $stats['pageviews'] : 0, 'sessions' => isset( $stats['sessions'] ) ? $stats['sessions'] : 0, 'sessionsNote' => isset( $stats['sessionsNote'] ) ? $stats['sessionsNote'] : '', 'visitors' => array_key_exists( 'visitors', $stats ) ? $stats['visitors'] : null, 'visitorsNote' => isset( $stats['visitorsNote'] ) ? $stats['visitorsNote'] : '', ), 'trend' => self::public_trend( isset( $payload['trend'] ) ? $payload['trend'] : null ), 'topPages' => self::slice_rows( isset( $payload['topPages'] ) ? $payload['topPages'] : array(), $limit, array( 'path', 'count' ) ), 'referrers' => $referrers, 'goals' => isset( $payload['goals'] ) ? $payload['goals'] : null, 'funnels' => isset( $payload['funnels'] ) ? $payload['funnels'] : null, 'recentEvents' => self::public_events( isset( $payload['recentEvents'] ) ? $payload['recentEvents'] : array(), $limit ), 'overviewError' => self::public_notice( isset( $payload['overviewError'] ) ? $payload['overviewError'] : null ), 'summaryError' => self::public_notice( isset( $payload['summaryError'] ) ? $payload['summaryError'] : null ), ); } /** * @param mixed $notice Error notice. * @return array{code: string, message: string}|null */ private static function public_notice( $notice ) { if ( ! is_array( $notice ) ) { return null; } return array( 'code' => isset( $notice['code'] ) ? (string) $notice['code'] : '', 'message' => isset( $notice['message'] ) ? (string) $notice['message'] : '', ); } /** * Headers for analytics responses and for pages viewed by someone who can see the numbers. * * @return array */ public static function private_cache_headers() { return array( 'Cache-Control' => 'private, no-store, max-age=0, must-revalidate', 'Pragma' => 'no-cache', ); } /** * True when this response could otherwise be stored and reused for a visitor. * * @return bool */ public static function should_make_private() { if ( ! Causewise_Admin::can_view() ) { return false; } return self::request_has_embed(); } /** * @param mixed $content Post content. * @return bool */ public static function content_has_embed( $content ) { $content = (string) $content; if ( '' === $content ) { return false; } if ( false !== strpos( $content, '