rest_url( self::REST_NS . '/analytics' ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'dashboardUrl' => admin_url( 'admin.php?page=' . self::PAGE_SLUG ), 'settingsUrl' => admin_url( 'options-general.php?page=' . Causewise_Settings::PAGE_SLUG ), ); } /** * Top-level analytics screen. Tracking settings stay under Settings. */ public static function register_page() { add_menu_page( __( 'CauseWise Analytics', 'causewise-analytics' ), __( 'CauseWise', 'causewise-analytics' ), Causewise_Settings::CAPABILITY, self::PAGE_SLUG, array( __CLASS__, 'render_page' ), 'dashicons-chart-area', 30 ); } /** * Print the mount point for the React dashboard. */ public static function render_page() { if ( ! self::can_view() ) { wp_die( esc_html__( 'Sorry, you are not allowed to view CauseWise Analytics.', 'causewise-analytics' ), 403 ); } $range = '7d'; if ( isset( $_GET['range'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view filter. The value is sanitized and is not saved. $range = Causewise_Api::sanitize_range( sanitize_text_field( wp_unslash( $_GET['range'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view filter. The value is sanitized and is not saved. } ?>
id ) || 'dashboard' !== $screen->id ) { return $hidden; } return array_values( array_diff( $hidden, self::widget_ids() ) ); } /** * Dashboard widgets. Registered late so cleanup on wp_dashboard_setup * does not drop them. wp_add_dashboard_widget adds nothing when the * current screen is missing. */ public static function register_widgets() { if ( ! self::can_view() ) { return; } self::ensure_dashboard_screen(); wp_add_dashboard_widget( 'causewise_stats', __( 'CauseWise Stats', 'causewise-analytics' ), array( __CLASS__, 'render_stats_widget' ), null, null, 'normal', 'high' ); $widgets = array( 'causewise_pageviews' => array( 'title' => __( 'CauseWise Page Views', 'causewise-analytics' ), 'variant' => 'pageviews', 'context' => 'side', ), 'causewise_sessions' => array( 'title' => __( 'CauseWise Sessions', 'causewise-analytics' ), 'variant' => 'sessions', 'context' => 'side', ), 'causewise_traffic' => array( 'title' => __( 'CauseWise Traffic', 'causewise-analytics' ), 'variant' => 'traffic', 'context' => 'normal', ), 'causewise_top_pages' => array( 'title' => __( 'CauseWise Top Pages', 'causewise-analytics' ), 'variant' => 'pages', 'context' => 'normal', ), ); foreach ( $widgets as $widget_id => $widget ) { wp_add_dashboard_widget( $widget_id, $widget['title'], static function () use ( $widget ) { Causewise_Admin::render_widget( $widget['variant'] ); }, static function () use ( $widget_id ) { Causewise_Admin::render_control( $widget_id ); }, null, $widget['context'] ); } } /** * Saved date range for one widget. * * @param string $widget_id Widget ID. * @return string */ public static function widget_range( $widget_id ) { $options = get_option( 'dashboard_widget_options', array() ); $range = '7d'; if ( is_array( $options ) && isset( $options[ $widget_id ]['range'] ) ) { $range = $options[ $widget_id ]['range']; } return Causewise_Api::sanitize_range( $range ); } /** * Combined stats widget for the main Dashboard column. */ public static function render_stats_widget() { $range = self::widget_range( 'causewise_stats' ); echo ''; echo ''; } /** * Compact widget body plus a link to the full screen. * * @param string $variant pageviews, sessions, traffic, or pages. */ public static function render_widget( $variant ) { $widget_id = self::widget_id_for_variant( $variant ); $range = self::widget_range( $widget_id ); echo ''; echo ''; } /** * Date-range control shown by the core Configure link. * * @param string $widget_id Widget ID. */ public static function render_control( $widget_id ) { $method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' === $method && isset( $_POST['widget_id'] ) ) { $posted_id = sanitize_text_field( wp_unslash( $_POST['widget_id'] ) ); if ( $posted_id === $widget_id ) { check_admin_referer( 'edit-dashboard-widget_' . $widget_id, 'dashboard-widget-nonce' ); $posted_ranges = isset( $_POST['causewise_range'] ) ? wp_unslash( $_POST['causewise_range'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each selected value is sanitized before it is saved. $posted_range = '7d'; if ( is_array( $posted_ranges ) && isset( $posted_ranges[ $widget_id ] ) ) { $posted_range = sanitize_text_field( $posted_ranges[ $widget_id ] ); } $options = get_option( 'dashboard_widget_options', array() ); if ( ! is_array( $options ) ) { $options = array(); } if ( ! isset( $options[ $widget_id ] ) || ! is_array( $options[ $widget_id ] ) ) { $options[ $widget_id ] = array(); } $options[ $widget_id ]['range'] = Causewise_Api::sanitize_range( $posted_range ); update_option( 'dashboard_widget_options', $options ); } } $range = self::widget_range( $widget_id ); $choices = array( 'today' => __( 'Today', 'causewise-analytics' ), '7d' => __( 'Last 7 days', 'causewise-analytics' ), '30d' => __( 'Last 30 days', 'causewise-analytics' ), ); echo ''; echo ''; } /** * Load the dashboard bundle on the Home screen and the analytics screen. * * @param string $hook Current admin page hook. */ public static function enqueue( $hook ) { if ( ! self::can_view() ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $on_dashboard = 'index.php' === $hook || ( is_object( $screen ) && isset( $screen->id ) && 'dashboard' === $screen->id ); $on_analytics = 'toplevel_page_' . self::PAGE_SLUG === $hook; if ( ! $on_dashboard && ! $on_analytics ) { return; } wp_enqueue_style( 'causewise-analytics-admin', plugins_url( 'assets/shell.css', CAUSEWISE_ANALYTICS_FILE ), array(), CAUSEWISE_ANALYTICS_VERSION ); wp_enqueue_script( 'causewise-analytics-admin', plugins_url( 'assets/dashboard.js', CAUSEWISE_ANALYTICS_FILE ), array(), CAUSEWISE_ANALYTICS_VERSION, true ); wp_localize_script( 'causewise-analytics-admin', 'causewiseAnalytics', self::client_config() ); } /** * Authenticated proxy. The management key never leaves PHP. */ public static function register_rest() { register_rest_route( self::REST_NS, '/analytics', array( 'methods' => 'GET', 'permission_callback' => array( __CLASS__, 'can_view' ), 'callback' => array( __CLASS__, 'rest_analytics' ), 'args' => array( 'range' => array( 'type' => 'string', 'required' => false, 'sanitize_callback' => array( 'Causewise_Api', 'sanitize_range' ), ), 'view' => array( 'type' => 'string', 'required' => false, ), 'filter' => array( 'type' => 'string', 'required' => false, ), 'cursor' => array( 'type' => 'string', 'required' => false, ), 'limit' => array( 'type' => 'integer', 'required' => false, ), 'section' => array( 'type' => 'string', 'required' => false, ), ), ) ); $layout_args = array( 'methods' => 'GET', 'permission_callback' => array( __CLASS__, 'can_view' ), 'callback' => array( __CLASS__, 'rest_get_layout' ), ); register_rest_route( self::REST_NS, '/dashboard-layout', $layout_args ); register_rest_route( self::REST_NS, '/dashboard-layout', array( 'methods' => 'POST', 'permission_callback' => array( __CLASS__, 'can_view' ), 'callback' => array( __CLASS__, 'rest_save_layout' ), ) ); register_rest_route( self::REST_NS, '/dashboard-layout', array( 'methods' => 'DELETE', 'permission_callback' => array( __CLASS__, 'can_view' ), 'callback' => array( __CLASS__, 'rest_reset_layout' ), ) ); } /** * @param mixed $request REST request. * @return mixed */ public static function rest_analytics( $request ) { $range = '7d'; $view = 'dashboard'; $filter = 'all'; $cursor = ''; $limit = 20; $section = ''; if ( is_object( $request ) && method_exists( $request, 'get_param' ) ) { $range = Causewise_Api::sanitize_range( $request->get_param( 'range' ) ); $view = (string) $request->get_param( 'view' ); $filter = Causewise_Api::sanitize_event_filter( $request->get_param( 'filter' ) ); $cursor = Causewise_Api::sanitize_event_cursor( $request->get_param( 'cursor' ) ); $limit = Causewise_Api::sanitize_event_limit( $request->get_param( 'limit' ) ); $raw = $request->get_param( 'section' ); if ( is_string( $raw ) && '' !== trim( $raw ) ) { $section = Causewise_Embeds::sanitize_kind( $raw ); if ( '' === $section ) { return self::rest_result( array( 'ok' => false, 'status' => 400, 'code' => 'invalid_section', 'message' => __( 'Choose a CauseWise analytics block.', 'causewise-analytics' ), ) ); } } } if ( '' !== $section ) { $display = 8; if ( is_object( $request ) && method_exists( $request, 'get_param' ) ) { $display = Causewise_Embeds::sanitize_limit( $request->get_param( 'limit' ) ); } $result = Causewise_Embeds::load( $range, $section, $display ); } elseif ( 'events' === $view ) { $result = Causewise_Api::load_events( $range, $filter, $cursor, $limit ); } else { $view = 'overview' === $view ? 'overview' : 'dashboard'; $result = Causewise_Api::load( $range, $view ); } return self::rest_result( $result ); } /** * Saved layout for the current user and one dashboard instance. * * @param mixed $request REST request. * @return mixed */ public static function rest_get_layout( $request ) { $denied = self::layout_read_denied(); if ( null !== $denied ) { return $denied; } $instance = self::layout_instance_from_request( $request ); if ( '' === $instance ) { return self::layout_error( 400, 'invalid_instance', __( 'Choose a dashboard.', 'causewise-analytics' ) ); } return self::rest_result( array( 'ok' => true, 'status' => 200, 'instance' => $instance, 'layout' => self::read_user_layout( $instance ), ) ); } /** * Replace the current user's layout for one dashboard. The user id in the body is ignored. * * @param mixed $request REST request. * @return mixed */ public static function rest_save_layout( $request ) { $denied = self::layout_write_denied( $request ); if ( null !== $denied ) { return $denied; } $instance = self::layout_instance_from_request( $request ); $layout = self::sanitize_stored_layout( self::request_value( $request, 'layout' ) ); if ( '' === $instance || null === $layout ) { return self::layout_error( 400, 'invalid_layout', __( 'The dashboard layout could not be saved.', 'causewise-analytics' ) ); } self::write_user_layout( $instance, $layout ); return self::rest_result( array( 'ok' => true, 'status' => 200, 'instance' => $instance, 'layout' => $layout, ) ); } /** * Remove the current user's layout for one dashboard. * * @param mixed $request REST request. * @return mixed */ public static function rest_reset_layout( $request ) { $denied = self::layout_write_denied( $request ); if ( null !== $denied ) { return $denied; } $instance = self::layout_instance_from_request( $request ); if ( '' === $instance ) { return self::layout_error( 400, 'invalid_instance', __( 'Choose a dashboard.', 'causewise-analytics' ) ); } self::delete_user_layout( $instance ); return self::rest_result( array( 'ok' => true, 'status' => 200, 'instance' => $instance, 'layout' => null, ) ); } /** * @param mixed $layout Raw preference. * @return array{v: int, panels: array