PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.1
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / ai-copilot / hooks.php

hooks.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.1, at includes/ai-copilot/hooks.php

205 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — AI Copilot WordPress hooks.
4 *
5 * Intercepts comment inserts/edits, then schedules an async WP-Cron job
6 * to run the OpenAI spam/harmful analysis outside the current HTTP
7 * request. Moderation stays responsive even when the OpenAI API is slow.
8 *
9 * Comment analysis is the only auto-analysis the copilot performs — it
10 * feeds the comments-window spam score. Posts, pages, and taxonomy terms
11 * are NOT analyzed; the AI assistant finds them with native WordPress
12 * keyword search instead (see search.php).
13 *
14 * Deduplication: a 60-second transient (`desktop_mode_ai_q_{type}_{id}`) prevents
15 * the same comment from being queued twice when WordPress fires the hook
16 * multiple times in one request.
17 *
18 * @package WPDesktopMode
19 */
20
21 defined( 'ABSPATH' ) || exit;
22
23 // ---------------------------------------------------------------------------
24 // Helpers
25 // ---------------------------------------------------------------------------
26
27 /**
28 * Schedules an AI analysis job and ensures it runs even in environments
29 * where WP-Cron's HTTP-based spawn_cron() cannot reach the site
30 * (e.g. Docker dev setups where localhost:PORT doesn't resolve from
31 * inside the container).
32 *
33 * Two-track approach:
34 * 1. WP-Cron: reliable in production with a system cron or a host
35 * that can make loopback HTTP requests.
36 * 2. Shutdown handler: runs the job in the same PHP process, after
37 * the HTTP response has been sent to the browser via
38 * fastcgi_finish_request() (available in PHP-FPM, which Docker
39 * environments use). Falls back to running after the request in
40 * non-FPM setups (e.g. WP-CLI).
41 *
42 * The deduplication transient prevents the same entity from being
43 * queued and run twice within the guard window.
44 *
45 * @since 0.14.0
46 *
47 * @param string $hook Cron hook name, e.g. 'desktop_mode_ai_analyze_post'.
48 * @param array $args Arguments passed to the hook callback.
49 * @param string $dedup_key Unique string used to build the transient key.
50 */
51 function desktop_mode_ai_schedule_job( $hook, array $args, $dedup_key ) {
52 $transient = 'desktop_mode_ai_q_' . md5( $dedup_key );
53
54 if ( get_transient( $transient ) ) {
55 return; // Already queued within the guard window — skip.
56 }
57
58 // Schedule via WP-Cron for production environments.
59 wp_schedule_single_event( time(), $hook, $args );
60
61 // Mark as queued before the shutdown handler fires so re-entrant
62 // saves (e.g. a meta update during analysis) don't double-queue.
63 set_transient( $transient, 1, 120 );
64
65 // Run on shutdown — covers Docker dev environments and WP-CLI where
66 // WP-Cron's loopback HTTP request cannot reach the site.
67 // PHP_INT_MAX priority ensures we run last, after WordPress has
68 // finished any pending DB writes from the current request.
69 add_action(
70 'shutdown',
71 static function () use ( $hook, $args ) {
72 // Send the HTTP response to the browser before the
73 // (potentially slow) OpenAI call so the editor stays
74 // responsive. fastcgi_finish_request() is a PHP-FPM
75 // function; in other SAPIs (CLI, Apache mod_php) it is
76 // not available and we proceed without it — the analysis
77 // still runs, it just blocks the request exit briefly.
78 //
79 // The OpenAI HTTP call itself bumps `set_time_limit()`
80 // when (and only when) it is about to fire — see
81 // `desktop_mode_ai_do_request()` in `openai.php`.
82 // Bumping it here would widen the scope to every
83 // scheduled job whether or not it ends up hitting the
84 // remote API, which the WordPress.org plugin review
85 // guidelines discourage.
86 if ( function_exists( 'fastcgi_finish_request' ) ) {
87 fastcgi_finish_request();
88 }
89
90 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- generic dispatcher; caller passes a desktop_mode_* hook name.
91 do_action_ref_array( $hook, $args );
92 },
93 PHP_INT_MAX
94 );
95 }
96
97 /**
98 * Returns the user ID to attribute the API call to, trying three sources
99 * in priority order:
100 *
101 * 1. The currently logged-in user (HTTP request context).
102 * 2. A provided fallback ID (e.g. post author).
103 * 3. The first administrator who has AI enabled — covers anonymous
104 * comments, WP-CLI imports, and REST API requests without an
105 * authenticated user context.
106 *
107 * @since 0.14.0
108 *
109 * @param int $fallback_user_id Author/owner to try when no current user.
110 * @return int User ID, or 0 if no AI-enabled user could be found.
111 */
112 function desktop_mode_ai_resolve_user_id( $fallback_user_id = 0 ) {
113 $uid = get_current_user_id();
114 if ( $uid > 0 ) {
115 return $uid;
116 }
117
118 $fallback = (int) $fallback_user_id;
119 if ( $fallback > 0 ) {
120 return $fallback;
121 }
122
123 // Last resort: any administrator with AI configured. Scans the first
124 // 20 admins to avoid a full table scan on large sites.
125 return desktop_mode_ai_find_enabled_user();
126 }
127
128 /**
129 * Returns the first administrator user ID that has AI features enabled.
130 *
131 * Used as a last-resort fallback for anonymous comments, WP-CLI imports,
132 * and other contexts where no user session is available.
133 *
134 * @since 0.14.0
135 *
136 * @return int User ID, or 0 if none found.
137 */
138 function desktop_mode_ai_find_enabled_user() {
139 $admin_ids = get_users(
140 array(
141 'role' => 'administrator',
142 'number' => 20,
143 'fields' => 'ID',
144 )
145 );
146
147 foreach ( $admin_ids as $uid ) {
148 if ( desktop_mode_ai_is_enabled( (int) $uid ) ) {
149 return (int) $uid;
150 }
151 }
152
153 return 0;
154 }
155
156 // ---------------------------------------------------------------------------
157 // Comments
158 // ---------------------------------------------------------------------------
159
160 /**
161 * Shared handler for new and edited comments.
162 *
163 * @since 0.14.0
164 *
165 * @param int $comment_id The comment ID.
166 */
167 function desktop_mode_ai_on_comment_change( $comment_id ) {
168 $comment = get_comment( $comment_id );
169 if ( ! $comment instanceof WP_Comment ) {
170 return;
171 }
172
173 // Skip pingbacks and trackbacks — only analyze real human comments.
174 if ( '' !== $comment->comment_type && 'comment' !== $comment->comment_type ) {
175 return;
176 }
177
178 // Resolve the user: comment author user_id if logged-in, otherwise
179 // fall back to any admin who has AI configured. We use the comment's
180 // own user_id first since the commenter may have AI enabled; then
181 // fall back to current_user (moderator context), then to 0 (rejected).
182 $user_id = (int) $comment->user_id;
183 if ( $user_id <= 0 ) {
184 $user_id = desktop_mode_ai_resolve_user_id();
185 }
186 if ( $user_id <= 0 ) {
187 return;
188 }
189 if ( ! desktop_mode_ai_is_enabled( $user_id ) ) {
190 return;
191 }
192
193 desktop_mode_ai_schedule_job(
194 'desktop_mode_ai_analyze_comment',
195 array( $comment_id, $user_id ),
196 'comment_' . $comment_id
197 );
198 }
199
200 // `wp_insert_comment` fires after a new comment is inserted into the DB.
201 add_action( 'wp_insert_comment', 'desktop_mode_ai_on_comment_change', 20, 1 );
202
203 // `edit_comment` fires after an existing comment is updated.
204 add_action( 'edit_comment', 'desktop_mode_ai_on_comment_change', 20, 1 );
205