self::MENU_SLUG ); $log_id = (int) $log_id; if ( $log_id > 0 ) { $args['apwpm_log'] = $log_id; } $url = add_query_arg( $args, admin_url( 'admin.php' ) ); if ( $log_id > 0 ) { $url .= '#timeline'; } return $url; } /** * Whether a user may open a specific log (e.g. from a mention email). * * @param int $user_id User ID. * @param int $log_id Log row ID. * @return bool */ public static function user_can_view_log( $user_id, $log_id ) { $user_id = (int) $user_id; $log_id = (int) $log_id; if ( $user_id <= 0 || $log_id <= 0 ) { return false; } if ( user_can( $user_id, 'manage_options' ) ) { return true; } $user = get_userdata( $user_id ); if ( ! $user ) { return false; } $needle = '@' . $user->user_login; foreach ( (array) APWPM_DB::get_comments_for_log( $log_id ) as $comment ) { if ( (int) $comment->user_id === $user_id ) { return true; } if ( false !== stripos( (string) $comment->body, $needle ) ) { return true; } } return (bool) APWPM_Compat::filter( 'apwpm_user_can_view_log', 'ap_user_can_view_log', false, $user_id, $log_id ); } /** * Whether the current user may load the Activity Timeline admin app. * * @param int $log_id Optional log ID from ?apwpm_log= for mention deep links. * @return bool */ public static function can_access_admin( $log_id = 0 ) { if ( ! is_user_logged_in() ) { return false; } if ( current_user_can( 'manage_options' ) ) { return true; } $log_id = (int) $log_id; if ( $log_id > 0 && self::user_can_view_log( get_current_user_id(), $log_id ) ) { return true; } return (bool) APWPM_Compat::filter( 'apwpm_can_access_timeline', 'ap_can_access_timeline', false, $log_id ); } public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } public function register() { add_action( 'admin_init', array( $this, 'redirect_legacy_timeline_url' ) ); add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_filter( 'plugin_action_links_' . APWPM_PLUGIN_BASENAME, array( $this, 'plugin_action_links' ) ); } /** * Old Slack/email links used page=activitypilot; redirect to the real menu slug. */ public function redirect_legacy_timeline_url() { $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( ! is_admin() || empty( $page ) ) { return; } $page = sanitize_key( (string) $page ); if ( self::LEGACY_MENU_SLUG !== $page ) { return; } $log_id = (int) filter_input( INPUT_GET, 'apwpm_log', FILTER_VALIDATE_INT ); if ( ! self::can_access_admin( $log_id ) ) { return; } wp_safe_redirect( self::timeline_url( $log_id ) ); exit; } public function register_menu() { add_menu_page( __( 'Activity Timeline', 'activitypilot' ), __( 'Activity Timeline', 'activitypilot' ), 'manage_options', self::MENU_SLUG, array( $this, 'render_app' ), $this->menu_icon(), 30 ); add_submenu_page( self::MENU_SLUG, __( 'Timeline', 'activitypilot' ), __( 'Timeline', 'activitypilot' ), 'manage_options', self::MENU_SLUG, array( $this, 'render_app' ) ); add_submenu_page( self::MENU_SLUG, __( 'Analytics', 'activitypilot' ), __( 'Analytics', 'activitypilot' ), 'manage_options', self::MENU_SLUG . '#analytics', array( $this, 'render_app' ) ); add_submenu_page( self::MENU_SLUG, __( 'Reports', 'activitypilot' ), __( 'Reports', 'activitypilot' ), 'manage_options', self::MENU_SLUG . '#reports', array( $this, 'render_app' ) ); add_submenu_page( self::MENU_SLUG, __( 'Settings', 'activitypilot' ), __( 'Settings', 'activitypilot' ), 'manage_options', self::MENU_SLUG . '#settings', array( $this, 'render_app' ) ); } public function plugin_action_links( $links ) { $dashboard = '' . esc_html__( 'Timeline', 'activitypilot' ) . ''; array_unshift( $links, $dashboard ); return $links; } /** * Inline SVG (data URI) for the menu icon — a small timeline glyph. */ protected function menu_icon() { $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); } public function enqueue_assets( $hook ) { if ( false === strpos( (string) $hook, self::MENU_SLUG ) ) { return; } wp_enqueue_style( 'apwpm-admin', APWPM_PLUGIN_URL . 'admin/css/apwpm-admin.css', array(), APWPM_VERSION ); wp_enqueue_style( 'apwpm-reports', APWPM_PLUGIN_URL . 'assets/css/apwpm-reports.css', array(), APWPM_VERSION ); wp_enqueue_script( 'apwpm-admin', APWPM_PLUGIN_URL . 'admin/js/apwpm-admin.js', array( 'wp-element', 'wp-i18n', 'wp-api-fetch' ), APWPM_VERSION, true ); wp_localize_script( 'apwpm-admin', 'APWPM', array( 'rest' => esc_url_raw( rest_url( APWPM_REST_API::NAMESPACE . '/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'multisite' => is_multisite(), 'isNetwork' => false, 'sites' => is_multisite() && class_exists( 'APWPM_Multisite' ) ? APWPM_Multisite::list_sites() : array(), 'currentUserId' => get_current_user_id(), 'canManage' => current_user_can( 'manage_options' ), 'reportsCssUrl' => APWPM_PLUGIN_URL . 'assets/css/apwpm-reports.css', 'i18n' => self::i18n_strings(), ) ); wp_set_script_translations( 'apwpm-admin', 'activitypilot' ); } /** * Centralised translation map — shared with the multisite/network UI. */ public static function i18n_strings() { return array( 'title' => __( 'ActivityPilot for WP', 'activitypilot' ), 'subtitle' => __( 'Track, visualize and audit every change happening on your site.', 'activitypilot' ), 'timeline' => __( 'Timeline', 'activitypilot' ), 'dashboard' => __( 'Dashboard', 'activitypilot' ), 'insights' => __( 'Smart Insights', 'activitypilot' ), 'settings' => __( 'Settings', 'activitypilot' ), 'search' => __( 'Search description, user, title…', 'activitypilot' ), 'filterAction' => __( 'Action', 'activitypilot' ), 'filterObject' => __( 'Object', 'activitypilot' ), 'filterSeverity' => __( 'Severity', 'activitypilot' ), 'filterSite' => __( 'Site', 'activitypilot' ), 'all' => __( 'All', 'activitypilot' ), 'dateFrom' => __( 'From', 'activitypilot' ), 'dateTo' => __( 'To', 'activitypilot' ), 'apply' => __( 'Apply', 'activitypilot' ), 'reset' => __( 'Reset', 'activitypilot' ), 'exportCsv' => __( 'Export CSV', 'activitypilot' ), 'exportJson' => __( 'Export JSON', 'activitypilot' ), 'noResults' => __( 'No activity matches your filters yet.', 'activitypilot' ), 'loading' => __( 'Loading…', 'activitypilot' ), 'prev' => __( 'Prev', 'activitypilot' ), 'next' => __( 'Next', 'activitypilot' ), 'showing' => __( 'Showing', 'activitypilot' ), 'of' => __( 'of', 'activitypilot' ), 'errors24h' => __( 'Errors (24h)', 'activitypilot' ), 'events24h' => __( 'Events (24h)', 'activitypilot' ), 'events7d' => __( 'Events (7d)', 'activitypilot' ), 'recentErrors' => __( 'Recent errors (1h)', 'activitypilot' ), 'anomalies7d' => __( 'Anomalies (7d)', 'activitypilot' ), 'topActions' => __( 'Top actions (7d)', 'activitypilot' ), 'activityTrend' => __( 'Activity trend (14d)', 'activitypilot' ), 'recentInsights' => __( 'Recent smart insights', 'activitypilot' ), 'noInsights' => __( 'No anomalies detected yet — your site looks healthy.', 'activitypilot' ), 'rollback' => __( 'Rollback', 'activitypilot' ), 'rolledBack' => __( 'Rolled back', 'activitypilot' ), 'confirmRollback' => __( 'Are you sure you want to revert this action? This will create a new audit entry.', 'activitypilot' ), 'rollbackSuccess' => __( 'Action successfully rolled back.', 'activitypilot' ), 'rollbackFailed' => __( 'Rollback failed.', 'activitypilot' ), 'retention' => __( 'Retention (days)', 'activitypilot' ), 'breakDetect' => __( 'Break detection', 'activitypilot' ), 'errorThresh' => __( 'Error threshold', 'activitypilot' ), 'errorWindow' => __( 'Error window (minutes)', 'activitypilot' ), 'notifyEmail' => __( 'Alert email', 'activitypilot' ), 'enableAlerts' => __( 'Email alerts', 'activitypilot' ), 'enableInsights' => __( 'AI-powered anomaly detection', 'activitypilot' ), 'enableRollback' => __( 'Enable rollback for reversible events', 'activitypilot' ), 'enableErrorLog' => __( 'Capture PHP errors & fatals', 'activitypilot' ), 'advanced' => __( 'Advanced features', 'activitypilot' ), 'tracking' => __( 'Event tracking', 'activitypilot' ), 'save' => __( 'Save changes', 'activitypilot' ), 'saved' => __( 'Settings saved.', 'activitypilot' ), 'purge' => __( 'Purge all logs', 'activitypilot' ), 'confirmPurge' => __( 'This will permanently delete every activity log entry. Continue?', 'activitypilot' ), 'logLogins' => __( 'Logins & logouts', 'activitypilot' ), 'logPostChanges' => __( 'Post & page changes', 'activitypilot' ), 'logPluginChanges' => __( 'Plugin install/activate', 'activitypilot' ), 'logThemeChanges' => __( 'Theme changes', 'activitypilot' ), 'logUserChanges' => __( 'User profile & roles', 'activitypilot' ), 'logOptionChanges' => __( 'Critical site options', 'activitypilot' ), 'network' => __( 'Network', 'activitypilot' ), 'perSite' => __( 'Activity per site (7d)', 'activitypilot' ), // v1.3 additions. 'analytics' => __( 'Analytics', 'activitypilot' ), 'reports' => __( 'Reports', 'activitypilot' ), 'heatmap' => __( 'Activity heatmap', 'activitypilot' ), 'byHour' => __( 'Activity by hour of day', 'activitypilot' ), 'topUsers' => __( 'Most active users', 'activitypilot' ), 'report' => __( 'Activity report', 'activitypilot' ), 'printPdf' => __( 'Print / PDF', 'activitypilot' ), 'live' => __( 'Live', 'activitypilot' ), 'newEvents' => __( 'new events', 'activitypilot' ), 'darkMode' => __( 'Dark mode', 'activitypilot' ), 'lightMode' => __( 'Light mode', 'activitypilot' ), 'security' => __( 'Security & integrity', 'activitypilot' ), 'integrations' => __( 'Integrations', 'activitypilot' ), 'webhooks' => __( 'Webhooks & notifications', 'activitypilot' ), 'digests' => __( 'Scheduled reports', 'activitypilot' ), 'frequency' => __( 'Frequency', 'activitypilot' ), 'minSeverity' => __( 'Minimum severity to dispatch', 'activitypilot' ), 'enableWebhooks' => __( 'Enable webhooks', 'activitypilot' ), 'enableDigest' => __( 'Email activity digest', 'activitypilot' ), 'enableHashChain' => __( 'Tamper-proof hash chain', 'activitypilot' ), 'enableSudo' => __( 'Sudo mode for destructive actions', 'activitypilot' ), 'enableImmutable' => __( 'Append-only / immutable mode', 'activitypilot' ), 'enableFileMonitor' => __( 'File integrity monitoring', 'activitypilot' ), 'enableIpThrottle' => __( 'IP brute-force throttling', 'activitypilot' ), 'enablePiiRedact' => __( 'PII redaction', 'activitypilot' ), 'enableGeo' => __( 'Login geolocation', 'activitypilot' ), 'enableAsync' => __( 'Async batched logging', 'activitypilot' ), 'ipThrottleLimit' => __( 'Failed-login limit', 'activitypilot' ), 'ipThrottleWindow' => __( 'Window (minutes)', 'activitypilot' ), 'verifyChain' => __( 'Verify hash chain', 'activitypilot' ), 'chainIntact' => __( 'Hash chain intact', 'activitypilot' ), 'chainBroken' => __( 'Chain broken at row #', 'activitypilot' ), 'sudoPrompt' => __( 'Re-enter your password to continue:', 'activitypilot' ), 'sudoBad' => __( 'Wrong password. Action cancelled.', 'activitypilot' ), 'ack' => __( 'Ack', 'activitypilot' ), 'resolve' => __( 'Resolve', 'activitypilot' ), 'reopen' => __( 'Reopen', 'activitypilot' ), 'assignMe' => __( 'Assign to me', 'activitypilot' ), 'assigned' => __( 'Assigned', 'activitypilot' ), 'statusChanged' => __( 'Status updated', 'activitypilot' ), 'comments' => __( 'Comments', 'activitypilot' ), 'noComments' => __( 'No comments yet.', 'activitypilot' ), 'commentPlaceholder' => __( 'Add a comment… type @ to mention a user', 'activitypilot' ), 'post' => __( 'Post', 'activitypilot' ), 'savedViews' => __( 'Saved views', 'activitypilot' ), 'saveView' => __( 'Save current view', 'activitypilot' ), 'viewName' => __( 'Name this view:', 'activitypilot' ), 'viewSaved' => __( 'View saved', 'activitypilot' ), 'selected' => __( 'selected', 'activitypilot' ), 'updated' => __( 'updated', 'activitypilot' ), 'shortcuts' => __( 'Keyboard shortcuts', 'activitypilot' ), 'clear' => __( 'Clear', 'activitypilot' ), 'filteringFor' => __( 'Filtering analytics for', 'activitypilot' ), 'unsavedChanges' => __( 'You have unsaved changes', 'activitypilot' ), 'allSaved' => __( 'All changes saved', 'activitypilot' ), 'discard' => __( 'Discard', 'activitypilot' ), 'saving' => __( 'Saving…', 'activitypilot' ), 'unreadMentions' => __( 'unread mentions', 'activitypilot' ), 'enableMentionEmail' => __( 'Email users when @mentioned in comments', 'activitypilot' ), 'pinnedEvent' => __( 'Pinned event', 'activitypilot' ), 'unpin' => __( 'Unpin', 'activitypilot' ), 'eventNotFound' => __( 'Event not found or no longer available.', 'activitypilot' ), ); } public function render_app() { $log_id = (int) filter_input( INPUT_GET, 'apwpm_log', FILTER_VALIDATE_INT ); if ( ! self::can_access_admin( $log_id ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'activitypilot' ) ); } echo '
'; } }