PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
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 0.8.6 All 33 releases
desktop-mode / includes / agents / identity.php

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

259 lines 8.2 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 // On multisite `wp_delete_user()` only removes the user from the
154 // CURRENT site — the network account (and the agent's meta with
155 // it) lives on. The agent's whole identity is its wp user, so
156 // deleting the agent means the network-wide delete.
157 if ( is_multisite() ) {
158 if ( ! function_exists( 'wpmu_delete_user' ) ) {
159 require_once ABSPATH . 'wp-admin/includes/ms.php';
160 }
161 if ( null !== $reassign ) {
162 // `wpmu_delete_user()` has no reassign parameter; hand the
163 // current site's content over before the user goes.
164 wp_delete_user( (int) $user_id, $reassign );
165 }
166 $deleted = wpmu_delete_user( (int) $user_id );
167 } else {
168 $deleted = wp_delete_user( (int) $user_id, $reassign );
169 }
170 if ( ! $deleted ) {
171 return new WP_Error(
172 'openstation_agent_delete_failed',
173 __( 'Could not delete the agent user.', 'desktop-mode' )
174 );
175 }
176
177 /**
178 * Fires after an agent is deleted.
179 *
180 * @param int $user_id Agent user id (row no longer exists when this fires).
181 * @param int $actor_id User who deleted the agent.
182 */
183 do_action( 'openstation_agent_deleted', (int) $user_id, get_current_user_id() );
184
185 return true;
186 }
187
188 // ---------------------------------------------------------------------------
189 // Identity surface
190 //
191 // `openstation_agent_avatar_url()` lives in bootstrap.php: the WP
192 // Explorer integration needs it for the entity icon and loads while the
193 // feature flag is off, when this file does not.
194 // ---------------------------------------------------------------------------
195
196 /**
197 * Substitute the bot glyph for agent avatars across the WP admin.
198 *
199 * @param array $args Args being assembled by `get_avatar_data()`.
200 * @param int|string|WP_User|WP_Comment $id_or_email Identifier the caller passed.
201 * @return array
202 */
203 function openstation_agent_avatar( $args, $id_or_email ) {
204 $user_id = 0;
205 if ( is_numeric( $id_or_email ) ) {
206 $user_id = (int) $id_or_email;
207 } elseif ( $id_or_email instanceof WP_User ) {
208 $user_id = (int) $id_or_email->ID;
209 } elseif ( $id_or_email instanceof WP_Comment ) {
210 $user_id = (int) $id_or_email->user_id;
211 } elseif ( is_string( $id_or_email ) && is_email( $id_or_email ) ) {
212 $user = get_user_by( 'email', $id_or_email );
213 if ( $user ) {
214 $user_id = (int) $user->ID;
215 }
216 }
217
218 if ( $user_id > 0 && openstation_agent_is_agent( $user_id ) ) {
219 $args['url'] = openstation_agent_avatar_url( $user_id );
220 $args['found_avatar'] = true;
221 }
222 return $args;
223 }
224 add_filter( 'pre_get_avatar_data', 'openstation_agent_avatar', 10, 2 );
225
226 /**
227 * Add a "Type" column to the wp-admin Users list that labels agents.
228 *
229 * @param string[] $columns Existing column id => label map.
230 * @return string[]
231 */
232 function openstation_agent_users_columns( $columns ) {
233 $columns['openstation_agent_type'] = __( 'Type', 'desktop-mode' );
234 return $columns;
235 }
236 add_filter( 'manage_users_columns', 'openstation_agent_users_columns' );
237
238 /**
239 * Render the cell for the "Type" column.
240 *
241 * @param string $output Existing rendered HTML.
242 * @param string $column_name Column id.
243 * @param int $user_id User id being rendered.
244 * @return string
245 */
246 function openstation_agent_users_custom_column( $output, $column_name, $user_id ) {
247 if ( 'openstation_agent_type' !== $column_name ) {
248 return $output;
249 }
250 if ( openstation_agent_is_agent( $user_id ) ) {
251 return '<span class="os-agent-type" aria-label="' . esc_attr__( 'OpenStation agent', 'desktop-mode' ) . '">'
252 . '<span class="dashicons dashicons-superhero" aria-hidden="true"></span> '
253 . esc_html__( 'Agent', 'desktop-mode' )
254 . '</span>';
255 }
256 return '<span class="os-agent-type-human">' . esc_html__( 'Person', 'desktop-mode' ) . '</span>';
257 }
258 add_filter( 'manage_users_custom_column', 'openstation_agent_users_custom_column', 10, 3 );
259