403 ) ); } return $this->verify_rest_nonce( $request ); } /** * Read access to a single log row (admins or users @mentioned on that event). */ public function permission_check_log( WP_REST_Request $request ) { $log_id = (int) $request['id']; if ( ! class_exists( 'APWPM_Admin' ) || ! APWPM_Admin::can_access_admin( $log_id ) ) { return new WP_Error( 'apwpm_forbidden', __( 'You are not allowed to access this resource.', 'activitypilot' ), array( 'status' => 403 ) ); } return $this->verify_rest_nonce( $request ); } /** * @param WP_REST_Request $request Request. * @return true|WP_Error */ protected function verify_rest_nonce( WP_REST_Request $request ) { $nonce = $request->get_header( 'x_wp_nonce' ); if ( $nonce && ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { return new WP_Error( 'apwpm_bad_nonce', __( 'Invalid nonce.', 'activitypilot' ), array( 'status' => 403 ) ); } return true; } /** * Tighter gate for destructive / cross-blog operations. */ public function permission_check_network( WP_REST_Request $request ) { if ( ! is_multisite() || ! current_user_can( 'manage_network' ) ) { return new WP_Error( 'apwpm_forbidden', __( 'Network admin required.', 'activitypilot' ), array( 'status' => 403 ) ); } return $this->verify_rest_nonce( $request ); } public function register_routes() { register_rest_route( self::NAMESPACE, '/logs', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_logs' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => array( 'page' => array( 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint' ), 'per_page' => array( 'type' => 'integer', 'default' => 25, 'sanitize_callback' => 'absint' ), 'user_id' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ), 'action_type' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key' ), 'object_type' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key' ), 'severity' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key' ), 'search' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ), 'date_from' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ), 'date_to' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ), 'blog_id' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ), 'anomalies' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ), 'status' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key' ), 'assignee_id' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ), 'country' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ), 'since_id' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ), 'orderby' => array( 'type' => 'string', 'default' => 'created_at', 'sanitize_callback' => 'sanitize_key' ), 'order' => array( 'type' => 'string', 'default' => 'DESC', 'sanitize_callback' => 'sanitize_key' ), ), ) ); // Views (saved filters). register_rest_route( self::NAMESPACE, '/views', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_views' ), 'permission_callback' => array( $this, 'permission_check' ) ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_view' ), 'permission_callback' => array( $this, 'permission_check' ) ), ) ); register_rest_route( self::NAMESPACE, '/views/(?P\d+)', array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_view' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); // Workflow. register_rest_route( self::NAMESPACE, '/workflow/status/(?P\d+)', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'set_status' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); register_rest_route( self::NAMESPACE, '/workflow/assign/(?P\d+)', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'set_assignee' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); register_rest_route( self::NAMESPACE, '/comments/(?P\d+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'list_comments' ), 'permission_callback' => array( $this, 'permission_check' ) ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_comment' ), 'permission_callback' => array( $this, 'permission_check' ) ), ) ); register_rest_route( self::NAMESPACE, '/comments/(?P\d+)/(?P\d+)', array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_comment' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); // Webhook test. register_rest_route( self::NAMESPACE, '/webhook-test', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'webhook_test' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); // Sudo (re-auth). register_rest_route( self::NAMESPACE, '/sudo', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'sudo_status' ), 'permission_callback' => array( $this, 'permission_check' ) ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'sudo_confirm' ), 'permission_callback' => array( $this, 'permission_check' ) ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'sudo_revoke' ), 'permission_callback' => array( $this, 'permission_check' ) ), ) ); // Hash chain verify. register_rest_route( self::NAMESPACE, '/integrity/verify', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'integrity_verify' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); // Heatmap / hour-of-day / drilldown analytics. register_rest_route( self::NAMESPACE, '/analytics', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_analytics' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => array( 'days' => array( 'type' => 'integer', 'default' => 14, 'sanitize_callback' => 'absint' ), 'user_id' => array( 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint' ), ), ) ); // Report (HTML, print to PDF). register_rest_route( self::NAMESPACE, '/report', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_report' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => array( 'period' => array( 'type' => 'string', 'default' => 'weekly', 'sanitize_callback' => 'sanitize_key' ), ), ) ); // Bulk operations. register_rest_route( self::NAMESPACE, '/bulk', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'bulk' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); // Single log by ID (for deep links / email CTAs). register_rest_route( self::NAMESPACE, '/logs/(?P\d+)', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_log' ), 'permission_callback' => array( $this, 'permission_check_log' ), 'args' => array( 'id' => array( 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint' ), ), ) ); // User search (for @mention autocomplete). register_rest_route( self::NAMESPACE, '/users/search', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'search_users' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => array( 'q' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ), 'limit' => array( 'type' => 'integer', 'default' => 8, 'sanitize_callback' => 'absint' ), ), ) ); // Unread @mentions for the current user. register_rest_route( self::NAMESPACE, '/mentions', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_mentions' ), 'permission_callback' => array( $this, 'permission_check' ) ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'clear_mentions' ), 'permission_callback' => array( $this, 'permission_check' ) ), ) ); register_rest_route( self::NAMESPACE, '/insights', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_insights' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => array( 'limit' => array( 'type' => 'integer', 'default' => 10, 'sanitize_callback' => 'absint' ), ), ) ); register_rest_route( self::NAMESPACE, '/rollback/(?P\d+)', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rollback' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => array( 'id' => array( 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint' ), ), ) ); register_rest_route( self::NAMESPACE, '/network', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_network' ), 'permission_callback' => array( $this, 'permission_check_network' ), 'args' => array( 'days' => array( 'type' => 'integer', 'default' => 7, 'sanitize_callback' => 'absint' ), ), ) ); register_rest_route( self::NAMESPACE, '/stats', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_stats' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => array( 'days' => array( 'type' => 'integer', 'default' => 14, 'sanitize_callback' => 'absint' ), ), ) ); register_rest_route( self::NAMESPACE, '/filters', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_filters' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); register_rest_route( self::NAMESPACE, '/export', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'export' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => array( 'format' => array( 'type' => 'string', 'default' => 'csv', 'sanitize_callback' => 'sanitize_key' ), ), ) ); register_rest_route( self::NAMESPACE, '/purge', array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'purge' ), 'permission_callback' => array( $this, 'permission_check' ), ) ); register_rest_route( self::NAMESPACE, '/settings', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_settings' ), 'permission_callback' => array( $this, 'permission_check' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_settings' ), 'permission_callback' => array( $this, 'permission_check' ), ), ) ); } public function get_logs( WP_REST_Request $request ) { $args = array( 'page' => $request->get_param( 'page' ), 'per_page' => $request->get_param( 'per_page' ), 'user_id' => $request->get_param( 'user_id' ), 'action_type' => $request->get_param( 'action_type' ), 'object_type' => $request->get_param( 'object_type' ), 'severity' => $request->get_param( 'severity' ), 'search' => $request->get_param( 'search' ), 'date_from' => $request->get_param( 'date_from' ), 'date_to' => $request->get_param( 'date_to' ), 'blog_id' => $request->get_param( 'blog_id' ), 'anomalies' => $request->get_param( 'anomalies' ), 'status' => $request->get_param( 'status' ), 'assignee_id' => $request->get_param( 'assignee_id' ), 'country' => $request->get_param( 'country' ), 'since_id' => $request->get_param( 'since_id' ), 'orderby' => $request->get_param( 'orderby' ), 'order' => $request->get_param( 'order' ), ); $result = APWPM_DB::get_logs( $args ); // Look up comment counts for the visible log IDs in a single query. $ids = array_map( function ( $r ) { return (int) $r['id']; }, $result['items'] ); $comment_counts = APWPM_Workflow::counts_for_logs( $ids ); foreach ( $result['items'] as &$row ) { $row['context_decoded'] = ! empty( $row['context'] ) ? json_decode( $row['context'], true ) : null; $row['human_time_diff'] = human_time_diff( strtotime( $row['created_at'] ), current_time( 'timestamp' ) ) . ' ago'; $row['avatar'] = ! empty( $row['user_id'] ) ? get_avatar_url( (int) $row['user_id'], array( 'size' => 64 ) ) : ''; $row['can_rollback'] = class_exists( 'APWPM_Rollback' ) ? APWPM_Rollback::can_rollback( $row ) : false; $row['assignee'] = ( ! empty( $row['assignee_id'] ) ) ? ( get_userdata( (int) $row['assignee_id'] )->user_login ?? '' ) : ''; $row['comment_count'] = isset( $comment_counts[ (int) $row['id'] ] ) ? (int) $comment_counts[ (int) $row['id'] ] : 0; } return rest_ensure_response( $result ); } /* -------------------------- Sudo -------------------------- */ protected function require_sudo() { if ( class_exists( 'APWPM_Sudo' ) && APWPM_Sudo::enabled() && ! APWPM_Sudo::is_active() ) { return new WP_Error( 'apwpm_sudo_required', __( 'Re-authentication required for this action.', 'activitypilot' ), array( 'status' => 412 ) ); } return true; } public function sudo_status() { return rest_ensure_response( array( 'enabled' => APWPM_Sudo::enabled(), 'active' => APWPM_Sudo::is_active(), ) ); } public function sudo_confirm( WP_REST_Request $request ) { $pwd = (string) $request->get_param( 'password' ); $ok = APWPM_Sudo::confirm_password( $pwd ); return rest_ensure_response( array( 'success' => (bool) $ok ) ); } public function sudo_revoke() { APWPM_Sudo::revoke(); return rest_ensure_response( array( 'success' => true ) ); } /* -------------------------- Views -------------------------- */ public function get_views() { $views = APWPM_Views::for_user( get_current_user_id() ); foreach ( $views as &$v ) { $v->filters = json_decode( $v->filters, true ); } return rest_ensure_response( $views ); } public function create_view( WP_REST_Request $request ) { $name = (string) $request->get_param( 'name' ); $filters = $request->get_param( 'filters' ); $id = APWPM_Views::create( get_current_user_id(), $name, $filters ); if ( is_wp_error( $id ) ) { return $id; } return rest_ensure_response( array( 'id' => $id ) ); } public function delete_view( WP_REST_Request $request ) { $ok = APWPM_Views::delete( get_current_user_id(), (int) $request->get_param( 'id' ) ); return rest_ensure_response( array( 'success' => (bool) $ok ) ); } /* -------------------------- Workflow -------------------------- */ public function set_status( WP_REST_Request $request ) { $result = APWPM_Workflow::set_status( (int) $request->get_param( 'id' ), (string) $request->get_param( 'status' ) ); if ( is_wp_error( $result ) ) { return $result; } return rest_ensure_response( array( 'success' => true ) ); } public function set_assignee( WP_REST_Request $request ) { $result = APWPM_Workflow::assign( (int) $request->get_param( 'id' ), (int) $request->get_param( 'assignee_id' ) ); if ( is_wp_error( $result ) ) { return $result; } return rest_ensure_response( array( 'success' => true ) ); } public function list_comments( WP_REST_Request $request ) { return rest_ensure_response( APWPM_Workflow::get_comments( (int) $request->get_param( 'id' ) ) ); } public function create_comment( WP_REST_Request $request ) { $body = (string) $request->get_param( 'body' ); $id = APWPM_Workflow::add_comment( (int) $request->get_param( 'id' ), $body ); if ( is_wp_error( $id ) ) { return $id; } return rest_ensure_response( array( 'id' => $id ) ); } public function delete_comment( WP_REST_Request $request ) { $ok = APWPM_Workflow::delete_comment( (int) $request->get_param( 'cid' ) ); return rest_ensure_response( array( 'success' => $ok ) ); } /* -------------------------- Single log -------------------------- */ public function get_log( WP_REST_Request $request ) { $id = (int) $request->get_param( 'id' ); $row = APWPM_DB::get_row( $id ); if ( ! $row ) { return new WP_Error( 'apwpm_not_found', __( 'Event not found.', 'activitypilot' ), array( 'status' => 404 ) ); } $row = (array) $row; $row['context_decoded'] = ! empty( $row['context'] ) ? json_decode( $row['context'], true ) : null; $row['human_time_diff'] = human_time_diff( strtotime( $row['created_at'] ), current_time( 'timestamp' ) ) . ' ago'; $row['avatar'] = ! empty( $row['user_id'] ) ? get_avatar_url( (int) $row['user_id'], array( 'size' => 64 ) ) : ''; $row['can_rollback'] = class_exists( 'APWPM_Rollback' ) ? APWPM_Rollback::can_rollback( $row ) : false; $row['assignee'] = ( ! empty( $row['assignee_id'] ) ) ? ( get_userdata( (int) $row['assignee_id'] )->user_login ?? '' ) : ''; $counts = APWPM_Workflow::counts_for_logs( array( $id ) ); $row['comment_count'] = isset( $counts[ $id ] ) ? (int) $counts[ $id ] : 0; return rest_ensure_response( $row ); } /* -------------------------- User search -------------------------- */ public function search_users( WP_REST_Request $request ) { $q = (string) $request->get_param( 'q' ); $limit = max( 1, min( 20, (int) $request->get_param( 'limit' ) ) ); $args = array( 'number' => $limit, 'fields' => array( 'ID', 'user_login', 'display_name', 'user_email' ), ); if ( $q !== '' ) { $args['search'] = '*' . esc_attr( $q ) . '*'; $args['search_columns'] = array( 'user_login', 'display_name', 'user_email' ); } else { // No query — return most recently active editors. $args['orderby'] = 'registered'; $args['order'] = 'DESC'; } $users = get_users( $args ); $out = array(); foreach ( $users as $u ) { $out[] = array( 'id' => (int) $u->ID, 'login' => $u->user_login, 'name' => $u->display_name, 'avatar' => get_avatar_url( $u->ID, array( 'size' => 32 ) ), ); } return rest_ensure_response( $out ); } /* -------------------------- Mentions -------------------------- */ public function get_mentions() { return rest_ensure_response( array( 'unread' => APWPM_Workflow::unread_mentions(), ) ); } public function clear_mentions() { APWPM_Workflow::clear_mention_count(); return rest_ensure_response( array( 'success' => true ) ); } /* -------------------------- Webhooks -------------------------- */ public function webhook_test( WP_REST_Request $request ) { $channel = sanitize_key( (string) $request->get_param( 'channel' ) ); $result = APWPM_Webhooks::send_test( $channel ?: 'slack' ); if ( is_wp_error( $result ) ) { return $result; } $code = wp_remote_retrieve_response_code( $result ); return rest_ensure_response( array( 'success' => $code >= 200 && $code < 300, 'status_code' => (int) $code, ) ); } /* -------------------------- Integrity -------------------------- */ public function integrity_verify( WP_REST_Request $request ) { $limit = (int) $request->get_param( 'limit' ); return rest_ensure_response( APWPM_Integrity::verify( $limit ?: 5000 ) ); } /* -------------------------- Analytics -------------------------- */ public function get_analytics( WP_REST_Request $request ) { $days = (int) $request->get_param( 'days' ); $user_id = (int) $request->get_param( 'user_id' ); $table = APWPM_DB::table(); $cutoff = APWPM_DB::cutoff( $days ?: 14, 'DAY' ); $heat = APWPM_DB::get_analytics_heatmap( $table, $user_id, $cutoff ); $hod = APWPM_DB::get_analytics_by_hour( $table, $user_id, $cutoff ); $users = APWPM_DB::get_analytics_top_users( $table, $cutoff ); return rest_ensure_response( array( 'heatmap' => $heat, 'by_hour' => $hod, 'top_users' => $users, ) ); } /* -------------------------- Reports -------------------------- */ public function get_report( WP_REST_Request $request ) { $period = (string) $request->get_param( 'period' ); $report = APWPM_Reports::build_report( $period ); return rest_ensure_response( array( 'report' => $report, 'html' => APWPM_Reports::render_html( $report ), ) ); } /* -------------------------- Bulk -------------------------- */ public function bulk( WP_REST_Request $request ) { $ids = array_map( 'intval', (array) $request->get_param( 'ids' ) ); $action = sanitize_key( (string) $request->get_param( 'action' ) ); if ( in_array( $action, array( 'delete', 'purge' ), true ) ) { $sudo = $this->require_sudo(); if ( is_wp_error( $sudo ) ) { return $sudo; } } $count = 0; foreach ( $ids as $id ) { switch ( $action ) { case 'acknowledge': APWPM_Workflow::set_status( $id, 'acknowledged' ); $count++; break; case 'resolve': APWPM_Workflow::set_status( $id, 'resolved' ); $count++; break; case 'reopen': APWPM_Workflow::set_status( $id, 'open' ); $count++; break; case 'delete': if ( false !== APWPM_Compat::filter( 'apwpm_can_prune', 'ap_can_prune', true ) ) { APWPM_DB::delete_log_by_id( $id ); $count++; } break; } } return rest_ensure_response( array( 'success' => true, 'count' => $count, 'action' => $action ) ); } public function get_insights( WP_REST_Request $request ) { $limit = (int) $request->get_param( 'limit' ); return rest_ensure_response( APWPM_Insights::recent_anomalies( $limit ) ); } public function rollback( WP_REST_Request $request ) { $sudo = $this->require_sudo(); if ( is_wp_error( $sudo ) ) { return $sudo; } $settings = (array) get_option( 'apwpm_settings', array() ); if ( empty( $settings['enable_rollback'] ) ) { return new WP_Error( 'apwpm_disabled', __( 'Rollback feature is disabled.', 'activitypilot' ), array( 'status' => 400 ) ); } $id = (int) $request->get_param( 'id' ); $result = APWPM_Rollback::execute( $id ); if ( empty( $result['success'] ) ) { return new WP_Error( 'apwpm_rollback_failed', $result['message'], array( 'status' => 400 ) ); } return rest_ensure_response( $result ); } public function get_network( WP_REST_Request $request ) { $days = (int) $request->get_param( 'days' ); return rest_ensure_response( array( 'is_multisite' => is_multisite(), 'sites' => class_exists( 'APWPM_Multisite' ) ? APWPM_Multisite::list_sites() : array(), 'per_blog' => APWPM_DB::counts_by_blog( $days ), 'totals' => array( 'total' => $this->count_in_period( $days, 'DAY' ), 'errors' => $this->count_errors_in_period( $days ), 'anomalies' => $this->count_anomalies_in_period( $days ), ), ) ); } protected function count_errors_in_period( $days ) { return APWPM_DB::count_errors_since( $days ); } protected function count_anomalies_in_period( $days ) { if ( ! in_array( 'is_anomaly', APWPM_DB::columns(), true ) ) { return 0; } return APWPM_DB::count_anomalies_since( $days ); } public function get_stats( WP_REST_Request $request ) { $days = (int) $request->get_param( 'days' ); return rest_ensure_response( array( 'daily' => APWPM_DB::get_daily_stats( $days ), 'recent_errors' => APWPM_DB::count_recent_errors( 60 ), 'total_24h' => $this->count_in_period( 24, 'HOUR' ), 'total_7d' => $this->count_in_period( 7, 'DAY' ), 'top_actions' => $this->top_actions(), ) ); } protected function count_in_period( $amount, $unit ) { return APWPM_DB::count_since( $amount, $unit ); } protected function top_actions() { return APWPM_DB::top_actions_since( 7, 6 ); } public function get_filters() { return rest_ensure_response( array( 'actions' => APWPM_DB::get_distinct( 'action_type' ), 'objects' => APWPM_DB::get_distinct( 'object_type' ), 'severities' => APWPM_Logger::SEVERITIES, ) ); } public function export( WP_REST_Request $request ) { $format = $request->get_param( 'format' ); $args = array( 'page' => 1, 'per_page' => 5000, 'user_id' => (int) $request->get_param( 'user_id' ), 'action_type' => sanitize_key( (string) $request->get_param( 'action_type' ) ), 'object_type' => sanitize_key( (string) $request->get_param( 'object_type' ) ), 'severity' => sanitize_key( (string) $request->get_param( 'severity' ) ), 'search' => sanitize_text_field( (string) $request->get_param( 'search' ) ), 'date_from' => sanitize_text_field( (string) $request->get_param( 'date_from' ) ), 'date_to' => sanitize_text_field( (string) $request->get_param( 'date_to' ) ), ); $result = APWPM_DB::get_logs( $args ); APWPM_Logger::log( array( 'action_type' => 'logs_exported', 'object_type' => 'export', 'description' => sprintf( /* translators: 1: format, 2: count */ __( 'Exported %2$d log entries as %1$s.', 'activitypilot' ), $format, count( $result['items'] ) ), 'severity' => 'info', ) ); return APWPM_Exporter::respond( $result['items'], $format ); } public function purge( WP_REST_Request $request ) { $sudo = $this->require_sudo(); if ( is_wp_error( $sudo ) ) { return $sudo; } APWPM_DB::truncate(); APWPM_Logger::log( array( 'action_type' => 'logs_purged', 'object_type' => 'log', 'description' => __( 'All activity logs were purged.', 'activitypilot' ), 'severity' => 'critical', ) ); return rest_ensure_response( array( 'success' => true ) ); } public function get_settings() { return rest_ensure_response( get_option( 'apwpm_settings', array() ) ); } public function update_settings( WP_REST_Request $request ) { $incoming = $request->get_json_params(); if ( ! is_array( $incoming ) ) { return new WP_Error( 'apwpm_bad_payload', __( 'Expected JSON object.', 'activitypilot' ), array( 'status' => 400 ) ); } $current = (array) get_option( 'apwpm_settings', array() ); $bool_keys = array( 'log_logins', 'log_post_changes', 'log_plugin_changes', 'log_theme_changes', 'log_user_changes', 'log_option_changes', 'break_detection', 'enable_alerts', 'enable_insights', 'enable_rollback', 'enable_error_log', 'enable_async', 'enable_hashchain', 'enable_sudo', 'enable_file_monitor', 'enable_ip_throttle', 'enable_pii_redact', 'enable_immutable', 'enable_geo', 'enable_woocommerce', 'enable_forms', 'enable_seo', 'enable_webhooks', 'enable_digest', 'enable_live', 'enable_mention_email', ); foreach ( $bool_keys as $k ) { if ( isset( $incoming[ $k ] ) ) { $current[ $k ] = ! empty( $incoming[ $k ] ) ? 1 : 0; } } if ( isset( $incoming['retention_days'] ) ) { $current['retention_days'] = max( 1, min( 3650, (int) $incoming['retention_days'] ) ); } if ( isset( $incoming['error_threshold'] ) ) { $current['error_threshold'] = max( 1, min( 1000, (int) $incoming['error_threshold'] ) ); } if ( isset( $incoming['error_window_min'] ) ) { $current['error_window_min'] = max( 1, min( 1440, (int) $incoming['error_window_min'] ) ); } if ( isset( $incoming['notify_email'] ) ) { $email = sanitize_email( (string) $incoming['notify_email'] ); if ( is_email( $email ) ) { $current['notify_email'] = $email; } } $url_keys = array( 'webhook_slack', 'webhook_discord', 'webhook_teams', 'webhook_generic' ); foreach ( $url_keys as $k ) { if ( isset( $incoming[ $k ] ) ) { $current[ $k ] = esc_url_raw( (string) $incoming[ $k ] ); } } if ( isset( $incoming['webhook_secret'] ) ) { $current['webhook_secret'] = sanitize_text_field( (string) $incoming['webhook_secret'] ); } if ( isset( $incoming['webhook_min_sev'] ) ) { $sev = sanitize_key( (string) $incoming['webhook_min_sev'] ); if ( in_array( $sev, APWPM_Logger::SEVERITIES, true ) ) { $current['webhook_min_sev'] = $sev; } } if ( isset( $incoming['digest_freq'] ) ) { $f = sanitize_key( (string) $incoming['digest_freq'] ); if ( in_array( $f, array( 'daily', 'weekly', 'monthly' ), true ) ) { $current['digest_freq'] = $f; } } if ( isset( $incoming['ip_throttle_limit'] ) ) { $current['ip_throttle_limit'] = max( 3, min( 100, (int) $incoming['ip_throttle_limit'] ) ); } if ( isset( $incoming['ip_throttle_window'] ) ) { $current['ip_throttle_window'] = max( 1, min( 240, (int) $incoming['ip_throttle_window'] ) ); } if ( isset( $incoming['retention_per_sev'] ) && is_array( $incoming['retention_per_sev'] ) ) { $clean = array(); foreach ( $incoming['retention_per_sev'] as $sev => $days ) { $sev = sanitize_key( $sev ); if ( in_array( $sev, APWPM_Logger::SEVERITIES, true ) ) { $clean[ $sev ] = max( 0, min( 3650, (int) $days ) ); } } $current['retention_per_sev'] = $clean; } update_option( 'apwpm_settings', $current ); APWPM_Logger::log( array( 'action_type' => 'settings_updated', 'object_type' => 'setting', 'description' => __( 'Activity Timeline settings were updated.', 'activitypilot' ), 'severity' => 'info', ) ); return rest_ensure_response( $current ); } }