PluginProbe
SQLite Object Cache / 1.6.3
SQLite Object Cache v1.6.3
1.6.5 trunk 0.1.7 1.0.0 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.4.0 1.4.1 1.5.1 1.5.4 1.5.5 1.5.6 1.5.7 All 30 releases
sqlite-object-cache / includes / class-sqlite-object-cache.php

class-sqlite-object-cache.php in SQLite Object Cache 1.6.3, at includes/class-sqlite-object-cache.php

726 lines 23.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main plugin class file.
4 *
5 * @package WordPress Plugin Template/Includes
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Main plugin class.
14 *
15 * Only make one instance of this, please.
16 */
17 class SQLite_Object_Cache {
18 const CLEAN_EVENT_HOOK = 'sqlite_object_cache_clean';
19
20 /**
21 * Settings class object
22 *
23 * @var object
24 * @access public
25 * @since 1.0.0
26 */
27 public $settings;
28
29 /**
30 * The version number.
31 *
32 * @var string
33 * @access public
34 * @since 1.0.0
35 */
36 public $_version;
37
38 /**
39 * The token.
40 *
41 * @var string
42 * @access public
43 * @since 1.0.0
44 */
45 public $_token;
46
47 /**
48 * The main plugin file.
49 *
50 * @var string
51 * @access public
52 * @since 1.0.0
53 */
54 public $file;
55
56 /**
57 * The main plugin directory.
58 *
59 * @var string
60 * @access public
61 * @since 1.0.0
62 */
63 public $dir;
64
65 /**
66 * The plugin assets directory.
67 *
68 * @var string
69 * @access public
70 * @since 1.0.0
71 */
72 public $assets_dir;
73
74 /**
75 * The plugin assets URL.
76 *
77 * @var string
78 * @access public
79 * @since 1.0.0
80 */
81 public $assets_url;
82
83 /**
84 * Suffix for JavaScripts.
85 *
86 * @var string
87 * @access public
88 * @since 1.0.0
89 */
90 public $script_suffix;
91
92 /**
93 * Path for the drop-in when it is working.
94 * @var string
95 */
96 public $dropinfiledest;
97
98 /**
99 * Path for the drop-in in the plugin tree.
100 * @var string
101 */
102 public $dropinfilesource;
103
104 /**
105 * Minimum required sqlite version.
106 *
107 * @var string
108 */
109 public $minimum_sqlite_version = '3.7.0';
110
111 /**
112 * Constructor function.
113 *
114 * @param string $file File constructor.
115 * @param string $version Plugin version.
116 */
117 public function __construct( $file = '', $version = '1.6.3' ) {
118 $this->_version = $version;
119 $this->_token = 'sqlite_object_cache';
120
121 // Load plugin environment variables.
122 $this->file = $file;
123 $this->dir =
124 trailingslashit( dirname( trailingslashit( WP_PLUGIN_DIR ) . plugin_basename( $this->file ) ) );
125 $this->assets_dir = trailingslashit( $this->dir . 'assets' );
126 $this->dropinfilesource = $this->assets_dir . 'drop-in/object-cache.php';
127 $this->dropinfiledest = trailingslashit( WP_CONTENT_DIR ) . 'object-cache.php';
128
129 $this->assets_url = esc_url( trailingslashit( plugins_url( '/assets/', $this->file ) ) );
130
131 register_activation_hook( $this->file, array( $this, 'on_activation' ) );
132 register_deactivation_hook( $this->file, array( $this, 'on_deactivation' ) );
133
134 if ( is_admin() ) {
135 /* Suppress backups of .sqlite files if possible. */
136 new SQLite_Backup_Exclusion();
137
138 /* Health check */
139 new SQLite_Object_Cache_Opcache();
140
141 add_action( 'admin_notices', function () {
142 $flushed = get_site_transient( 'sqlite-object-cache-flush-on-update' );
143 if ( $flushed ) {
144 delete_site_transient( 'sqlite-object-cache-flush-on-update' );
145 $message = __( 'Flushed the SQLite Object Cache successfully after a software installation or upgrade.', 'sqlite-object-cache' );
146 ?>
147 <div class="notice notice-success sqlite-object-cache is-dismissible">
148 <p><?php echo esc_html( $message ); ?></p>
149 </div>
150 <?php
151 }
152 } );
153 }
154
155 add_action( 'admin_init', array( $this, 'maybe_update_dropin' ) );
156
157 /* Ensure nothing is cached after software upgrades. */
158 add_action( 'upgrader_process_complete', function () {
159 add_action( 'shutdown', function () {
160 wp_cache_flush();
161 set_site_transient( 'sqlite-object-cache-flush-on-update', true, HOUR_IN_SECONDS );
162 }, 999 );
163 wp_cache_flush();
164 }, 0 );
165
166
167 /* Handle cron cache cleanup */
168 add_action( self::CLEAN_EVENT_HOOK, array( $this, 'clean_job' ), 10, 0 );
169 if ( ! wp_next_scheduled( self::CLEAN_EVENT_HOOK ) ) {
170 wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::CLEAN_EVENT_HOOK );
171 }
172 /* Handle probabilistic non-cron cleanup, one request in 2500. */
173 /* We don't need cryptographic-grade random numbering here. */
174 // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_rand
175 if ( 1 === rand( 1, 2500 ) ) {
176 add_action( 'shutdown', function () {
177 $this->clean_job( 1.25 );
178 }, 999, 0 );
179 }
180
181 /* Admin bar flush button - works on both frontend and backend. */
182 $option = get_option( $this->_token . '_settings', array() );
183 if ( array_key_exists ('adminbarflush', $option ) && 'on' === $option['adminbarflush'] ) {
184 add_action( 'init', array( $this, 'handle_admin_bar_flush' ) );
185 add_action ( 'init', function() {
186 if ( ! ( is_multisite() && ! is_main_site() ) && current_user_can( 'manage_options') ) {
187 add_action( 'admin_bar_menu', array( $this, 'admin_bar_flush_button' ), 100 );
188 add_action( 'admin_notices', array( $this, 'maybe_show_flush_notice' ) );
189 }
190 });
191 }
192 }
193
194 /**
195 * WP_Cron task to clean cache entries.
196 *
197 * @param float $grace_factor Default 1.0
198 *
199 * @return void
200 */
201 public function clean_job( $grace_factor = 1.0 ) {
202 $option = get_option( $this->_token . '_settings', array() );
203 $target_size = empty ( $option['target_size'] ) ? 16 : $option['target_size'];
204 $target_size *= ( 1024 * 1024 );
205 $threshold_size = (int) ( $target_size * $grace_factor );
206
207 global $wp_object_cache;
208 if ( ! method_exists( $wp_object_cache, 'sqlite_get_size' ) ) {
209 return;
210 }
211 /* Clean up old statistics. Do this even when the cache is not over size. */
212 $retention = empty ( $option['retainmeasurements'] ) ? 24 : $option['retainmeasurements'];
213 $wp_object_cache->sqlite_reset_statistics( $retention * HOUR_IN_SECONDS );
214
215 $current_size = $wp_object_cache->sqlite_get_size();
216 /* Skip this if the current size is small enough. */
217 if ( $current_size <= $threshold_size ) {
218 return;
219 }
220
221 /* Remove expired items (transients mostly). */
222 if ( $wp_object_cache->sqlite_remove_expired() ) {
223 /* If anything was removed, get the size again. */
224 $current_size = $wp_object_cache->sqlite_get_size();
225 }
226
227 /* Skip this if the size is small enough, after removing expired items. */
228 if ( $current_size <= $threshold_size ) {
229 return;
230 }
231
232 /* Delete the least-recently-updated items to get to the target size. */
233 $wp_object_cache->sqlite_delete_old( $target_size, $current_size );
234 }
235
236 /**
237 * Plugin activation hook
238 *
239 * @return void
240 */
241 public function on_activation() {
242
243 $this->sync_apcu_global_to_option( true );
244 if ( true === $this->has_sqlite() ) {
245 add_action( 'shutdown', array( $this, 'update_dropin' ) );
246 add_action( 'shutdown', array( $this, 'delete_all_transients_from_db' ) );
247 }
248 }
249
250 /**
251 * Determine whether we can use SQLite3.
252 *
253 * @return bool|string true, or an error message.
254 */
255 public function has_sqlite() {
256 if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
257 return __( 'You cannot use the SQLite Object Cache plugin. Your server does not have php\'s SQLite3 extension installed.', 'sqlite-object-cache' );
258 }
259
260 $sqlite_version = $this->sqlite_get_version();
261 if ( version_compare( $sqlite_version, $this->minimum_sqlite_version ) < 0 ) {
262 return sprintf(
263 /* translators: 1 actual SQLite version. 2 required SQLite version) */
264 __( 'You cannot use the SQLite Object Cache plugin. Your server only offers SQLite3 version %1$s, but at least %2$s is required.', 'sqlite-object-cache' ),
265 $sqlite_version, $this->minimum_sqlite_version );
266 }
267
268 return true;
269 }
270
271 /**
272 * Get the version number for the SQLite extension.
273 *
274 * @return string|false SQLite's version number.
275 */
276 public function sqlite_get_version() {
277
278 if ( class_exists( 'SQLite3' ) ) {
279 $version = SQLite3::version();
280
281 return $version['versionString'];
282 }
283
284 return false;
285 }
286
287 /**
288 * Test if we can write in the WP_CONTENT_DIR and modify the `object-cache.php` drop-in
289 *
290 * @return true|WP_Error
291 * @author Till Krüss
292 *
293 */
294 public function test_filesystem_writing() {
295 global $wp_filesystem;
296
297 if ( ! $this->initialize_filesystem( '', true ) ) {
298 return new WP_Error( 'fs', __( 'Could not initialize filesystem.', 'sqlite-object-cache' ) );
299 }
300
301 $testfiledest = WP_CONTENT_DIR . '/sqlite-write-test.tmp';
302
303 if ( ! $wp_filesystem->exists( $this->dropinfilesource ) ) {
304 return new WP_Error( 'exists', __( 'Object cache drop-in file doesn’t exist.', 'sqlite-object-cache' ) );
305 }
306
307 if ( $wp_filesystem->exists( $testfiledest ) && ! $wp_filesystem->delete( $testfiledest ) ) {
308 return new WP_Error( 'delete', __( 'Test file exists, but couldn’t be deleted.', 'sqlite-object-cache' ) );
309 }
310
311 if ( ! $wp_filesystem->is_writable( WP_CONTENT_DIR ) ) {
312 return new WP_Error( 'copy', __( 'Content directory is not writable.', 'sqlite-object-cache' ) );
313 }
314
315 if ( ! $wp_filesystem->copy( $this->dropinfilesource, $testfiledest, true, FS_CHMOD_FILE ) ) {
316 return new WP_Error( 'copy', __( 'Failed to copy test file.', 'sqlite-object-cache' ) );
317 }
318
319 if ( ! $wp_filesystem->exists( $testfiledest ) ) {
320 return new WP_Error( 'exists', __( 'Copied test file doesn’t exist.', 'sqlite-object-cache' ) );
321 }
322
323 $meta = get_file_data( $testfiledest, array( 'Version' => 'Version' ) );
324
325 if ( $meta['Version'] !== $this->_version ) {
326 return new WP_Error( 'version', __( 'Couldn’t verify test file contents.', 'sqlite-object-cache' ) );
327 }
328
329 if ( ! $wp_filesystem->delete( $testfiledest ) ) {
330 return new WP_Error( 'delete', __( 'Copied test file couldn’t be deleted.', 'sqlite-object-cache' ) );
331 }
332
333 return true;
334 }
335
336 /**
337 * Initializes the WP filesystem API to be ready for use
338 *
339 * @param string $url The URL to post the form to.
340 * @param bool $silent Whether to ask the user for credentials if necessary or not.
341 *
342 * @return bool
343 * @author Till Krüss
344 *
345 */
346 public function initialize_filesystem( $url, $silent = false ) {
347 require_once ABSPATH . 'wp-admin/includes/file.php';
348 if ( $silent ) {
349 ob_start();
350 }
351
352 $credentials = request_filesystem_credentials( $url );
353
354 if ( false === $credentials ) {
355 if ( $silent ) {
356 ob_end_clean();
357 }
358
359 return false;
360 }
361
362 if ( ! WP_Filesystem( $credentials ) ) {
363 request_filesystem_credentials( $url );
364
365 if ( $silent ) {
366 ob_end_clean();
367 }
368
369 return false;
370 }
371
372 return true;
373 }
374
375 /**
376 * Calls the drop-in update method if necessary
377 *
378 * @return void
379 * @author Till Krüss
380 */
381 public function maybe_update_dropin() {
382 if ( defined( 'WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE' ) && WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE ) {
383 return;
384 }
385
386 $has = $this->has_sqlite();
387 if ( ( true === $has ) && $this->object_cache_dropin_needs_updating() ) {
388 add_action( 'shutdown', array( $this, 'update_dropin' ) );
389 }
390 }
391
392 /**
393 * Checks if the `object-cache.php` drop-in is outdated
394 * @return bool
395 * @author Till Krüss
396 *
397 */
398 public function object_cache_dropin_needs_updating() {
399 global $wp_object_cache;
400 if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
401 return true;
402 }
403 return version_compare( $wp_object_cache->dropin_get_version(), $this->_version, '<' );
404 }
405
406 /**
407 * Updates the `object-cache.php` drop-in
408 *
409 * @return void
410 * @author Till Krüss
411 */
412 public function update_dropin() {
413 global $wp_filesystem;
414 $has = $this->has_sqlite();
415 if ( true === $has && $this->initialize_filesystem( '', true ) ) {
416
417 $this->delete_sqlite_files();
418 $result = $wp_filesystem->copy( $this->dropinfilesource, $this->dropinfiledest, true, FS_CHMOD_FILE );
419 /**
420 * Fires on cache enable event
421 *
422 * @param bool $result Whether the filesystem event (copy of the `object-cache.php` file) was successful.
423 *
424 * @since 0.1.0
425 */
426 do_action( 'sqlite_object_cache_update_dropin', $result );
427 }
428 }
429
430 /**
431 * Validates the `object-cache.php` drop-in
432 *
433 * @return bool
434 * @author Till Krüss
435 */
436 public function validate_object_cache_dropin() {
437 global $wp_object_cache;
438
439 if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
440 return false;
441 }
442
443 $dropin = get_plugin_data( $this->dropinfiledest );
444 $plugin = get_plugin_data( $this->dropinfilesource );
445
446 /**
447 * Filters the drop-in validation state
448 *
449 * @param bool $state The validation state of the drop-in.
450 * @param string $dropin The `PluginURI` of the drop-in.
451 * @param string $plugin The `PluginURI` of the plugin.
452 *
453 * @since 2.0.16
454 */
455 return apply_filters(
456 'sqlite_object_cache_validate_dropin',
457 $dropin['PluginURI'] === $plugin['PluginURI'],
458 $dropin['PluginURI'],
459 $plugin['PluginURI']
460 );
461 }
462
463 /**
464 * Plugin deactivation hook
465 *
466 * @param string $plugin Plugin basename.
467 *
468 * @return void
469 * @author Till Krüss
470 */
471 public function on_deactivation( $plugin ) {
472
473 wp_cache_flush();
474
475 wp_unschedule_hook( self::CLEAN_EVENT_HOOK );
476 $this->delete_sqlite_files();
477 $this->delete_dropin();
478 $this->delete_all_transients_from_db();
479 }
480
481 private function delete_sqlite_files() {
482 global $wp_filesystem;
483 global $wp_object_cache;
484
485 if ( method_exists( $wp_object_cache, 'sqlite_files' ) ) {
486 if ( $this->initialize_filesystem( '', true ) ) {
487 foreach ( $wp_object_cache->sqlite_files() as $file ) {
488 $wp_filesystem->delete( $file );
489 }
490 }
491 }
492 }
493
494 private function delete_dropin() {
495 global $wp_filesystem;
496 ob_start();
497
498 if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
499 $wp_filesystem->delete( $this->dropinfiledest );
500 }
501
502 ob_end_clean();
503 }
504
505 /**
506 * Deletes all transients from the database.
507 *
508 * It is necessary to do this when deactivating a persistent object cache
509 * because the transients are kept there, and the ones in the database are
510 * possibly stale.
511 *
512 * This hammers performance. But deactivating a persistent object cache is
513 * not a common operation.
514 *
515 * @global wpdb $wpdb WordPress database abstraction object.
516 */
517 public function delete_all_transients_from_db() {
518 global $wpdb;
519 if ( ! is_multisite() ) {
520 $this->clear_blog_transients();
521 // Single site stores site transients in the options table.
522 $this->clear_blog_transients( '_site_transient_' );
523 } else {
524 foreach ( get_sites( array( 'number' => 0, 'fields' => 'ids', 'no_found_rows' => true, 'orderby' => false ) ) as $site_id ) {
525 try {
526 switch_to_blog( $site_id );
527 $this->clear_blog_transients();
528 } catch ( Exception $ex ) {
529 /* Empty, intentionally. Don't crash clearing transients. */
530 } finally {
531 restore_current_blog();
532 }
533 }
534 // Multisite stores site transients in the sitemeta table.
535 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
536 $wpdb->query(
537 $wpdb->prepare(
538 "DELETE FROM {$wpdb->sitemeta} WHERE a.meta_key LIKE %s",
539 $wpdb->esc_like( '_site_transient_' ) . '%'
540 )
541 );
542
543 }
544 }
545
546 /**
547 * DELETE transients from the current blog's options table.
548 *
549 * @param string $tag _site_transient_ or _transient_, the default.
550 *
551 * @return void
552 */
553 private function clear_blog_transients( $tag = '_transient_') {
554 global $wpdb;
555 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
556 $wpdb->query(
557 $wpdb->prepare(
558 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
559 $wpdb->esc_like( $tag ) . '%'
560 )
561 );
562 }
563
564 /**
565 * @return bool True if APCu support is activated for this plugin.
566 */
567 public function apcu_is_activated() {
568 return defined( 'WP_SQLITE_OBJECT_CACHE_APCU' ) && WP_SQLITE_OBJECT_CACHE_APCU
569 && $this->apcu_extension_is_enabled();
570 }
571
572 /**
573 * @return bool True if the APCu extension is loaded and enabled.
574 */
575 public function apcu_extension_is_enabled() {
576 return function_exists( 'apcu_enabled' ) && apcu_enabled();
577 }
578
579 /**
580 * Make sure WP_SQLITE_OBJECT_CACHE_APCU and $option['use_apcu'] match.
581 *
582 * @param bool $unconditional true if we should always change the option to match WP_SQLITE_OBJECT_CACHE_APCU.
583 *
584 * @return string 'on' or 'off': the current activation state of epcu.
585 */
586 public function sync_apcu_global_to_option( $unconditional = true ) {
587 global $wp_object_cache;
588 $option = get_option( $this->_token . '_settings', array() );
589 $option_dirty = false;
590 $updated_flag = array_key_exists( 'use_apcu_updated', $option );
591 $use_apcu = array_key_exists( 'use_apcu', $option ) && 'on' === $option['use_apcu'] ? 'on' : 'off';
592 if ( $updated_flag ) {
593 unset ( $option['use_apcu_updated'] );
594 $option_dirty = true;
595 }
596 $target_use_apcu = $this->apcu_is_activated() ? 'on' : 'off';
597 if ( ! $unconditional && $updated_flag ) {
598 $target_use_apcu = $use_apcu;
599 }
600 if ( $target_use_apcu !== $use_apcu ) {
601 $option['use_apcu'] = $target_use_apcu;
602 $option_dirty = true;
603 }
604 if ( $option_dirty ) {
605 if ( method_exists( $wp_object_cache, 'apcu-clear_cache' ) ) {
606 $wp_object_cache->apcu_clear_cache();
607 }
608 update_option( $this->_token . '_settings', $option, true );
609 }
610
611 return $target_use_apcu;
612 }
613
614 /**
615 * Add a flush button to the admin bar.
616 *
617 * @param WP_Admin_Bar $wp_admin_bar The admin bar instance.
618 *
619 * @return void
620 */
621 public function admin_bar_flush_button( $wp_admin_bar ) {
622 if ( ! is_admin_bar_showing() ) {
623 return;
624 }
625
626 if ( ! current_user_can( 'manage_options' ) ) {
627 return;
628 }
629
630 // Only show on main site for multisite installations.
631 if ( is_multisite() && ! is_main_site() ) {
632 return;
633 }
634
635 $flush_url = add_query_arg(
636 array( 'sqlite_object_cache_action' => 'flush' ),
637 remove_query_arg( 'sqlite_object_cache_action' )
638 );
639 $nonced_url = wp_nonce_url( $flush_url, 'sqlite_object_cache_flush' );
640
641 $wp_admin_bar->add_node(
642 array(
643 'id' => 'sqlite-object-cache-flush',
644 'title' => __( 'Flush Object Cache', 'sqlite-object-cache' ),
645 'href' => $nonced_url,
646 'meta' => array(
647 'title' => __( 'Delete all object cache entries now, including all transients.', 'sqlite-object-cache' ),
648 ),
649 )
650 );
651 }
652
653 /**
654 * Handle the admin bar flush action.
655 *
656 * @return void
657 */
658 public function handle_admin_bar_flush() {
659 if ( ! isset( $_GET['sqlite_object_cache_action'] ) ) {
660 return;
661 }
662
663 $action = sanitize_key( wp_unslash( $_GET['sqlite_object_cache_action'] ) );
664
665 if ( 'flush' !== $action ) {
666 return;
667 }
668
669 if ( ! current_user_can( 'manage_options' ) ) {
670 wp_die( esc_html__( 'You do not have permission to flush the object cache.', 'sqlite-object-cache' ) );
671 }
672
673 // Only allow flush on main site for multisite installations (matches button visibility).
674 if ( is_multisite() && ! is_main_site() ) {
675 wp_die( esc_html__( 'Cache flush is only available on the main site.', 'sqlite-object-cache' ) );
676 }
677
678 check_admin_referer( 'sqlite_object_cache_flush' );
679
680 wp_cache_flush();
681
682 // Use a user-specific transient so only the initiating user sees the flush notice (applies to both single-site and multisite).
683 $transient_key = 'sqlite_object_cache_flushed_' . get_current_user_id();
684 set_transient( $transient_key, true, 30 );
685
686 $referer = wp_get_referer();
687 if ( $referer ) {
688 $redirect_url = $referer;
689 } else {
690 $redirect_url = is_admin() ? admin_url() : home_url();
691 }
692 // Remove action and nonce query parameters from URL.
693 $redirect_url = remove_query_arg( array( 'sqlite_object_cache_action', '_wpnonce' ), $redirect_url );
694
695 wp_safe_redirect( $redirect_url );
696 exit;
697 }
698
699 /**
700 * Check for flush transient and show notice if set.
701 *
702 * @return void
703 */
704 public function maybe_show_flush_notice() {
705 $transient_key = 'sqlite_object_cache_flushed_' . get_current_user_id();
706 if ( get_transient( $transient_key ) ) {
707 delete_transient( $transient_key );
708 $this->show_flush_notice();
709 }
710 }
711
712 /**
713 * Display a notice when the object cache was flushed via admin bar.
714 *
715 * @return void
716 */
717 public function show_flush_notice() {
718 ?>
719 <div class="notice notice-success sqlite-object-cache is-dismissible">
720 <p><?php esc_html_e( 'SQLite Object Cache flushed successfully.', 'sqlite-object-cache' ); ?></p>
721 </div>
722 <?php
723 }
724
725 }
726