array( 'label'=>__( 'Cases & handover', 'logged-ai' ), 'eyebrow'=>__( 'KEEP THE REVIEW TOGETHER', 'logged-ai' ), 'title'=>__( 'Pick up the review where you left it.', 'logged-ai' ), 'intro'=>__( 'Keep related evidence, your notes and decisions in one case. Return to it yourself or let an authorized teammate continue.', 'logged-ai' ), 'free'=>__( 'Find and inspect individual records, including the actor, result and captured differences.', 'logged-ai' ), 'pro'=>__( 'Review supported associations in one case, add notes, assign the review and retain its decision history.', 'logged-ai' ), 'fit'=>__( 'Useful when a question involves several changes, you need to return later, or someone else needs the context.', 'logged-ai' ), 'steps'=>array( array(__( 'Plugin updated', 'logged-ai' ),'11:21 / configuration',__( 'A component update starts the review. Compare the recorded result with your planned maintenance.', 'logged-ai' ),__( 'A case keeps this record beside the related observations.', 'logged-ai' )), array(__( 'Settings changed', 'logged-ai' ),'11:40 / configuration',__( 'A later configuration change adds context. Its sequence alone does not establish a cause.', 'logged-ai' ),__( 'Keep the source fact separate from your interpretation.', 'logged-ai' )), array(__( 'Permissions changed', 'logged-ai' ),'11:45 / identity',__( 'Review the captured role difference and whether the access change was intended.', 'logged-ai' ),__( 'Record a decision against the case and return to its history.', 'logged-ai' )) ), ), 'alert' => array( 'label'=>__( 'Advanced alerts', 'logged-ai' ), 'eyebrow'=>__( 'SELECT THE CHANGE AND THE DESTINATION', 'logged-ai' ), 'title'=>__( 'Get selected alerts where your team works.', 'logged-ai' ), 'intro'=>__( 'Configure an email, Slack or HTTPS webhook destination and choose the supported activity that should trigger an alert.', 'logged-ai' ), 'free'=>__( 'Use the included critical security and failure notices. Routine activity notification emails are in Pro.', 'logged-ai' ), 'pro'=>__( 'Choose configurable rules and email, Slack or webhook destinations, then review recorded delivery results.', 'logged-ai' ), 'fit'=>__( 'Useful when a team needs selected notifications in its existing tools. Each destination needs explicit setup.', 'logged-ai' ), 'steps'=>array( array(__( 'Choose the change', 'logged-ai' ),__( '01 / illustrative rule', 'logged-ai' ),__( 'Select a supported permissions-change event and review the rule conditions.', 'logged-ai' ),__( 'Start with a change your team needs to review; this example saves no rule.', 'logged-ai' )), array(__( 'Choose the destination', 'logged-ai' ),__( '02 / configured channel', 'logged-ai' ),__( 'Select an email recipient, Slack incoming webhook or HTTPS webhook you control.', 'logged-ai' ),__( 'Review the destination and its privacy terms before enabling it. This example makes no connection.', 'logged-ai' )), array(__( 'Verify the result', 'logged-ai' ),__( '03 / delivery history', 'logged-ai' ),__( 'After setup, check the recorded delivery result and confirm receipt with the intended recipient.', 'logged-ai' ),__( 'A destination accepting a message does not prove a person received or read it. Background processing must be available.', 'logged-ai' )) ), ), 'report' => array( 'label'=>__( 'Scheduled reports', 'logged-ai' ), 'eyebrow'=>__( 'LESS REBUILDING THE SAME REPORT', 'logged-ai' ), 'title'=>__( 'Set up the review once. Reuse it next time.', 'logged-ai' ), 'intro'=>__( 'Save the filters for a client, store or team review, then schedule private reports from that saved search.', 'logged-ai' ), 'free'=>__( 'Filter the activity you need and export a private CSV or JSON record on demand.', 'logged-ai' ), 'pro'=>__( 'Reuse saved searches and generate private reports on a configured schedule, with optional report-ready notices.', 'logged-ai' ), 'fit'=>__( 'Useful for recurring client, store or internal reviews with a consistent scope.', 'logged-ai' ), 'steps'=>array( array(__( 'Save the scope', 'logged-ai' ),__( '01 / reusable search', 'logged-ai' ),__( 'Choose the evidence and filters that answer your recurring question.', 'logged-ai' ),__( 'Save that definition so the next review starts from the same scope.', 'logged-ai' )), array(__( 'Set the period', 'logged-ai' ),__( '02 / reporting window', 'logged-ai' ),__( 'Choose the supported period and inspect the retained evidence it contains.', 'logged-ai' ),__( 'A structured local summary explains the selected records and their limits.', 'logged-ai' )), array(__( 'Configure the repeat', 'logged-ai' ),__( '03 / scheduled review', 'logged-ai' ),__( 'Choose a daily or weekly schedule and, if needed, report-ready email notices. This example creates no schedule.', 'logged-ai' ),__( 'Background workers generate private reports. An authorized user still needs to open the report; a notice is not the report itself.', 'logged-ai' )) ), ), 'prioritize' => array( 'label'=>__( 'Review priorities', 'logged-ai' ), 'eyebrow'=>__( 'A REVIEW ORDER YOU CAN INSPECT', 'logged-ai' ), 'title'=>__( 'See the reasons behind the ranking.', 'logged-ai' ), 'intro'=>__( 'Start with a priority, then inspect the contributing source facts and available context.', 'logged-ai' ), 'free'=>__( 'Use recorded severity, filters and sample charts to choose which events to inspect.', 'logged-ai' ), 'pro'=>__( 'Inspect an experimental local ranking, its contribution breakdown and evaluation coverage.', 'logged-ai' ), 'fit'=>__( 'Useful when deciding where to begin across many supported events.', 'logged-ai' ), 'steps'=>array( array(__( 'Role contribution', 'logged-ai' ),__( '+45 / illustrative points', 'logged-ai' ),__( 'This example adds the built-in administrator role. The model treats it as a sensitive change.', 'logged-ai' ),__( 'Inspect the captured before/after roles before interpreting the score.', 'logged-ai' )), array(__( 'Case context', 'logged-ai' ),__( '+30 / illustrative points', 'logged-ai' ),__( 'An active high-priority case supplies additional context in this example.', 'logged-ai' ),__( 'Open the related evidence and inspect the relationship.', 'logged-ai' )), array(__( 'Reviewable priority', 'logged-ai' ),__( '75 / illustrative score', 'logged-ai' ),__( '45 + 30 = 75 in this example. A product priority is not the probability of an attack.', 'logged-ai' ),__( 'Coverage and unavailable context remain visible; the ranking is experimental.', 'logged-ai' )) ), ), ); } /** Register only local discovery and explicitly submitted personal preferences. */ public static function boot(): void { add_action( 'admin_menu', static function (): void { add_submenu_page( 'logged-ai', __( 'Explore Logged.ai Pro', 'logged-ai' ), esc_html__( 'Explore Pro', 'logged-ai' ), 'read', 'logged-ai-plans', array( self::class, 'page' ) ); }, 20 ); add_action( 'wp_ajax_logged_ai_discovery_preference', array( self::class, 'save_preference' ) ); } /** Fixed workflow keys; never carry evidence, filter values or recipient details. */ private static function featured(): array { return array( 'report', 'alert', 'investigate' ); } /** Commercial state is read locally; a status refresh cannot earn activation. */ public static function state(): string { if ( ! defined( 'LOGGED_AI_PRO_FILE' ) ) { return defined( 'WP_PLUGIN_DIR' ) && is_file( WP_PLUGIN_DIR . '/logged-ai-pro/logged-ai-pro.php' ) ? 'installed' : 'free'; } return class_exists( '\\LoggedAI\\Pro\\ProAccess' ) && \LoggedAI\Pro\ProAccess::unlocked() ? 'active' : 'locked'; } /** Only absolute HTTPS purchase URLs, without credentials. */ public static function purchase_url( mixed $candidate ): string { if ( ! is_string( $candidate ) || ! filter_var( $candidate, FILTER_VALIDATE_URL ) ) { return ''; } $parts = wp_parse_url( $candidate ); return is_array( $parts ) && 'https' === strtolower( $parts['scheme'] ?? '' ) && ! empty( $parts['host'] ) && ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) ? $candidate : ''; } /** Only already registered, authorized current-site destinations. */ public static function destinations( array $groups, string $base ): array { if ( is_network_admin() ) { return array(); } $found = array(); foreach ( $groups as $group ) { $found += $group['pages']; } $out = array(); $routes = array( 'report' => array( 'logged-ai-reports', __( 'Open reports', 'logged-ai' ), array( 'logged_ai_view_events', 'logged_ai_manage_investigations', 'logged_ai_export_events' ) ), 'alert' => array( 'logged-ai-advanced-alerts', __( 'Open advanced alerts', 'logged-ai' ), array( 'logged_ai_view_events', 'logged_ai_manage_investigations', 'logged_ai_manage_alerts' ) ), 'investigate' => array( 'logged-ai-cases', __( 'Open cases', 'logged-ai' ), array( 'logged_ai_view_events', 'logged_ai_manage_investigations' ) ), 'prioritize' => array( 'logged-ai-investigations', __( 'Open priorities', 'logged-ai' ), array( 'logged_ai_view_events', 'logged_ai_manage_investigations' ) ), ); foreach ( $routes as $key => $route ) { $allowed = isset( $found[ $route[0] ] ); foreach ( $route[2] as $capability ) { $allowed = $allowed && current_user_can( $capability ); } if ( $allowed ) { $out[ $key ] = array( 'url' => add_query_arg( 'page', $route[0], $base ), 'label' => $route[1] ); } } return $out; } /** Resolve a purchase, activation or real workflow action without performing it. */ public static function actions( array $destinations ): array { if ( is_network_admin() ) { return array(); } $state = self::state(); $purchase = self::purchase_url( defined( 'LOGGED_AI_PRO_PURCHASE_URL' ) ? constant( 'LOGGED_AI_PRO_PURCHASE_URL' ) : '' ); $purchase = $purchase ?: 'https://logged.ai/pricing/'; $labels = array( 'report' => __( 'Get Pro for scheduled reports', 'logged-ai' ), 'alert' => __( 'Get Pro for advanced alerts', 'logged-ai' ), 'investigate' => __( 'Get Pro for cases & handover', 'logged-ai' ), ); $can_activate = false; if ( 'locked' === $state && class_exists( '\\LoggedAI\\Pro\\CommercialSettings' ) ) { $can_activate = true; foreach ( \LoggedAI\Pro\CommercialSettings::caps() as $capability ) { $can_activate = $can_activate && current_user_can( $capability ); } $groups = Navigation::available( false ); $can_activate = $can_activate && isset( $groups['settings']['pages']['logged-ai-commercial'] ); } $actions = array(); foreach ( self::featured() as $key ) { if ( 'free' === $state ) { $actions[ $key ] = array( 'url' => $purchase, 'label' => $labels[ $key ], 'kind' => 'purchase' ); } elseif ( 'installed' === $state && current_user_can( is_multisite() ? 'manage_network_plugins' : 'activate_plugins' ) ) { $actions[ $key ] = array( 'url' => is_multisite() ? network_admin_url( 'plugins.php' ) : admin_url( 'plugins.php' ), 'label' => __( 'Activate Pro', 'logged-ai' ), 'kind' => 'activate' ); } elseif ( $can_activate ) { $actions[ $key ] = array( 'url' => admin_url( 'admin.php?page=logged-ai-commercial' ), 'label' => __( 'Activate Pro', 'logged-ai' ), 'kind' => 'activate' ); } elseif ( 'active' === $state && isset( $destinations[ $key ] ) && self::valid_destination( $key, $destinations[ $key ] ) ) { $actions[ $key ] = $destinations[ $key ] + array( 'kind' => 'open' ); } } return $actions; } /** Match an internal action to its current authorized route, never a supplied redirect. */ private static function valid_destination( string $workflow, array $destination ): bool { $available = self::destinations( Navigation::available( false ), admin_url( 'admin.php' ) ); return isset( $available[ $workflow ] ) && $destination === $available[ $workflow ]; } /** Personal UI preferences are site-scoped WordPress user options. */ private static function preferences(): array { return array( 'suggestions' => '1' === get_user_option( 'logged_ai_hide_pro_suggestions' ), 'previews' => '1' === get_user_option( 'logged_ai_hide_pro_previews' ), ); } private static function can_save_preferences(): bool { return ! is_network_admin() && current_user_can( 'logged_ai_manage_settings' ); } private static function preference_nonce(): string { return 'logged_ai_discovery:' . get_current_blog_id() . ':' . get_current_user_id(); } /** Save exactly one current-administrator preference; never plugin configuration. */ public static function save_preference(): void { if ( 'POST' !== AdminInput::method() || ! self::can_save_preferences() || ! AdminInput::nonce_shape( 'nonce' ) || ! AdminInput::nonce_shape( '_ajax_nonce' ) || ! AdminInput::nonce_shape() ) { wp_send_json_error( array( 'message' => __( 'Your preference could not be saved. Reload and try again.', 'logged-ai' ) ), 403 ); } check_ajax_referer( self::preference_nonce(), 'nonce' ); $preference = isset( $_POST['preference'] ) && is_string( $_POST['preference'] ) ? sanitize_key( wp_unslash( $_POST['preference'] ) ) : ''; $value = isset( $_POST['value'] ) && is_string( $_POST['value'] ) ? sanitize_text_field( wp_unslash( $_POST['value'] ) ) : ''; if ( ! in_array( $preference, array( 'suggestions', 'previews' ), true ) || ! in_array( $value, array( '0', '1' ), true ) ) { wp_send_json_error( array( 'message' => __( 'Choose a valid display preference.', 'logged-ai' ) ), 400 ); } $key = 'logged_ai_hide_pro_' . $preference; if ( $value !== get_user_option( $key ) && ! update_user_option( get_current_user_id(), $key, $value, false ) ) { wp_send_json_error( array( 'message' => __( 'Your preference could not be saved. Reload and try again.', 'logged-ai' ) ), 500 ); } wp_send_json_success( array( 'preferences' => self::preferences() ) ); } private static function preview_url( string $workflow ): string { return add_query_arg( array( 'page' => 'logged-ai-plans', 'workflow' => $workflow ), admin_url( 'admin.php' ) ) . '#lai-flow-' . $workflow . '-page'; } /** Optional examples; their personal switch remains available when the links are hidden. */ public static function navigation(): void { if ( is_network_admin() || ! current_user_can( 'read' ) || 'active' === self::state() ) { return; } $preferences = self::preferences(); $can_save = self::can_save_preferences(); echo '
' . esc_html__( 'Save recurring work, choose team alerts or keep an investigation together. Free includes critical security and failure notices and on-demand private exports.', 'logged-ai' ) . '
' . esc_html__( 'These choices apply only to your administrator account on this site. They do not change collection, alerts or reports.', 'logged-ai' ) . '
'; foreach ( array( 'suggestions' => array( __( 'Hide suggestions', 'logged-ai' ), __( 'Restore suggestions', 'logged-ai' ) ), 'previews' => array( __( 'Hide Pro previews', 'logged-ai' ), __( 'Restore Pro previews', 'logged-ai' ) ) ) as $key => $labels ) { foreach ( $labels as $index => $label ) { echo ' '; } } echo '