PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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
← All changes | includes/recycle-bin/realtime.php +128 -149 0.9.31.1.10 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Recycle Bin: real-time signal layer.
3 + * OpenStation — Recycle Bin: real-time signal layer.
4 4 *
5 5 * Two non-polling paths feed the open Recycle Bin window:
6 6 *
7 7 * 1. **Fast path — chromeless iframe**
@@ -6,15 +6,18 @@
6 6 *
7 7 * 1. **Fast path — chromeless iframe**
8 8 * Every recycle-bin-relevant action (`wp_trash_post`,
9 9 * `untrash_post`, `before_delete_post`, plus our four
10 - * `desktop_mode_recycle_bin_*` actions) flips a per-request
10 + * `openstation_recycle_bin_*` actions) flips a per-request
11 11 * static flag. At `admin_footer`, if the request is chromeless
12 12 * AND the flag is set, we emit a 12-line inline script that
13 13 * `postMessage`s the parent shell with `type:
14 - * 'desktop-mode-recycle-bin-changed'`. The parent dispatches our
14 + * 'os-recycle-bin-changed'`. The parent dispatches our
15 15 * `CustomEvent`, the open window refreshes. Cost: ~zero unless
16 - * a delete actually happened in this request.
16 + * a delete actually happened in this request. The per-domain
17 + * `os.<type>.changed` list-refresh broadcasts ride the
18 + * generic content-changes emitter instead (the changelog below
19 + * delegates into `includes/content-changes.php`).
17 20 *
18 21 * 2. **Catch-all path — Heartbeat**
19 22 * Every delete also bumps a single autoload=false option
20 23 * `_desktop_mode_recycle_bin_change_ts` (a millisecond timestamp).
@@ -19,9 +22,9 @@
19 22 * Every delete also bumps a single autoload=false option
20 23 * `_desktop_mode_recycle_bin_change_ts` (a millisecond timestamp).
21 24 * The Heartbeat `heartbeat_received` filter checks the client's
22 25 * last-seen ts — if the option is newer, the response includes
23 - * `desktop_mode_recycle_bin: { changed, ts }`. The bin only subscribes
26 + * `openstation_recycle_bin: { changed, ts }`. The bin only subscribes
24 27 * while its window is open, so users without the bin open pay
25 28 * zero. The cost per tick is one cached option read.
26 29 *
27 30 * Why two paths: chromeless iframes that produce a footer (form
@@ -29,15 +32,21 @@
29 32 * Trash" buttons) get instant updates. Everything else (AJAX list
30 33 * actions, REST `DELETE`, other browser tabs, WP-CLI, cron) drips
31 34 * in within the heartbeat cadence (15s active, 60s away).
32 35 *
33 - * @package WPDesktopMode
34 - * @since 0.6.0
36 + * @package OpenStation
35 37 */
36 38
37 39 defined( 'ABSPATH' ) || exit;
38 40
39 -const DESKTOP_MODE_RECYCLE_BIN_CHANGE_OPTION = '_desktop_mode_recycle_bin_change_ts';
41 +/**
42 + * The VALUE keeps its pre-rebrand spelling on purpose: it is a
43 + * persisted or externally-visible identifier, so renaming it would
44 + * orphan data already written by live installs (or break a live
45 + * URL). The mismatch between this constant's name and its value is
46 + * deliberate — it is NOT a half-finished rename.
47 + */
48 +const OPENSTATION_RECYCLE_BIN_CHANGE_OPTION = '_desktop_mode_recycle_bin_change_ts';
40 49
41 50 /**
42 51 * Per-request "did this request trigger a recycle-bin change" flag.
43 52 *
@@ -44,14 +53,12 @@
44 53 * Backed by a function-static so it survives across hook callbacks
45 54 * within the same PHP request. Reading without args returns the
46 55 * current value; passing `true` sets it.
47 56 *
48 - * @since 0.6.0
49 - *
50 57 * @param bool|null $set Set the flag.
51 58 * @return bool
52 59 */
53 -function desktop_mode_recycle_bin_request_dirty( $set = null ) {
60 +function openstation_recycle_bin_request_dirty( $set = null ) {
54 61 static $dirty = false;
55 62 if ( null !== $set ) {
56 63 $dirty = (bool) $set;
57 64 }
@@ -67,15 +74,13 @@
67 74 *
68 75 * Stored as autoload=false to keep the option out of the always-
69 76 * loaded options query — recycle-bin polling is a "you opened the
70 77 * window, you opted in" cost, not a per-pageload cost.
71 - *
72 - * @since 0.6.0
73 78 */
74 -function desktop_mode_recycle_bin_signal_change() {
79 +function openstation_recycle_bin_signal_change() {
75 80 $ts = (int) round( microtime( true ) * 1000 );
76 - update_option( DESKTOP_MODE_RECYCLE_BIN_CHANGE_OPTION, $ts, false );
77 - desktop_mode_recycle_bin_request_dirty( true );
81 + update_option( OPENSTATION_RECYCLE_BIN_CHANGE_OPTION, $ts, false );
82 + openstation_recycle_bin_request_dirty( true );
78 83
79 84 /**
80 85 * Fires after the recycle bin's "something changed" signal is
81 86 * bumped. Subscribers can use this to push their own real-time
@@ -81,13 +86,11 @@
81 86 * bumped. Subscribers can use this to push their own real-time
82 87 * signal (websocket, SSE, etc.) without re-hooking every delete
83 88 * action individually.
84 89 *
85 - * @since 0.6.0
86 - *
87 90 * @param int $ts Milliseconds-since-epoch timestamp of the change.
88 91 */
89 - do_action( 'desktop_mode_recycle_bin_signal', $ts );
92 + do_action( 'openstation_recycle_bin_signal', $ts );
90 93 }
91 94
92 95 /**
93 96 * Wrapper for `wp_trash_post`-style actions that pass `$post_id`.
@@ -92,34 +95,36 @@
92 95 /**
93 96 * Wrapper for `wp_trash_post`-style actions that pass `$post_id`.
94 97 *
95 98 * Captures the post id + post type into the per-request changelog
96 - * so the chromeless footer can emit one `desktop-mode-broadcast`
99 + * so the chromeless footer can emit one `os-broadcast`
97 100 * postMessage per affected domain (e.g. one for `post`, one for
98 101 * `attachment`, …). Subscribers — the recycle bin window, plus
99 102 * any plugin that registered a domain listener — react.
100 103 *
101 - * @since 0.6.0
102 - *
103 104 * @param int $post_id Post id being mutated.
104 105 * @param string $action One of 'trashed', 'untrashed', 'deleted'.
105 106 */
106 -function desktop_mode_recycle_bin_signal_change_for_post( $post_id, $action = 'trashed' ) {
107 +function openstation_recycle_bin_signal_change_for_post( $post_id, $action = 'trashed' ) {
107 108 $post = get_post( $post_id );
108 109 if ( $post instanceof WP_Post ) {
109 - desktop_mode_recycle_bin_record_change( (string) $post->post_type, (int) $post_id, (string) $action );
110 + openstation_recycle_bin_record_change( (string) $post->post_type, (int) $post_id, (string) $action );
110 111 }
111 - desktop_mode_recycle_bin_signal_change();
112 + openstation_recycle_bin_signal_change();
112 113 }
113 114
114 115 /**
115 116 * Per-request changelog: `[ post_type ][ action ] = int[] ids`.
116 117 *
117 - * Reads when called without args; mutates when called with a
118 - * post_type. Static-store pattern, same shape as the dirty
119 - * helper above so test introspection is symmetric.
118 + * Thin wrapper over the generic content-changes recorder
119 + * (`includes/content-changes.php`) — the generic module
120 + * owns the changelog AND the per-domain `os.<type>.changed`
121 + * footer broadcasts, so a trash and a save flow through one emitter.
122 + * The wrapper is kept because the recycle-bin hook wiring below and
123 + * third-party code grep for it.
120 124 *
121 - * @since 0.6.0
125 + * Reads when called without args; records when called with a
126 + * post_type.
122 127 *
123 128 * @param string $post_type Optional. Mutate this domain.
124 129 * @param int $post_id Optional. Id to record.
125 130 * @param string $action Optional. Verb (trashed/untrashed/deleted).
@@ -124,22 +129,13 @@
124 129 * @param int $post_id Optional. Id to record.
125 130 * @param string $action Optional. Verb (trashed/untrashed/deleted).
126 131 * @return array Full changelog when called with no args.
127 132 */
128 -function desktop_mode_recycle_bin_record_change( $post_type = '', $post_id = 0, $action = '' ) {
129 - static $log = array();
130 -
131 - if ( '' === $post_type ) {
132 - return $log;
133 +function openstation_recycle_bin_record_change( $post_type = '', $post_id = 0, $action = '' ) {
134 + if ( '' !== $post_type ) {
135 + openstation_content_changes_record( (string) $post_type, (int) $post_id, (string) $action );
133 136 }
134 - if ( ! isset( $log[ $post_type ] ) ) {
135 - $log[ $post_type ] = array();
136 - }
137 - if ( ! isset( $log[ $post_type ][ $action ] ) ) {
138 - $log[ $post_type ][ $action ] = array();
139 - }
140 - $log[ $post_type ][ $action ][] = (int) $post_id;
141 - return $log;
137 + return openstation_content_changes_log();
142 138 }
143 139
144 140 /**
145 141 * Whether the current chromeless request should emit the footer
@@ -155,17 +151,15 @@
155 151 * paint inside the iframe (typically <500ms after the click) and
156 152 * can refresh if its `seenTs` is older. The cost is one cached
157 153 * `get_option` + ~12 lines of inline JS per chromeless render.
158 154 *
159 - * @since 0.6.0
160 - *
161 155 * @return bool
162 156 */
163 -function desktop_mode_recycle_bin_should_emit_footer_signal() {
164 - if ( ! function_exists( 'desktop_mode_is_chromeless_request' ) ) {
157 +function openstation_recycle_bin_should_emit_footer_signal() {
158 + if ( ! function_exists( 'openstation_is_chromeless_request' ) ) {
165 159 return false;
166 160 }
167 - if ( ! desktop_mode_is_chromeless_request() ) {
161 + if ( ! openstation_is_chromeless_request() ) {
168 162 return false;
169 163 }
170 164
171 165 /**
@@ -171,18 +165,16 @@
171 165 /**
172 166 * Filter whether to emit the chromeless footer postMessage on
173 167 * the current request.
174 168 *
175 - * @since 0.6.0
176 - *
177 169 * @param bool $emit Default true on any chromeless render. The
178 - * `desktop_mode_recycle_bin_request_dirty()` helper
170 + * `openstation_recycle_bin_request_dirty()` helper
179 171 * reports whether THIS request itself mutated
180 172 * state — useful inside the filter for plugins
181 173 * that only want to ride the "this request
182 174 * trashed something" signal.
183 175 */
184 - return (bool) apply_filters( 'desktop_mode_recycle_bin_emit_footer_signal', true );
176 + return (bool) apply_filters( 'openstation_recycle_bin_emit_footer_signal', true );
185 177 }
186 178
187 179 /**
188 180 * Emits the chromeless-iframe → parent footer signal.
@@ -191,80 +183,44 @@
191 183 * footers so we don't race against unrelated emits. The inline
192 184 * script is ~14 lines uncompressed, doesn't import jQuery, and is
193 185 * a no-op when `window.parent === window` (defensive — the same
194 186 * gate the existing chromeless bridge uses).
195 - *
196 - * @since 0.6.0
197 187 */
198 -function desktop_mode_recycle_bin_emit_footer_signal() {
199 - if ( ! desktop_mode_recycle_bin_should_emit_footer_signal() ) {
188 +function openstation_recycle_bin_emit_footer_signal() {
189 + if ( ! openstation_recycle_bin_should_emit_footer_signal() ) {
200 190 return;
201 191 }
202 192
203 - $ts = (int) get_option( DESKTOP_MODE_RECYCLE_BIN_CHANGE_OPTION, 0 );
204 - $changelog = desktop_mode_recycle_bin_record_change();
193 + $ts = (int) get_option( OPENSTATION_RECYCLE_BIN_CHANGE_OPTION, 0 );
205 194
206 - if ( $ts <= 0 && empty( $changelog ) ) {
195 + if ( $ts <= 0 ) {
207 196 // Nothing has ever been trashed via this site — no point
208 197 // teaching the parent shell about a 0 high-water mark.
209 198 return;
210 199 }
211 200
212 - // Per-domain broadcast envelope: one entry per affected
213 - // post_type, with verb-keyed id lists. The parent shell's
214 - // `installBroadcastReceiver` translates each into a
215 - // `desktop-mode.<post_type>.changed` broadcast — the bin (and
216 - // any plugin that subscribes to that exact topic) reacts.
217 - $broadcasts = array();
218 - foreach ( $changelog as $post_type => $by_action ) {
219 - foreach ( $by_action as $action => $ids ) {
220 - $broadcasts[] = array(
221 - 'topic' => 'desktop-mode.' . $post_type . '.changed',
222 - 'payload' => array(
223 - 'source' => 'admin',
224 - 'action' => (string) $action,
225 - 'ids' => array_values( array_unique( array_map( 'intval', $ids ) ) ),
226 - ),
227 - );
228 - }
229 - }
230 - $broadcasts_json = wp_json_encode( $broadcasts );
201 + // Only the bin-specific ts signal is emitted here. The per-domain
202 + // `os.<post_type>.changed` broadcasts moved to the
203 + // generic content-changes emitter (`includes/content-changes.php`,
204 + // same `admin_footer` slot) — the bin's changelog delegates into
205 + // it, so a trash and a save flow through one emitter and each
206 + // type/action pair is broadcast exactly once per render.
231 207 ?>
232 - <script id="desktop-mode-recycle-bin-realtime-signal">
208 + <script id="os-recycle-bin-realtime-signal">
233 209 ( function () {
234 210 if ( window.parent === window ) {
235 211 return;
236 212 }
237 - var origin = window.location.origin;
238 213 try {
239 214 window.parent.postMessage(
240 215 {
241 - type: 'desktop-mode-recycle-bin-changed',
216 + type: 'os-recycle-bin-changed',
242 217 ts: <?php echo (int) $ts; ?>,
243 218 source: 'chromeless'
244 219 },
245 - origin
220 + window.location.origin
246 221 );
247 222 } catch ( _err ) { /* swallow */ }
248 -
249 - /*
250 - * Per-domain broadcast envelopes — one postMessage per
251 - * affected post type. The parent shell's broadcast
252 - * receiver fans these out as `desktop-mode.<type>.changed`
253 - * subscriptions. Only emitted when the request actually
254 - * mutated something: a no-op chromeless render skips this
255 - * branch entirely.
256 - */
257 - var broadcasts = <?php echo $broadcasts_json ? $broadcasts_json : '[]'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>;
258 - for ( var i = 0; i < broadcasts.length; i++ ) {
259 - try {
260 - window.parent.postMessage( {
261 - type: 'desktop-mode-broadcast',
262 - topic: broadcasts[ i ].topic,
263 - payload: broadcasts[ i ].payload
264 - }, origin );
265 - } catch ( _err ) { /* swallow */ }
266 - }
267 223 } )();
268 224 </script>
269 225 <?php
270 226 }
@@ -274,42 +230,49 @@
274 230 * heard from me?".
275 231 *
276 232 * The Heartbeat API runs server-side every 15s (active window),
277 233 * 60s (background tab), or 120s (idle). The bin's tab opts in by
278 - * sending `desktop_mode_recycle_bin_seen_ts` in its outgoing data; if the
234 + * sending `openstation_recycle_bin_seen_ts` in its outgoing data; if the
279 235 * key is absent we early-return so users without the bin open pay
280 236 * zero per tick.
281 237 *
282 - * @since 0.6.0
283 - *
284 238 * @param array $response Heartbeat response (passed by ref via filter).
285 239 * @param array $data Client-sent payload.
286 240 * @return array
287 241 */
288 -function desktop_mode_recycle_bin_heartbeat_received( $response, $data ) {
242 +function openstation_recycle_bin_heartbeat_received( $response, $data ) {
289 243 if ( ! is_array( $response ) ) {
290 244 $response = array();
291 245 }
292 - if ( ! isset( $data['desktop_mode_recycle_bin_seen_ts'] ) ) {
246 + if ( ! isset( $data['openstation_recycle_bin_seen_ts'] ) ) {
293 247 return $response;
294 248 }
295 - if ( function_exists( 'desktop_mode_recycle_bin_user_can_use' ) && ! desktop_mode_recycle_bin_user_can_use() ) {
249 + if ( function_exists( 'openstation_recycle_bin_user_can_use' ) && ! openstation_recycle_bin_user_can_use() ) {
296 250 return $response;
297 251 }
298 252
299 - $seen = (int) $data['desktop_mode_recycle_bin_seen_ts'];
300 - $latest = (int) get_option( DESKTOP_MODE_RECYCLE_BIN_CHANGE_OPTION, 0 );
253 + $seen = (int) $data['openstation_recycle_bin_seen_ts'];
254 + $latest = (int) get_option( OPENSTATION_RECYCLE_BIN_CHANGE_OPTION, 0 );
255 + $changed = $latest > $seen;
301 256
302 - // Authoritative count travels on every tick — it's the cheapest
303 - // way to keep the dock/icon badge truthful when the bin window
304 - // is closed. `desktop_mode_recycle_bin_count()` is a fast COUNT(*) that
305 - // hits the same option-cached query each post-status.
306 - $response['desktop_mode_recycle_bin'] = array(
307 - 'changed' => $latest > $seen,
257 + $response['openstation_recycle_bin'] = array(
258 + 'changed' => $changed,
308 259 'ts' => $latest,
309 - 'count' => desktop_mode_recycle_bin_count(),
310 260 );
311 261
262 + // The authoritative count only travels when something actually
263 + // changed since the client's high-water mark. The count cannot
264 + // drift without the change-ts bumping (every capture / restore /
265 + // purge bumps it), so an unchanged tick would recompute the same
266 + // number — `openstation_recycle_bin_count()` runs up to two
267 + // COUNT(*) WP_Querys plus a comment count, a real per-tick cost
268 + // multiplied across every user with the shell open. The client
269 + // treats `count` as optional and keeps its current badge value
270 + // when the key is absent.
271 + if ( $changed ) {
272 + $response['openstation_recycle_bin']['count'] = openstation_recycle_bin_count();
273 + }
274 +
312 275 return $response;
313 276 }
314 277
315 278 /**
@@ -314,55 +277,71 @@
314 277
315 278 /**
316 279 * Wire the deletion hooks. We listen for both the WordPress core
317 280 * verbs (`wp_trash_post`, `untrash_post`, `before_delete_post`) and
318 - * our own `desktop_mode_recycle_bin_*` lifecycle actions — the former
281 + * our own `openstation_recycle_bin_*` lifecycle actions — the former
319 282 * catches deletes that bypass our REST endpoints (Quick Edit, REST
320 283 * `DELETE`, WP-CLI, list-table bulk actions); the latter catches
321 284 * the bin's own restore/purge so other tabs see the change.
322 285 *
323 286 * Hooked together inside one bootstrap to make the wiring auditable
324 - * — `grep desktop_mode_recycle_bin_signal_change` finds every emitter.
325 - *
326 - * @since 0.6.0
287 + * — `grep openstation_recycle_bin_signal_change` finds every emitter.
327 288 */
328 -function desktop_mode_recycle_bin_register_realtime_hooks() {
329 - add_action( 'wp_trash_post', function ( $post_id ) {
330 - desktop_mode_recycle_bin_signal_change_for_post( $post_id, 'trashed' );
331 - } );
332 - add_action( 'untrash_post', function ( $post_id ) {
333 - desktop_mode_recycle_bin_signal_change_for_post( $post_id, 'untrashed' );
334 - } );
335 - add_action( 'before_delete_post', function ( $post_id ) {
336 - desktop_mode_recycle_bin_signal_change_for_post( $post_id, 'deleted' );
337 - } );
289 +function openstation_recycle_bin_register_realtime_hooks() {
290 + add_action(
291 + 'wp_trash_post',
292 + function ( $post_id ) {
293 + openstation_recycle_bin_signal_change_for_post( $post_id, 'trashed' );
294 + }
295 + );
296 + add_action(
297 + 'untrash_post',
298 + function ( $post_id ) {
299 + openstation_recycle_bin_signal_change_for_post( $post_id, 'untrashed' );
300 + }
301 + );
302 + add_action(
303 + 'before_delete_post',
304 + function ( $post_id ) {
305 + openstation_recycle_bin_signal_change_for_post( $post_id, 'deleted' );
306 + }
307 + );
338 308
339 309 // Comments use a different verb space — `trashed_comment` /
340 310 // `untrashed_comment` / `deleted_comment` fire from
341 311 // `wp_set_comment_status`. Map each into our changelog so the
342 - // chromeless footer can broadcast `desktop-mode.comment.changed`
312 + // chromeless footer can broadcast `os.comment.changed`
343 313 // to the Comments-list iframe; the bin captures and lists trashed
344 314 // comments too, and third-party plugins can subscribe to the same
345 315 // topic by hooking the changelog.
346 - add_action( 'trashed_comment', function ( $comment_id ) {
347 - desktop_mode_recycle_bin_record_change( 'comment', (int) $comment_id, 'trashed' );
348 - desktop_mode_recycle_bin_signal_change();
349 - } );
350 - add_action( 'untrashed_comment', function ( $comment_id ) {
351 - desktop_mode_recycle_bin_record_change( 'comment', (int) $comment_id, 'untrashed' );
352 - desktop_mode_recycle_bin_signal_change();
353 - } );
354 - add_action( 'deleted_comment', function ( $comment_id ) {
355 - desktop_mode_recycle_bin_record_change( 'comment', (int) $comment_id, 'deleted' );
356 - desktop_mode_recycle_bin_signal_change();
357 - } );
316 + add_action(
317 + 'trashed_comment',
318 + function ( $comment_id ) {
319 + openstation_recycle_bin_record_change( 'comment', (int) $comment_id, 'trashed' );
320 + openstation_recycle_bin_signal_change();
321 + }
322 + );
323 + add_action(
324 + 'untrashed_comment',
325 + function ( $comment_id ) {
326 + openstation_recycle_bin_record_change( 'comment', (int) $comment_id, 'untrashed' );
327 + openstation_recycle_bin_signal_change();
328 + }
329 + );
330 + add_action(
331 + 'deleted_comment',
332 + function ( $comment_id ) {
333 + openstation_recycle_bin_record_change( 'comment', (int) $comment_id, 'deleted' );
334 + openstation_recycle_bin_signal_change();
335 + }
336 + );
358 337
359 - add_action( 'desktop_mode_recycle_bin_item_captured', 'desktop_mode_recycle_bin_signal_change' );
360 - add_action( 'desktop_mode_recycle_bin_after_restore', 'desktop_mode_recycle_bin_signal_change' );
361 - add_action( 'desktop_mode_recycle_bin_after_purge', 'desktop_mode_recycle_bin_signal_change' );
362 - add_action( 'desktop_mode_recycle_bin_emptied', 'desktop_mode_recycle_bin_signal_change' );
338 + add_action( 'openstation_recycle_bin_item_captured', 'openstation_recycle_bin_signal_change' );
339 + add_action( 'openstation_recycle_bin_after_restore', 'openstation_recycle_bin_signal_change' );
340 + add_action( 'openstation_recycle_bin_after_purge', 'openstation_recycle_bin_signal_change' );
341 + add_action( 'openstation_recycle_bin_emptied', 'openstation_recycle_bin_signal_change' );
363 342
364 - add_action( 'admin_footer', 'desktop_mode_recycle_bin_emit_footer_signal', 100 );
343 + add_action( 'admin_footer', 'openstation_recycle_bin_emit_footer_signal', 100 );
365 344
366 - add_filter( 'heartbeat_received', 'desktop_mode_recycle_bin_heartbeat_received', 10, 2 );
345 + add_filter( 'heartbeat_received', 'openstation_recycle_bin_heartbeat_received', 10, 2 );
367 346 }
368 -add_action( 'init', 'desktop_mode_recycle_bin_register_realtime_hooks', 5 );
347 +add_action( 'init', 'openstation_recycle_bin_register_realtime_hooks', 5 );