PluginProbe
Stream – Activity Log & Audit Trail / 3.0.1
Stream – Activity Log & Audit Trail v3.0.1
4.4.0 4.3.0 4.2.2 4.2.1 trunk 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.1 3.1.1 3.10.0 3.2.0 3.2.1 3.2.2 3.2.3 All 50 releases
← All changes | classes/class-plugin.php +31 -501 trunk3.0.1 View file →
@@ -1,27 +1,14 @@
1 1 <?php
2 -/**
3 - * Initializes plugin
4 - *
5 - * @package WP_Stream;
6 - */
7 -
8 2 namespace WP_Stream;
9 3
10 -use RuntimeException;
11 -
12 -/**
13 - * Class Plugin
14 - */
15 4 class Plugin {
16 5 /**
17 - * Plugin version number.
6 + * Plugin version number
18 7 *
19 - * TODO Maybe pass this as a constructor dependency?
20 - *
21 8 * @const string
22 9 */
23 - const VERSION = '4.4.0';
10 + const VERSION = '3.0.1';
24 11
25 12 /**
26 13 * WP-CLI command
27 14 *
@@ -28,116 +15,39 @@
28 15 * @const string
29 16 */
30 17 const WP_CLI_COMMAND = 'stream';
31 18
32 -
33 19 /**
34 - * Used to check if it's a single site, not multisite.
35 - *
36 - * @const string
37 - */
38 - const SINGLE_SITE = 'single';
39 -
40 - /**
41 - * Used to check if it's a multisite with the plugin network enabled.
42 - *
43 - * @const string
44 - */
45 - const MULTI_NETWORK = 'multisite-network';
46 -
47 - /**
48 - * Used to check if it's a multisite with the plugin not network enabled.
49 - *
50 - * @const string
51 - */
52 - const MULTI_NOT_NETWORK = 'multisite-not-network';
53 -
54 - /**
55 - * Holds and manages WordPress Admin configurations.
56 - *
57 20 * @var Admin
58 21 */
59 22 public $admin;
60 23
61 24 /**
62 - * Holds and manages alerts.
63 - *
64 - * @var Alerts
65 - */
66 - public $alerts;
67 -
68 - /**
69 - * Holds and manages alerts lists.
70 - *
71 - * @var Alerts_List
72 - */
73 - public $alerts_list;
74 -
75 - /**
76 - * Holds and manages WordPress Abilities API integration.
77 - *
78 - * @var Abilities
79 - */
80 - public $abilities;
81 -
82 - /**
83 - * Holds and manages connectors
84 - *
85 25 * @var Connectors
86 26 */
87 27 public $connectors;
88 28
89 29 /**
90 - * Holds and manages DB connections.
91 - *
92 30 * @var DB
93 31 */
94 32 public $db;
95 33
96 34 /**
97 - * Holds and manages records.
98 - *
99 35 * @var Log
100 36 */
101 37 public $log;
102 38
103 39 /**
104 - * Stores and manages WordPress settings.
105 - *
106 40 * @var Settings
107 41 */
108 42 public $settings;
109 43
110 44 /**
111 - * Process DB migrations.
112 - *
113 45 * @var Install
114 46 */
115 47 public $install;
116 48
117 49 /**
118 - * Backend used to schedule Stream's deferred work (purge / reset).
119 - *
120 - * Either an {@see AS_Scheduler} (Action Scheduler, default) or a
121 - * {@see Cron_Scheduler} (WP-Cron fallback), selected at construction via
122 - * the `wp_stream_use_action_scheduler` filter.
123 - *
124 - * @var Scheduler
125 - */
126 - public $scheduler;
127 -
128 - /**
129 - * Whether the bundled Action Scheduler library was loaded.
130 - *
131 - * Set from a file_exists() check at construction (see __construct), so it
132 - * is reliable on `plugins_loaded` even though AS only declares its as_*()
133 - * API later on `init`.
134 - *
135 - * @var bool
136 - */
137 - protected $action_scheduler_available = false;
138 -
139 - /**
140 50 * URLs and Paths used by the plugin
141 51 *
142 52 * @var array
143 53 */
@@ -143,15 +53,8 @@
143 53 */
144 54 public $locations = array();
145 55
146 56 /**
147 - * IP address for the current request to be associated with the log entry.
148 - *
149 - * @var null|false|string Valid IP address, null if not set, false if invalid.
150 - */
151 - protected $client_ip_address;
152 -
153 - /**
154 57 * Class constructor
155 58 */
156 59 public function __construct() {
157 60 $locate = $this->locate_plugin();
@@ -165,80 +68,43 @@
165 68 );
166 69
167 70 spl_autoload_register( array( $this, 'autoload' ) );
168 71
169 - // Determine the scheduler backend, then load Action Scheduler only if
170 - // it is the selected backend. AS remains a hard, bundled dependency
171 - // for the WordPress.org build (see issue #1907), but integrators who
172 - // opt into the WP-Cron fallback via `wp_stream_use_action_scheduler`
173 - // pay neither the require_once nor AS's own `init` bootstrap.
174 - //
175 - // Availability is tracked from a file_exists() check rather than
176 - // function_exists(): AS only declares its as_*() API on `init`, but
177 - // this constructor runs at plugin-file inclusion time (before the
178 - // `plugins_loaded` action fires), so the functions are not defined
179 - // yet. Scheduler methods are only ever called on/after `init`
180 - // (wp_loaded, AJAX, cron), by which point the API is loaded.
181 - $action_scheduler = $this->locations['dir'] . '/vendor/woocommerce/action-scheduler/action-scheduler.php';
182 - $this->action_scheduler_available = file_exists( $action_scheduler );
183 -
184 - // Select the scheduler backend for Stream's deferred work, loading the
185 - // bundled AS library only if it is the chosen backend.
186 - $this->scheduler = $this->create_scheduler();
187 -
188 - // Load helper functions.
72 + // Load helper functions
189 73 require_once $this->locations['inc_dir'] . 'functions.php';
190 74
191 - // Load DB helper interface/class.
192 - $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' );
193 - $driver = null;
194 -
195 - if ( class_exists( $driver_class ) ) {
196 - $driver = new $driver_class();
197 - $this->db = new DB( $driver );
75 + // Load DB helper interface/class
76 + $driver = '\WP_Stream\DB';
77 + if ( class_exists( $driver ) ) {
78 + $this->db = new DB( $this );
198 79 }
199 80
200 - $error = false;
201 81 if ( ! $this->db ) {
202 - $error = esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' );
203 - } elseif ( ! $driver instanceof DB_Driver ) {
204 - $error = esc_html__( 'Stream: DB driver must implement DB Driver interface.', 'stream' );
205 - }
206 -
207 - if ( $error ) {
208 82 wp_die(
209 - esc_html( $error ),
83 + esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' ),
210 84 esc_html__( 'Stream DB Error', 'stream' )
211 85 );
212 86 }
213 87
214 - // Load languages.
88 + // Load languages
215 89 add_action( 'plugins_loaded', array( $this, 'i18n' ) );
216 90
217 - // Load logger class.
91 + // Load logger class
218 92 $this->log = apply_filters( 'wp_stream_log_handler', new Log( $this ) );
219 93
220 - // Set the IP address for the current request.
221 - $this->client_ip_address = wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP );
222 -
223 - // Load settings and connectors after widgets_init and before the default init priority.
94 + // Load settings and connectors after widgets_init and before the default init priority
224 95 add_action( 'init', array( $this, 'init' ), 9 );
225 96
226 - // Add frontend indicator.
97 + // Add frontend indicator
227 98 add_action( 'wp_head', array( $this, 'frontend_indicator' ) );
228 99
229 - // Change DB driver after plugin loaded if any add-ons want to replace.
230 - add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ), 20 );
231 -
232 - // Load admin area classes.
233 - if ( is_admin() || ( defined( 'WP_STREAM_DEV_DEBUG' ) && WP_STREAM_DEV_DEBUG ) || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
100 + // Load admin area classes
101 + if ( is_admin() || ( defined( 'WP_STREAM_DEV_DEBUG' ) && WP_STREAM_DEV_DEBUG ) ) {
234 102 $this->admin = new Admin( $this );
235 - $this->install = $driver->setup_storage( $this );
236 - } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) {
237 - $this->admin = new Admin( $this, $driver );
103 + $this->install = new Install( $this );
238 104 }
239 105
240 - // Load WP-CLI command.
106 + // Load WP-CLI command
241 107 if ( defined( 'WP_CLI' ) && WP_CLI ) {
242 108 \WP_CLI::add_command( self::WP_CLI_COMMAND, 'WP_Stream\CLI' );
243 109 }
244 110 }
@@ -243,71 +109,14 @@
243 109 }
244 110 }
245 111
246 112 /**
247 - * Build the scheduler backend for Stream's deferred work (purge / reset).
248 - *
249 - * Defaults to Action Scheduler when its bundled library is present.
250 - * Integrators that bundle Stream and run reliable cron (e.g. Cavalcade)
251 - * can force the WP-Cron fallback by returning false from
252 - * `wp_stream_use_action_scheduler`. When the fallback is chosen, the
253 - * bundled AS library is not loaded at all (no require_once, no AS `init`
254 - * bootstrap). See issue #1907.
255 - *
256 - * @return Scheduler
257 - */
258 - public function create_scheduler() {
259 - /**
260 - * Filter whether Stream uses Action Scheduler for its deferred work.
261 - *
262 - * IMPORTANT — timing: this filter is applied in Plugin::__construct(),
263 - * which runs when the Stream plugin file is included, i.e. BEFORE the
264 - * `plugins_loaded` action. Callbacks must therefore be registered from
265 - * code that loads before Stream: an mu-plugin, wp-config.php, or a
266 - * plugin guaranteed to load earlier. Registering it from a regular
267 - * plugin's `plugins_loaded` hook is too late and will be ignored.
268 - *
269 - * @param bool $use_action_scheduler Whether to use Action Scheduler.
270 - * Defaults to true when the bundled
271 - * AS library is present. Return a
272 - * real boolean: the value is cast
273 - * with (bool), so PHP string
274 - * truthiness applies to strings
275 - * (e.g. 'false' is truthy).
276 - */
277 - $use_action_scheduler = (bool) apply_filters(
278 - 'wp_stream_use_action_scheduler',
279 - $this->action_scheduler_available
280 - );
281 -
282 - if ( ! $use_action_scheduler ) {
283 - return new Cron_Scheduler();
284 - }
285 -
286 - // Guard a forced `__return_true` override when the bundled AS library
287 - // is absent: returning AS_Scheduler without loading AS would fatal on
288 - // the first unguarded as_*() call. Fall back to the cron scheduler
289 - // instead. The default path never hits this — the filter defaults to
290 - // $this->action_scheduler_available.
291 - if ( ! $this->action_scheduler_available ) {
292 - return new Cron_Scheduler();
293 - }
294 -
295 - // Load the bundled AS library before instantiating its scheduler.
296 - // AS's own ActionScheduler_Versions arbitration handles a host that
297 - // already provides its own copy.
298 - require_once $this->locations['dir'] . '/vendor/woocommerce/action-scheduler/action-scheduler.php';
299 -
300 - return new AS_Scheduler();
301 - }
302 -
303 - /**
304 113 * Autoloader for classes
305 114 *
306 - * @param string $class_name Fully qualified classname to be loaded.
115 + * @param string $class
307 116 */
308 - public function autoload( $class_name ) {
309 - if ( ! preg_match( '/^(?P<namespace>.+)\\\\(?P<autoload>[^\\\\]+)$/', $class_name, $matches ) ) {
117 + function autoload( $class ) {
118 + if ( ! preg_match( '/^(?P<namespace>.+)\\\\(?P<autoload>[^\\\\]+)$/', $class, $matches ) ) {
310 119 return;
311 120 }
312 121
313 122 static $reflection;
@@ -337,19 +146,16 @@
337 146 public function i18n() {
338 147 load_plugin_textdomain( 'stream', false, dirname( $this->locations['plugin'] ) . '/languages/' );
339 148 }
340 149
341 - /**
342 - * Load Settings, Notifications, and Connectors
150 + /*
151 + * Load Settings and Connectors
343 152 *
344 153 * @action init
345 154 */
346 155 public function init() {
347 - $this->settings = new Settings( $this );
348 - $this->connectors = new Connectors( $this );
349 - $this->alerts = new Alerts( $this );
350 - $this->alerts_list = new Alerts_List( $this );
351 - $this->abilities = new Abilities( $this );
156 + $this->settings = new Settings( $this );
157 + $this->connectors = new Connectors( $this );
352 158 }
353 159
354 160 /**
355 161 * Displays an HTML comment in the frontend head to indicate that Stream is activated,
@@ -359,10 +165,9 @@
359 165 *
360 166 * @return string|void An HTML comment, or nothing if the value is filtered out.
361 167 */
362 168 public function frontend_indicator() {
363 - /* translators: Localization not needed */
364 - $comment = sprintf( 'Stream WordPress user activity plugin v%s', esc_html( $this->get_version() ) );
169 + $comment = sprintf( 'Stream WordPress user activity plugin v%s', esc_html( $this->get_version() ) ); // Localization not needed
365 170
366 171 /**
367 172 * Filter allows the HTML output of the frontend indicator comment
368 173 * to be altered or removed, if desired.
@@ -371,9 +176,9 @@
371 176 */
372 177 $comment = apply_filters( 'wp_stream_frontend_indicator', $comment );
373 178
374 179 if ( ! empty( $comment ) ) {
375 - printf( "<!-- %s -->\n", esc_html( $comment ) );
180 + echo sprintf( "<!-- %s -->\n", esc_html( $comment ) ); // xss ok
376 181 }
377 182 }
378 183
379 184 /**
@@ -379,15 +184,17 @@
379 184 /**
380 185 * Version of plugin_dir_url() which works for plugins installed in the plugins directory,
381 186 * and for plugins bundled with themes.
382 187 *
188 + * @throws \Exception
189 + *
383 190 * @return array
384 191 */
385 192 private function locate_plugin() {
386 - $dir_url = trailingslashit( plugins_url( '', __DIR__ ) );
387 - $dir_path = plugin_dir_path( __DIR__ );
388 - $dir_basename = basename( $dir_path );
389 - $plugin_basename = trailingslashit( $dir_basename ) . 'stream.php';
193 + $dir_url = trailingslashit( plugins_url( '', dirname( __FILE__ ) ) );
194 + $dir_path = plugin_dir_path( dirname( __FILE__ ) );
195 + $dir_basename = basename( $dir_path );
196 + $plugin_basename = trailingslashit( $dir_basename ) . $dir_basename. '.php';
390 197
391 198 return compact( 'dir_url', 'dir_path', 'dir_basename', 'plugin_basename' );
392 199 }
393 200
@@ -397,283 +204,6 @@
397 204 * @return string
398 205 */
399 206 public function get_version() {
400 207 return self::VERSION;
401 - }
402 -
403 - /**
404 - * Change plugin database driver in case driver plugin loaded after stream
405 - */
406 - public function plugins_loaded() {
407 - // Load DB helper interface/class.
408 - $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' );
409 -
410 - if ( class_exists( $driver_class ) ) {
411 - $driver = new $driver_class();
412 - $this->db = new DB( $driver );
413 - }
414 - }
415 -
416 - /**
417 - * Returns true if Stream is network activated, otherwise false
418 - *
419 - * @return bool
420 - */
421 - public function is_network_activated() {
422 -
423 - $is_network_activated = false;
424 -
425 - if ( $this->is_mustuse() ) {
426 - $is_network_activated = true;
427 - } else {
428 - if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
429 - require_once ABSPATH . '/wp-admin/includes/plugin.php';
430 - }
431 - $is_network_activated = is_plugin_active_for_network( $this->locations['plugin'] );
432 - }
433 -
434 - /**
435 - * Filter allows the network activated detection to be overridden.
436 - *
437 - * @param string $is_network_activated Whether the plugin is network activated.
438 - * @param WP_Stream\Plugin $plugin The stream plugin object.
439 - */
440 - return apply_filters( 'wp_stream_is_network_activated', $is_network_activated, $this );
441 - }
442 -
443 - /**
444 - * Returns true if Stream is a must-use plugin, otherwise false
445 - *
446 - * @return bool
447 - */
448 - public function is_mustuse() {
449 - $stream_php = trailingslashit( WPMU_PLUGIN_DIR ) . $this->locations['plugin'];
450 -
451 - if ( file_exists( $stream_php ) && class_exists( 'WP_Stream\Plugin' ) ) {
452 - return true;
453 - }
454 -
455 - return false;
456 - }
457 -
458 - /**
459 - * Get the IP address for the current request.
460 - *
461 - * @return false|null|string Valid IP address, null if not set, false if invalid.
462 - */
463 - public function get_client_ip_address() {
464 - return apply_filters( 'wp_stream_client_ip_address', $this->client_ip_address );
465 - }
466 -
467 - /**
468 - * Get the site type.
469 - *
470 - * This function determines the type of site based on whether it is a single site or a multisite.
471 - * If it is a multisite, it also checks if it is network activated or not.
472 - *
473 - * @return string The site type
474 - */
475 - public function get_site_type(): string {
476 -
477 - // If it's a multisite, is it network activated or not?
478 - if ( is_multisite() ) {
479 - return $this->is_network_activated() ? self::MULTI_NETWORK : self::MULTI_NOT_NETWORK;
480 - }
481 -
482 - return self::SINGLE_SITE;
483 - }
484 -
485 - /**
486 - * Should the number of records which need to be processed be considered "large"?
487 - *
488 - * @param int $record_number The number of rows in the {$wpdb->prefix}_stream table to be processed.
489 - * @return bool Whether or not this should be considered large.
490 - */
491 - public function is_large_records_table( int $record_number ): bool {
492 - /**
493 - * Filters whether or not the number of records should be considered a large table.
494 - *
495 - * @since 4.1.0
496 - *
497 - * @param bool $is_large_table Whether or not the number of records should be considered large.
498 - * @param int $record_number The number of records being checked.
499 - */
500 - return apply_filters( 'wp_stream_is_large_records_table', $record_number > 1000000, $record_number );
501 - }
502 -
503 - /**
504 - * Checks if the plugin is running on a single site installation.
505 - *
506 - * @return bool True if the plugin is running on a single site installation, false otherwise.
507 - */
508 - public function is_single_site() {
509 - return self::SINGLE_SITE === $this->get_site_type();
510 - }
511 -
512 - /**
513 - * Check if the plugin is activated on a multisite installation but not network activated.
514 - *
515 - * @return bool True if the plugin is activated on a multisite installation but not network activated, false otherwise.
516 - */
517 - public function is_multisite_not_network_activated() {
518 - return self::MULTI_NOT_NETWORK === $this->get_site_type();
519 - }
520 -
521 - /**
522 - * Check if the plugin is activated on a multisite network.
523 - *
524 - * @return bool True if the plugin is network activated on a multisite, false otherwise.
525 - */
526 - public function is_multisite_network_activated() {
527 - return self::MULTI_NETWORK === $this->get_site_type();
528 - }
529 -
530 - /**
531 - * Enqueue a script along with a stylesheet if it exists.
532 - *
533 - * @param string $handle Script handle.
534 - * @param array $additional_dependencies Additional dependencies.
535 - * @param array $data Data to pass to the script.
536 - *
537 - * @throws RuntimeException If built JavaScript assets are not found.
538 - * @return void
539 - */
540 - public function enqueue_asset( $handle, $additional_dependencies = array(), $data = array() ) {
541 - // If is enqueued already, bail out.
542 - if ( wp_script_is( $handle ) ) {
543 - return;
544 - }
545 -
546 - $path = untrailingslashit( $this->locations['dir'] );
547 - $url = untrailingslashit( $this->locations['url'] );
548 -
549 - $script_asset_path = "$path/build/$handle.asset.php";
550 -
551 - if ( ! file_exists( $script_asset_path ) ) {
552 - throw new RuntimeException( 'Built JavaScript assets not found. Please run `npm run build`' );
553 - }
554 -
555 - $script_asset = require $script_asset_path; // phpcs:disable WPThemeReview.CoreFunctionality.FileInclude.FileIncludeFound
556 -
557 - wp_enqueue_script(
558 - "wp-stream-$handle",
559 - "$url/build/$handle.js",
560 - array_merge(
561 - $script_asset['dependencies'],
562 - (array) $additional_dependencies
563 - ),
564 - $script_asset['version'],
565 - true
566 - );
567 -
568 - if ( file_exists( "$path/build/$handle.css" ) ) {
569 - wp_enqueue_style(
570 - "wp-stream-$handle",
571 - "$url/build/$handle.css",
572 - array(),
573 - $script_asset['version']
574 - );
575 - }
576 -
577 - if ( ! empty( $data ) ) {
578 - wp_add_inline_script(
579 - "wp-stream-$handle",
580 - sprintf( 'window["%s"] = %s;', esc_attr( "wp-stream-$handle" ), wp_json_encode( $data ) ),
581 - 'before'
582 - );
583 - }
584 - }
585 -
586 - /**
587 - * Enqueue select2 script and locale file if exists.
588 - *
589 - * @return string Script handle.
590 - */
591 - public function with_select2() {
592 - $handle = 'wp-stream-select2';
593 -
594 - // If is enqueued already, bail out.
595 - if ( wp_script_is( $handle ) ) {
596 - return $handle;
597 - }
598 -
599 - $path = untrailingslashit( $this->locations['dir'] );
600 - $url = untrailingslashit( $this->locations['url'] );
601 -
602 - wp_enqueue_script(
603 - $handle,
604 - "$url/build/select2/js/select2.full.min.js",
605 - array( 'jquery' ),
606 - filemtime( "$path/build/select2/js/select2.full.min.js" ),
607 - true
608 - );
609 - wp_enqueue_style(
610 - $handle,
611 - "$url/build/select2/css/select2.min.css",
612 - array(),
613 - filemtime( "$path/build/select2/css/select2.min.css" )
614 - );
615 -
616 - $locale = get_locale();
617 - $lang = substr( $locale, 0, 2 );
618 - $search_files = array( $locale, $lang, 'en' );
619 -
620 - foreach ( $search_files as $search_file ) {
621 - if ( file_exists( "$path/build/select2/js/i18n/$search_file.js" ) ) {
622 - wp_enqueue_script(
623 - sanitize_title( "$handle-$search_file" ),
624 - "$url/build/select2/js/i18n/$search_file.js",
625 - array( $handle ),
626 - filemtime( "$path/build/select2/js/i18n/$search_file.js" ),
627 - true
628 - );
629 - break;
630 - }
631 - }
632 -
633 - return $handle;
634 - }
635 -
636 - /**
637 - * Enqueue jquery.timeago script and locale file if exists.
638 - *
639 - * @return string Script handle.
640 - */
641 - public function with_jquery_timeago() {
642 - $handle = 'wp-stream-jquery-timeago';
643 -
644 - // If is enqueued already, bail out.
645 - if ( wp_script_is( $handle ) ) {
646 - return $handle;
647 - }
648 -
649 - $path = untrailingslashit( $this->locations['dir'] );
650 - $url = untrailingslashit( $this->locations['url'] );
651 -
652 - wp_enqueue_script(
653 - $handle,
654 - "$url/build/timeago/js/jquery.timeago.js",
655 - array( 'jquery' ),
656 - filemtime( "$path/build/timeago/js/jquery.timeago.js" ),
657 - true
658 - );
659 -
660 - $locale = get_locale();
661 - $lang = substr( $locale, 0, 2 );
662 - $search_files = array( $locale, $lang, 'en' );
663 -
664 - foreach ( $search_files as $search_file ) {
665 - if ( file_exists( "$path/build/timeago/js/locales/jquery.timeago.$search_file.js" ) ) {
666 - wp_enqueue_script(
667 - sanitize_title( "$handle-$search_file" ),
668 - "$url/build/timeago/js/locales/jquery.timeago.$search_file.js",
669 - array( $handle ),
670 - filemtime( "$path/build/timeago/js/locales/jquery.timeago.$search_file.js" ),
671 - true
672 - );
673 - break;
674 - }
675 - }
676 -
677 - return $handle;
678 208 }
679 209 }