PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / 2.0.1
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more v2.0.1
4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / freemius / includes / class-fs-logger.php
embedpress / freemius / includes Last commit date
debug 9 years ago entities 9 years ago managers 9 years ago sdk 9 years ago supplements 9 years ago class-freemius-abstract.php 9 years ago class-freemius.php 9 years ago class-fs-api.php 9 years ago class-fs-logger.php 9 years ago class-fs-plugin-updater.php 9 years ago class-fs-security.php 9 years ago fs-core-functions.php 9 years ago fs-essential-functions.php 9 years ago fs-plugin-info-dialog.php 9 years ago i18n.php 9 years ago index.php 9 years ago l10n.php 9 years ago
class-fs-logger.php
688 lines
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
6 * @since 1.0.3
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 class FS_Logger {
14 private $_id;
15 private $_on = false;
16 private $_echo = false;
17 private $_file_start = 0;
18 /**
19 * @var int PHP Process ID.
20 */
21 private static $_processID;
22 /**
23 * @var string PHP Script user name.
24 */
25 private static $_ownerName;
26 /**
27 * @var bool Is storage logging turned on.
28 */
29 private static $_isStorageLoggingOn;
30 /**
31 * @var int ABSPATH length.
32 */
33 private static $_abspathLength;
34
35 private static $LOGGERS = array();
36 private static $LOG = array();
37 private static $CNT = 0;
38 private static $_HOOKED_FOOTER = false;
39
40 private function __construct( $id, $on = false, $echo = false ) {
41 $this->_id = $id;
42
43 $bt = debug_backtrace();
44 $caller = $bt[2];
45
46 if ( false !== strpos( $caller['file'], 'plugins' ) ) {
47 $this->_file_start = strpos( $caller['file'], 'plugins' ) + strlen( 'plugins/' );
48 } else {
49 $this->_file_start = strpos( $caller['file'], 'themes' ) + strlen( 'themes/' );
50 }
51
52 if ( $on ) {
53 $this->on();
54 }
55 if ( $echo ) {
56 $this->echo_on();
57 }
58 }
59
60 /**
61 * @param string $id
62 * @param bool $on
63 * @param bool $echo
64 *
65 * @return FS_Logger
66 */
67 public static function get_logger( $id, $on = false, $echo = false ) {
68 $id = strtolower( $id );
69
70 if ( ! isset( self::$_processID ) ) {
71 self::init();
72 }
73
74 if ( ! isset( self::$LOGGERS[ $id ] ) ) {
75 self::$LOGGERS[ $id ] = new FS_Logger( $id, $on, $echo );
76 }
77
78 return self::$LOGGERS[ $id ];
79 }
80
81 /**
82 * Initialize logging global info.
83 *
84 * @author Vova Feldman (@svovaf)
85 * @since 1.2.1.6
86 */
87 private static function init() {
88 self::$_ownerName = function_exists( 'get_current_user' ) ?
89 get_current_user() :
90 'unknown';
91 self::$_isStorageLoggingOn = ( 1 == get_option( 'fs_storage_logger', 0 ) );
92 self::$_abspathLength = strlen( ABSPATH );
93 self::$_processID = mt_rand( 0, 32000 );
94
95 // Process ID may be `false` on errors.
96 if ( ! is_numeric( self::$_processID ) ) {
97 self::$_processID = 0;
98 }
99 }
100
101 private static function hook_footer() {
102 if ( self::$_HOOKED_FOOTER ) {
103 return;
104 }
105
106 if ( is_admin() ) {
107 add_action( 'admin_footer', 'FS_Logger::dump', 100 );
108 } else {
109 add_action( 'wp_footer', 'FS_Logger::dump', 100 );
110 }
111 }
112
113 function is_on() {
114 return $this->_on;
115 }
116
117 function on() {
118 $this->_on = true;
119
120 if ( ! function_exists( 'dbDelta' ) ) {
121 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
122 }
123
124 self::hook_footer();
125 }
126
127 function echo_on() {
128 $this->on();
129
130 $this->_echo = true;
131 }
132
133 function is_echo_on() {
134 return $this->_echo;
135 }
136
137 function get_id() {
138 return $this->_id;
139 }
140
141 function get_file() {
142 return $this->_file_start;
143 }
144
145 private function _log( &$message, $type = 'log', $wrapper ) {
146 if ( ! $this->is_on() ) {
147 return;
148 }
149
150 $bt = debug_backtrace();
151 $depth = $wrapper ? 3 : 2;
152 while ( $depth < count( $bt ) - 1 && 'eval' === $bt[ $depth ]['function'] ) {
153 $depth ++;
154 }
155
156 $caller = $bt[ $depth ];
157
158 /**
159 * Retrieve the correct call file & line number from backtrace
160 * when logging from a wrapper method.
161 *
162 * @author Vova Feldman
163 * @since 1.2.1.6
164 */
165 if ( empty( $caller['line'] ) ) {
166 $depth --;
167
168 while ( $depth >= 0 ) {
169 if ( ! empty( $bt[ $depth ]['line'] ) ) {
170 $caller['line'] = $bt[ $depth ]['line'];
171 $caller['file'] = $bt[ $depth ]['file'];
172 break;
173 }
174 }
175 }
176
177 $log = array_merge( $caller, array(
178 'cnt' => self::$CNT ++,
179 'logger' => $this,
180 'timestamp' => microtime( true ),
181 'log_type' => $type,
182 'msg' => $message,
183 ) );
184
185 if ( self::$_isStorageLoggingOn ) {
186 $this->db_log( $type, $message, self::$CNT, $caller );
187 }
188
189 self::$LOG[] = $log;
190
191 if ( $this->is_echo_on() && ! Freemius::is_ajax() ) {
192 echo self::format_html( $log ) . "\n";
193 }
194 }
195
196 function log( $message, $wrapper = false ) {
197 $this->_log( $message, 'log', $wrapper );
198 }
199
200 function info( $message, $wrapper = false ) {
201 $this->_log( $message, 'info', $wrapper );
202 }
203
204 function warn( $message, $wrapper = false ) {
205 $this->_log( $message, 'warn', $wrapper );
206 }
207
208 function error( $message, $wrapper = false ) {
209 $this->_log( $message, 'error', $wrapper );
210 }
211
212 /**
213 * Log API error.
214 *
215 * @author Vova Feldman (@svovaf)
216 * @since 1.2.1.5
217 *
218 * @param mixed $api_result
219 * @param bool $wrapper
220 */
221 function api_error( $api_result, $wrapper = false ) {
222 $message = '';
223 if ( is_object( $api_result ) && isset( $api_result->error ) ) {
224 $message = $api_result->error->message;
225 } else if ( is_object( $api_result ) ) {
226 $message = var_export( $api_result, true );
227 } else if ( is_string( $api_result ) ) {
228 $message = $api_result;
229 } else if ( empty( $api_result ) ) {
230 $message = 'Empty API result.';
231 }
232
233 $message = 'API Error: ' . $message;
234
235 $this->_log( $message, 'error', $wrapper );
236 }
237
238 function entrance( $message = '', $wrapper = false ) {
239 $msg = 'Entrance' . ( empty( $message ) ? '' : ' > ' ) . $message;
240
241 $this->_log( $msg, 'log', $wrapper );
242 }
243
244 function departure( $message = '', $wrapper = false ) {
245 $msg = 'Departure' . ( empty( $message ) ? '' : ' > ' ) . $message;
246
247 $this->_log( $msg, 'log', $wrapper );
248 }
249
250 #--------------------------------------------------------------------------------
251 #region Log Formatting
252 #--------------------------------------------------------------------------------
253
254 private static function format( $log, $show_type = true ) {
255 return '[' . str_pad( $log['cnt'], strlen( self::$CNT ), '0', STR_PAD_LEFT ) . '] [' . $log['logger']->_id . '] ' . ( $show_type ? '[' . $log['log_type'] . ']' : '' ) . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . ' >> ' . $log['msg'] . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ') ' : '' ) . ' [' . $log['timestamp'] . ']';
256 }
257
258 private static function format_html( $log ) {
259 return '<div style="font-size: 13px; font-family: monospace; color: #7da767; padding: 8px 3px; background: #000; border-bottom: 1px solid #555;">[' . $log['cnt'] . '] [' . $log['logger']->_id . '] [' . $log['log_type'] . '] <b><code style="color: #c4b1e0;">' . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . '</code> >> <b style="color: #f59330;">' . esc_html( $log['msg'] ) . '</b></b>' . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ')' : '' ) . ' [' . $log['timestamp'] . ']</div>';
260 }
261
262 #endregion
263
264 static function dump() {
265 ?>
266 <!-- BEGIN: Freemius PHP Console Log -->
267 <script type="text/javascript">
268 <?php
269 foreach ( self::$LOG as $log ) {
270 echo 'console.' . $log['log_type'] . '(' . json_encode( self::format( $log, false ) ) . ')' . "\n";
271 }
272 ?>
273 </script>
274 <!-- END: Freemius PHP Console Log -->
275 <?php
276 }
277
278 static function get_log() {
279 return self::$LOG;
280 }
281
282 #--------------------------------------------------------------------------------
283 #region Database Logging
284 #--------------------------------------------------------------------------------
285
286 /**
287 * @author Vova Feldman (@svovaf)
288 * @since 1.2.1.6
289 *
290 * @return bool
291 */
292 public static function is_storage_logging_on() {
293 if ( ! isset( self::$_isStorageLoggingOn ) ) {
294 self::$_isStorageLoggingOn = ( 1 == get_option( 'fs_storage_logger', 0 ) );
295 }
296
297 return self::$_isStorageLoggingOn;
298 }
299
300 /**
301 * Turns on/off database persistent debugging to capture
302 * multi-session logs to debug complex flows like
303 * plugin auto-deactivate on premium version activation.
304 *
305 * @todo Check if Theme Check has issues with DB tables for themes.
306 *
307 * @author Vova Feldman (@svovaf)
308 * @since 1.2.1.6
309 *
310 * @param bool $is_on
311 *
312 * @return bool
313 */
314 public static function _set_storage_logging( $is_on = true ) {
315 global $wpdb;
316
317 $table = "{$wpdb->prefix}fs_logger";
318
319 if ( $is_on ) {
320 /**
321 * Create logging table.
322 *
323 * NOTE:
324 * dbDelta must use KEY and not INDEX for indexes.
325 *
326 * @link https://core.trac.wordpress.org/ticket/2695
327 */
328 $result = $wpdb->query( "CREATE TABLE {$table} (
329 `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
330 `process_id` INT UNSIGNED NOT NULL,
331 `user_name` VARCHAR(64) NOT NULL,
332 `logger` VARCHAR(128) NOT NULL,
333 `log_order` INT UNSIGNED NOT NULL,
334 `type` ENUM('log','info','warn','error') NOT NULL DEFAULT 'log',
335 `message` TEXT NOT NULL,
336 `file` VARCHAR(256) NOT NULL,
337 `line` INT UNSIGNED NOT NULL,
338 `function` VARCHAR(256) NOT NULL,
339 `request_type` ENUM('call','ajax','cron') NOT NULL DEFAULT 'call',
340 `request_url` VARCHAR(1024) NOT NULL,
341 `created` DECIMAL(16, 6) NOT NULL,
342 PRIMARY KEY (`id`),
343 KEY `process_id` (`process_id` ASC),
344 KEY `process_logger` (`process_id` ASC, `logger` ASC),
345 KEY `function` (`function` ASC),
346 KEY `type` (`type` ASC))" );
347 } else {
348 /**
349 * Drop logging table.
350 */
351 $result = $wpdb->query( "DROP TABLE IF EXISTS $table;" );
352 }
353
354 if ( false !== $result ) {
355 update_option( 'fs_storage_logger', ( $is_on ? 1 : 0 ) );
356 }
357
358 return ( false !== $result );
359 }
360
361 /**
362 * @author Vova Feldman (@svovaf)
363 * @since 1.2.1.6
364 *
365 * @param string $type
366 * @param string $message
367 * @param int $log_order
368 * @param array $caller
369 *
370 * @return false|int
371 */
372 private function db_log(
373 &$type,
374 &$message,
375 &$log_order,
376 &$caller
377 ) {
378 global $wpdb;
379
380 $request_type = 'call';
381 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
382 $request_type = 'cron';
383 } else if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
384 $request_type = 'ajax';
385 }
386
387 $request_url = WP_FS__IS_HTTP_REQUEST ?
388 $_SERVER['REQUEST_URI'] :
389 '';
390
391 return $wpdb->insert(
392 "{$wpdb->prefix}fs_logger",
393 array(
394 'process_id' => self::$_processID,
395 'user_name' => self::$_ownerName,
396 'logger' => $this->_id,
397 'log_order' => $log_order,
398 'type' => $type,
399 'request_type' => $request_type,
400 'request_url' => $request_url,
401 'message' => $message,
402 'file' => isset( $caller['file'] ) ?
403 substr( $caller['file'], self::$_abspathLength ) :
404 '',
405 'line' => $caller['line'],
406 'function' => ( ! empty( $caller['class'] ) ? $caller['class'] . $caller['type'] : '' ) . $caller['function'],
407 'created' => microtime( true ),
408 )
409 );
410 }
411
412 /**
413 * Persistent DB logger columns.
414 *
415 * @var array
416 */
417 private static $_log_columns = array(
418 'id',
419 'process_id',
420 'user_name',
421 'logger',
422 'log_order',
423 'type',
424 'message',
425 'file',
426 'line',
427 'function',
428 'request_type',
429 'request_url',
430 'created',
431 );
432
433 /**
434 * Create DB logs query.
435 *
436 * @author Vova Feldman (@svovaf)
437 * @since 1.2.1.6
438 *
439 * @param bool $filters
440 * @param int $limit
441 * @param int $offset
442 * @param bool $order
443 * @param bool $escape_eol
444 *
445 * @return string
446 */
447 private static function build_db_logs_query(
448 $filters = false,
449 $limit = 200,
450 $offset = 0,
451 $order = false,
452 $escape_eol = false
453 ) {
454 global $wpdb;
455
456 $select = '*';
457
458 if ( $escape_eol ) {
459 $select = '';
460 for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) {
461 if ( $i > 0 ) {
462 $select .= ', ';
463 }
464
465 if ( 'message' !== self::$_log_columns[ $i ] ) {
466 $select .= self::$_log_columns[ $i ];
467 } else {
468 $select .= 'REPLACE(message , \'\n\', \' \') AS message';
469 }
470 }
471 }
472
473 $query = "SELECT {$select} FROM {$wpdb->prefix}fs_logger";
474 if ( is_array( $filters ) ) {
475 $criteria = array();
476
477 if ( ! empty( $filters['type'] ) && 'all' !== $filters['type'] ) {
478 $filters['type'] = strtolower( $filters['type'] );
479
480 switch ( $filters['type'] ) {
481 case 'warn_error':
482 $criteria[] = array( 'col' => 'type', 'val' => array( 'warn', 'error' ) );
483 break;
484 case 'error':
485 case 'warn':
486 $criteria[] = array( 'col' => 'type', 'val' => $filters['type'] );
487 break;
488 case 'info':
489 default:
490 $criteria[] = array( 'col' => 'type', 'val' => array( 'info', 'log' ) );
491 break;
492 }
493 }
494
495 if ( ! empty( $filters['request_type'] ) ) {
496 $filters['request_type'] = strtolower( $filters['request_type'] );
497
498 if ( in_array( $filters['request_type'], array( 'call', 'ajax', 'cron' ) ) ) {
499 $criteria[] = array( 'col' => 'request_type', 'val' => $filters['request_type'] );
500 }
501 }
502
503 if ( ! empty( $filters['file'] ) ) {
504 $criteria[] = array(
505 'col' => 'file',
506 'op' => 'LIKE',
507 'val' => '%' . esc_sql( $filters['file'] ),
508 );
509 }
510
511 if ( ! empty( $filters['function'] ) ) {
512 $criteria[] = array(
513 'col' => 'function',
514 'op' => 'LIKE',
515 'val' => '%' . esc_sql( $filters['function'] ),
516 );
517 }
518
519 if ( ! empty( $filters['process_id'] ) && is_numeric( $filters['process_id'] ) ) {
520 $criteria[] = array( 'col' => 'process_id', 'val' => $filters['process_id'] );
521 }
522
523 if ( ! empty( $filters['logger'] ) ) {
524 $criteria[] = array(
525 'col' => 'logger',
526 'op' => 'LIKE',
527 'val' => '%' . esc_sql( $filters['logger'] ) . '%',
528 );
529 }
530
531 if ( ! empty( $filters['message'] ) ) {
532 $criteria[] = array(
533 'col' => 'message',
534 'op' => 'LIKE',
535 'val' => '%' . esc_sql( $filters['message'] ) . '%',
536 );
537 }
538
539 if ( 0 < count( $criteria ) ) {
540 $query .= "\nWHERE\n";
541
542 $first = true;
543 foreach ( $criteria as $c ) {
544 if ( ! $first ) {
545 $query .= "AND\n";
546 }
547
548 if ( is_array( $c['val'] ) ) {
549 $operator = 'IN';
550
551 for ( $i = 0, $len = count( $c['val'] ); $i < $len; $i ++ ) {
552 $c['val'][ $i ] = "'" . esc_sql( $c['val'][ $i ] ) . "'";
553 }
554
555 $val = '(' . implode( ',', $c['val'] ) . ')';
556 } else {
557 $operator = ! empty( $c['op'] ) ? $c['op'] : '=';
558 $val = "'" . esc_sql( $c['val'] ) . "'";
559 }
560
561 $query .= "`{$c['col']}` {$operator} {$val}\n";
562
563 $first = false;
564 }
565 }
566 }
567
568 if ( ! is_array( $order ) ) {
569 $order = array(
570 'col' => 'id',
571 'order' => 'desc'
572 );
573 }
574
575 $query .= " ORDER BY {$order['col']} {$order['order']} LIMIT {$offset},{$limit}";
576
577 return $query;
578 }
579
580 /**
581 * Load logs from DB.
582 *
583 * @author Vova Feldman (@svovaf)
584 * @since 1.2.1.6
585 *
586 * @param bool $filters
587 * @param int $limit
588 * @param int $offset
589 * @param bool $order
590 *
591 * @return object[]|null
592 */
593 public static function load_db_logs(
594 $filters = false,
595 $limit = 200,
596 $offset = 0,
597 $order = false
598 ) {
599 global $wpdb;
600
601 $query = self::build_db_logs_query(
602 $filters,
603 $limit,
604 $offset,
605 $order
606 );
607
608 return $wpdb->get_results( $query );
609 }
610
611 /**
612 * Load logs from DB.
613 *
614 * @author Vova Feldman (@svovaf)
615 * @since 1.2.1.6
616 *
617 * @param bool $filters
618 * @param string $filename
619 * @param int $limit
620 * @param int $offset
621 * @param bool $order
622 *
623 * @return false|string File download URL or false on failure.
624 */
625 public static function download_db_logs(
626 $filters = false,
627 $filename = '',
628 $limit = 10000,
629 $offset = 0,
630 $order = false
631 ) {
632 global $wpdb;
633
634 $query = self::build_db_logs_query(
635 $filters,
636 $limit,
637 $offset,
638 $order,
639 true
640 );
641
642 $upload_dir = wp_upload_dir();
643 if ( empty( $filename ) ) {
644 $filename = 'fs-logs-' . date( 'Y-m-d_H-i-s', WP_FS__SCRIPT_START_TIME ) . '.csv';
645 }
646 $filepath = rtrim( $upload_dir['path'], '/' ) . "/{$filename}";
647
648 $query .= " INTO OUTFILE '{$filepath}' FIELDS TERMINATED BY '\t' ESCAPED BY '\\\\' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\\n'";
649
650 $columns = '';
651 for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) {
652 if ( $i > 0 ) {
653 $columns .= ', ';
654 }
655
656 $columns .= "'" . self::$_log_columns[ $i ] . "'";
657 }
658
659 $query = "SELECT {$columns} UNION ALL " . $query;
660
661 $result = $wpdb->query( $query );
662
663 if ( false === $result ) {
664 return false;
665 }
666
667 return rtrim( $upload_dir['url'], '/' ) . '/' . $filename;
668 }
669
670 /**
671 * @author Vova Feldman (@svovaf)
672 * @since 1.2.1.6
673 *
674 * @param string $filename
675 *
676 * @return string
677 */
678 public static function get_logs_download_url( $filename = '' ) {
679 $upload_dir = wp_upload_dir();
680 if ( empty( $filename ) ) {
681 $filename = 'fs-logs-' . date( 'Y-m-d_H-i-s', WP_FS__SCRIPT_START_TIME ) . '.csv';
682 }
683
684 return rtrim( $upload_dir['url'], '/' ) . $filename;
685 }
686
687 #endregion
688 }