PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.0
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 / agents / identity.php

identity.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.0.0, at includes/agents/identity.php

254 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Agents: identity layer (synthetic WordPress users).
4 *
5 * Each agent has a real row in `wp_users` so capability checks, edit
6 * locks, comment attribution, and the standard WP audit trail work
7 * without a parallel ACL. The row is "synthetic" only in that every
8 * login and session path is blocked — the agent never authenticates;
9 * it is invoked on the site's behalf.
10 *
11 * Those blocks, and `openstation_agent_is_agent()` itself, live in
12 * guard.php, which loads unconditionally. This file owns the row
13 * lifecycle (create / delete) and the identity surface: the bot avatar
14 * and the wp-admin Users list "Type" column, so administrators can
15 * tell synthetic accounts apart.
16 *
17 * Definition meta constants live in store.php.
18 *
19 * @package OpenStation
20 */
21
22 defined( 'ABSPATH' ) || exit;
23
24 require_once OPENSTATION_DIR . 'includes/agents/guard.php';
25
26 /**
27 * Resolve a unique `user_login` for an agent given its desired slug.
28 *
29 * Returns the input prefixed with `agent-`, or appends a numeric suffix
30 * if a user with that login already exists.
31 *
32 * @param string $slug Sanitized slug.
33 * @return string
34 */
35 function openstation_agent_resolve_unique_login( $slug ) {
36 $base = 'agent-' . $slug;
37 $login = $base;
38 $counter = 1;
39 while ( username_exists( $login ) ) {
40 ++$counter;
41 $login = $base . '-' . $counter;
42 }
43 return $login;
44 }
45
46 /**
47 * Build a synthetic, RFC-shaped email for an agent.
48 *
49 * The address is never sent to — it just satisfies `wp_insert_user`'s
50 * schema validation and reserves the slot so `email_exists()` stays
51 * unique across agents.
52 *
53 * @param string $slug Sanitized agent slug.
54 * @return string
55 */
56 function openstation_agent_synthetic_email( $slug ) {
57 $host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
58 if ( ! is_string( $host ) || '' === $host ) {
59 $host = 'invalid.local';
60 }
61 $email = $slug . '@agents.' . $host;
62 $counter = 1;
63 while ( email_exists( $email ) ) {
64 ++$counter;
65 $email = $slug . '+' . $counter . '@agents.' . $host;
66 }
67 return $email;
68 }
69
70 /**
71 * Create a synthetic agent user row. Definition meta is written by the
72 * `openstation_agent_create()` orchestrator in store.php — call that,
73 * not this, unless you only need the bare row.
74 *
75 * @param array{name:string, role:string, slug?:string} $args Agent
76 * creation args. `role` MUST be one of the site's registered
77 * roles. `slug` defaults to `sanitize_title( $name )`.
78 * @return WP_User|WP_Error
79 */
80 function openstation_agent_create_user( $args ) {
81 $name = isset( $args['name'] ) ? trim( (string) $args['name'] ) : '';
82 $role = isset( $args['role'] ) ? sanitize_key( $args['role'] ) : '';
83 $slug = isset( $args['slug'] ) ? sanitize_title( $args['slug'] ) : '';
84
85 if ( '' === $name ) {
86 return new WP_Error(
87 'openstation_agent_invalid_name',
88 __( 'Agent name is required.', 'desktop-mode' )
89 );
90 }
91
92 $roles = wp_roles()->get_names();
93 if ( '' === $role || ! isset( $roles[ $role ] ) ) {
94 return new WP_Error(
95 'openstation_agent_invalid_role',
96 __( 'Pick a valid WordPress role for the agent.', 'desktop-mode' )
97 );
98 }
99
100 if ( '' === $slug ) {
101 $slug = sanitize_title( $name );
102 }
103 if ( '' === $slug ) {
104 return new WP_Error(
105 'openstation_agent_invalid_slug',
106 __( 'Agent slug could not be derived from the name.', 'desktop-mode' )
107 );
108 }
109
110 $user_id = wp_insert_user(
111 array(
112 'user_login' => openstation_agent_resolve_unique_login( $slug ),
113 'user_email' => openstation_agent_synthetic_email( $slug ),
114 'user_pass' => wp_generate_password( 64, true, true ),
115 'display_name' => $name,
116 'nickname' => $name,
117 'role' => $role,
118 'show_admin_bar_front' => false,
119 )
120 );
121
122 if ( is_wp_error( $user_id ) ) {
123 return $user_id;
124 }
125
126 update_user_meta( $user_id, OPENSTATION_AGENT_USER_MARKER_META, '1' );
127
128 return new WP_User( $user_id );
129 }
130
131 /**
132 * Delete an agent user. Definition meta rows die with the user
133 * (`wp_delete_user()` removes all usermeta). Content the agent
134 * authored is NOT reassigned — pass a reassign id when the caller
135 * wants to keep it.
136 *
137 * @param int $user_id Agent user id.
138 * @param int|null $reassign Optional user id to reassign authored content to.
139 * @return true|WP_Error
140 */
141 function openstation_agent_delete( $user_id, $reassign = null ) {
142 if ( ! openstation_agent_is_agent( $user_id ) ) {
143 return new WP_Error(
144 'openstation_agent_not_an_agent',
145 __( 'User is not a OpenStation agent.', 'desktop-mode' )
146 );
147 }
148
149 if ( ! function_exists( 'wp_delete_user' ) ) {
150 require_once ABSPATH . 'wp-admin/includes/user.php';
151 }
152
153 $deleted = wp_delete_user( (int) $user_id, $reassign );
154 if ( ! $deleted ) {
155 return new WP_Error(
156 'openstation_agent_delete_failed',
157 __( 'Could not delete the agent user.', 'desktop-mode' )
158 );
159 }
160
161 /**
162 * Fires after an agent is deleted.
163 *
164 * @param int $user_id Agent user id (row no longer exists when this fires).
165 * @param int $actor_id User who deleted the agent.
166 */
167 do_action( 'openstation_agent_deleted', (int) $user_id, get_current_user_id() );
168
169 return true;
170 }
171
172 // ---------------------------------------------------------------------------
173 // Identity surface
174 // ---------------------------------------------------------------------------
175
176 /**
177 * URL of the bot avatar as a real static file.
178 *
179 * The avatar MUST be a file URL, not the data URI: consumers routinely
180 * run avatar URLs through `esc_url()` (wp-admin's `get_avatar()`, the
181 * desktop user-tile renderer), and `data` is not in
182 * `wp_allowed_protocols()` — the data URI silently becomes an empty
183 * string and the avatar renders broken.
184 *
185 * @return string
186 */
187 function openstation_agent_avatar_url() {
188 return OPENSTATION_URL . 'assets/images/agent-avatar.svg';
189 }
190
191 /**
192 * Substitute the bot glyph for agent avatars across the WP admin.
193 *
194 * @param array $args Args being assembled by `get_avatar_data()`.
195 * @param int|string|WP_User|WP_Comment $id_or_email Identifier the caller passed.
196 * @return array
197 */
198 function openstation_agent_avatar( $args, $id_or_email ) {
199 $user_id = 0;
200 if ( is_numeric( $id_or_email ) ) {
201 $user_id = (int) $id_or_email;
202 } elseif ( $id_or_email instanceof WP_User ) {
203 $user_id = (int) $id_or_email->ID;
204 } elseif ( $id_or_email instanceof WP_Comment ) {
205 $user_id = (int) $id_or_email->user_id;
206 } elseif ( is_string( $id_or_email ) && is_email( $id_or_email ) ) {
207 $user = get_user_by( 'email', $id_or_email );
208 if ( $user ) {
209 $user_id = (int) $user->ID;
210 }
211 }
212
213 if ( $user_id > 0 && openstation_agent_is_agent( $user_id ) ) {
214 $args['url'] = openstation_agent_avatar_url();
215 $args['found_avatar'] = true;
216 }
217 return $args;
218 }
219 add_filter( 'pre_get_avatar_data', 'openstation_agent_avatar', 10, 2 );
220
221 /**
222 * Add a "Type" column to the wp-admin Users list that labels agents.
223 *
224 * @param string[] $columns Existing column id => label map.
225 * @return string[]
226 */
227 function openstation_agent_users_columns( $columns ) {
228 $columns['openstation_agent_type'] = __( 'Type', 'desktop-mode' );
229 return $columns;
230 }
231 add_filter( 'manage_users_columns', 'openstation_agent_users_columns' );
232
233 /**
234 * Render the cell for the "Type" column.
235 *
236 * @param string $output Existing rendered HTML.
237 * @param string $column_name Column id.
238 * @param int $user_id User id being rendered.
239 * @return string
240 */
241 function openstation_agent_users_custom_column( $output, $column_name, $user_id ) {
242 if ( 'openstation_agent_type' !== $column_name ) {
243 return $output;
244 }
245 if ( openstation_agent_is_agent( $user_id ) ) {
246 return '<span class="os-agent-type" aria-label="' . esc_attr__( 'OpenStation agent', 'desktop-mode' ) . '">'
247 . '<span class="dashicons dashicons-superhero" aria-hidden="true"></span> '
248 . esc_html__( 'Agent', 'desktop-mode' )
249 . '</span>';
250 }
251 return '<span class="os-agent-type-human">' . esc_html__( 'Person', 'desktop-mode' ) . '</span>';
252 }
253 add_filter( 'manage_users_custom_column', 'openstation_agent_users_custom_column', 10, 3 );
254