PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.7
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 / desktop-files / cascade-cleanup.php

cascade-cleanup.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.7, at includes/desktop-files/cascade-cleanup.php

246 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Cascade cleanup of placements on entity trash.
4 *
5 * When a WordPress entity that a desktop shortcut points at is
6 * trashed or deleted, the matching placement rows are soft-trashed
7 * so the wallpaper stops painting tiles that resolve to nothing.
8 *
9 * Hooks cover every route an entity can leave the live set by:
10 *
11 * - `wp_trash_post` — post / page / attachment moved to
12 * WP trash (our drag-to-trash, our CMO,
13 * WP admin, WP-CLI all funnel through
14 * this).
15 * - `before_delete_post` — force-delete (bypasses the trash
16 * stage when `EMPTY_TRASH_DAYS === 0`
17 * or the caller passed `force_delete`).
18 * - `delete_attachment` — hard-delete path for attachments that
19 * skips `wp_trash_post`.
20 * - `deleted_user` — user account removed (with or without
21 * content reassignment).
22 *
23 * Implementation is a bulk UPDATE that bypasses the per-placement
24 * permission check used by user-initiated trash. The trashing
25 * action has already been authorized at the entity level (Core
26 * gates the `wp_trash_post` cap, `delete_users`, etc.); cascading
27 * to the desktop shortcuts is a downstream side-effect of that
28 * authorized action, not a fresh user gesture.
29 *
30 * Soft-trash semantics: `trashed_at_ms` is set but the row stays
31 * in the table. The placement surfaces in the Recycle Bin and the
32 * user can restore it from there (independent of the source entity's
33 * trash status — restoring a post does NOT auto-restore its shortcut).
34 *
35 * @package WPDesktopMode
36 * @since 0.8.9
37 */
38
39 defined( 'ABSPATH' ) || exit;
40
41 /**
42 * Soft-trash every live placement pointing at the given entity.
43 *
44 * Iterates `wp_desktop_mode_file_placements` for rows with the
45 * matching `file_type` + `file_ref` whose `trashed_at_ms` is
46 * unset, and stamps the trash columns. Each affected placement
47 * fires `desktop_mode_files_after_cascade_trash_placement` so
48 * plugins (and the live UI refresh path) can react.
49 *
50 * Idempotent: re-running with an already-trashed entity is a
51 * no-op because the `trashed_at_ms IS NULL` filter excludes
52 * placements that were trashed by a previous pass.
53 *
54 * Bulk-friendly: the function tolerates an entity with many
55 * placements across many users — the affected placement count
56 * is unbounded by the API but bounded in practice by the number
57 * of users who shortcutted the same entity.
58 *
59 * @since 0.8.9
60 *
61 * @param string $file_type File-type slug — `'post'`,
62 * `'attachment'`, `'user'`, plugin-
63 * defined. Must match
64 * {@see Desktop_Mode_File::type()}.
65 * @param string|int $file_ref Entity ref (post id, user id, …).
66 * @return int Number of placements soft-trashed.
67 */
68 function desktop_mode_files_cascade_trash_placements_for_entity( $file_type, $file_ref ) {
69 global $wpdb;
70 $file_type = (string) $file_type;
71 $file_ref = (string) $file_ref;
72 if ( '' === $file_type || '' === $file_ref ) {
73 return 0;
74 }
75 $tables = desktop_mode_files_table_names();
76
77 // SELECT the affected rows up front so we can fire a per-row
78 // action without a second roundtrip. Restrict to live (not yet
79 // trashed) placements — the cascade is idempotent and we don't
80 // want to re-stamp `trashed_at_ms` on rows the user trashed
81 // individually before the source entity was trashed.
82 $rows = $wpdb->get_results(
83 $wpdb->prepare(
84 "SELECT id, owner_id, parent_id
85 FROM {$tables['placements']}
86 WHERE file_type = %s
87 AND file_ref = %s
88 AND trashed_at_ms IS NULL",
89 $file_type,
90 $file_ref
91 ),
92 ARRAY_A
93 );
94 if ( empty( $rows ) ) {
95 return 0;
96 }
97
98 $now = desktop_mode_files_now_ms();
99 $trashed = 0;
100 foreach ( $rows as $row ) {
101 $placement_id = (int) $row['id'];
102 $owner_id = (int) $row['owner_id'];
103 $ancestry = desktop_mode_files_capture_ancestry( (int) $row['parent_id'] );
104 // `trashed_meta` carries the ancestry (so a restore knows
105 // where to put the tile back) plus a cascade marker so the
106 // recycle-bin UI can distinguish entity-cascade rows from
107 // user-initiated trashes if a plugin ever wants to render
108 // the source ("Trashed because the post was trashed.").
109 $meta = wp_json_encode(
110 array(
111 'ancestry' => $ancestry,
112 'cascade' => array(
113 'reason' => $file_type . '_trashed',
114 'file_type' => $file_type,
115 'file_ref' => $file_ref,
116 ),
117 )
118 );
119 $ok = $wpdb->update(
120 $tables['placements'],
121 array(
122 'trashed_at_ms' => $now,
123 'trashed_by' => $owner_id,
124 'trashed_meta' => $meta,
125 'updated_at_ms' => $now,
126 ),
127 array( 'id' => $placement_id ),
128 array( '%d', '%d', '%s', '%d' ),
129 array( '%d' )
130 );
131 if ( false === $ok ) {
132 continue;
133 }
134 ++$trashed;
135
136 /**
137 * Fires after a placement has been cascade-trashed because
138 * its source entity was trashed.
139 *
140 * @since 0.8.9
141 *
142 * @param int $placement_id Placement id.
143 * @param int $owner_id Placement owner.
144 * @param string $file_type Source entity file type.
145 * @param string|int $file_ref Source entity ref.
146 */
147 do_action(
148 'desktop_mode_files_after_cascade_trash_placement',
149 $placement_id,
150 $owner_id,
151 $file_type,
152 $file_ref
153 );
154 }
155
156 return $trashed;
157 }
158
159 /**
160 * Wrap a `(post_id)` Core action so it cascades against
161 * `file_type='post'`. Both `wp_trash_post` and `before_delete_post`
162 * pass the post id as the first arg.
163 *
164 * Attachments share the post-id space and CAN go through these
165 * hooks too — but attachment placements use `file_type='attachment'`
166 * (not `'post'`), so this wrapper would miss them. The separate
167 * `delete_attachment` hook below covers attachments.
168 *
169 * @since 0.8.9
170 *
171 * @param int $post_id Post id.
172 */
173 function desktop_mode_files_cascade_on_post_trash( $post_id ) {
174 $post_id = (int) $post_id;
175 if ( $post_id <= 0 ) {
176 return;
177 }
178 $post = get_post( $post_id );
179 if ( ! $post instanceof WP_Post ) {
180 return;
181 }
182 // Attachments route to the `attachment` file-type, NOT `post`,
183 // so the post-keyed cascade would no-op on them. The dedicated
184 // `delete_attachment` hook below catches the hard-delete path;
185 // the soft-trash path for attachments (when `EMPTY_TRASH_DAYS`
186 // is non-zero) also fires `wp_trash_post` — we cover it by
187 // dispatching to the attachment cascade below.
188 if ( 'attachment' === $post->post_type ) {
189 desktop_mode_files_cascade_trash_placements_for_entity(
190 'attachment',
191 (string) $post_id
192 );
193 return;
194 }
195 desktop_mode_files_cascade_trash_placements_for_entity( 'post', (string) $post_id );
196 }
197
198 add_action( 'wp_trash_post', 'desktop_mode_files_cascade_on_post_trash', 10, 1 );
199 add_action( 'before_delete_post', 'desktop_mode_files_cascade_on_post_trash', 10, 1 );
200
201 /**
202 * Force-delete path for attachments — Core's `wp_delete_attachment()`
203 * fires this BEFORE the attachment row is gone, so `get_post()` is
204 * still resolvable for any plugin that needs the metadata.
205 *
206 * @since 0.8.9
207 *
208 * @param int $attachment_id Attachment id.
209 */
210 function desktop_mode_files_cascade_on_attachment_delete( $attachment_id ) {
211 $attachment_id = (int) $attachment_id;
212 if ( $attachment_id <= 0 ) {
213 return;
214 }
215 desktop_mode_files_cascade_trash_placements_for_entity(
216 'attachment',
217 (string) $attachment_id
218 );
219 }
220
221 add_action( 'delete_attachment', 'desktop_mode_files_cascade_on_attachment_delete', 10, 1 );
222
223 /**
224 * User account removed — cascade-trash every shortcut whose
225 * `file_type='user', file_ref='<id>'` pointed at the gone user.
226 * Fires AFTER deletion (`deleted_user`) because user-deletion in
227 * Core involves reassigning content; we don't need the live user
228 * row, only its id.
229 *
230 * @since 0.8.9
231 *
232 * @param int $user_id Deleted user id.
233 */
234 function desktop_mode_files_cascade_on_user_delete( $user_id ) {
235 $user_id = (int) $user_id;
236 if ( $user_id <= 0 ) {
237 return;
238 }
239 desktop_mode_files_cascade_trash_placements_for_entity(
240 'user',
241 (string) $user_id
242 );
243 }
244
245 add_action( 'deleted_user', 'desktop_mode_files_cascade_on_user_delete', 10, 1 );
246