PluginProbe
Redirection / 4.6.2
Redirection v4.6.2
5.10.0 5.9.0 5.8.1 5.8.0 3.7.2 3.7.3 4.0 4.0.1 4.1 4.1.1 4.2 4.2.1 4.2.2 4.2.3 4.3 4.3.1 4.3.2 4.3.3 4.4 4.4.1 4.4.2 4.5 4.5.1 4.6.2 4.7.1 All 130 releases
redirection / redirection-admin.php

redirection-admin.php in Redirection 4.6.2, at redirection-admin.php

610 lines 19.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 require_once __DIR__ . '/models/group.php';
4 require_once __DIR__ . '/models/monitor.php';
5 require_once __DIR__ . '/models/file-io.php';
6 require_once __DIR__ . '/database/database.php';
7 require_once __DIR__ . '/redirection-capabilities.php';
8
9 define( 'RED_DEFAULT_PER_PAGE', 25 );
10 define( 'RED_MAX_PER_PAGE', 200 );
11
12 class Redirection_Admin {
13 private static $instance = null;
14 private $monitor;
15 private $fixit_failed = false;
16
17 public static function init() {
18 if ( is_null( self::$instance ) ) {
19 self::$instance = new Redirection_Admin();
20 }
21
22 return self::$instance;
23 }
24
25 public function __construct() {
26 add_action( 'admin_menu', [ $this, 'admin_menu' ] );
27 add_action( 'admin_notices', [ $this, 'update_nag' ] );
28 add_action( 'plugin_action_links_' . basename( dirname( REDIRECTION_FILE ) ) . '/' . basename( REDIRECTION_FILE ), [ $this, 'plugin_settings' ], 10, 4 );
29 add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 4 );
30 add_filter( 'redirection_save_options', [ $this, 'flush_schedule' ] );
31 add_filter( 'set-screen-option', [ $this, 'set_per_page' ], 10, 3 );
32 add_action( 'redirection_redirect_updated', [ $this, 'set_default_group' ], 10, 2 );
33
34 if ( defined( 'REDIRECTION_FLYING_SOLO' ) && REDIRECTION_FLYING_SOLO ) {
35 add_filter( 'script_loader_src', [ $this, 'flying_solo' ], 10, 2 );
36 }
37
38 register_deactivation_hook( REDIRECTION_FILE, [ 'Redirection_Admin', 'plugin_deactivated' ] );
39 register_uninstall_hook( REDIRECTION_FILE, [ 'Redirection_Admin', 'plugin_uninstall' ] );
40
41 $this->monitor = new Red_Monitor( red_get_options() );
42 $this->run_hacks();
43 }
44
45 // These are only called on the single standard site, or in the network admin of the multisite - they run across all available sites
46 public static function plugin_activated() {
47 Red_Database::apply_to_sites( function() {
48 Red_Flusher::clear();
49 red_set_options();
50 } );
51 }
52
53 // These are only called on the single standard site, or in the network admin of the multisite - they run across all available sites
54 public static function plugin_deactivated() {
55 Red_Database::apply_to_sites( function() {
56 Red_Flusher::clear();
57 } );
58 }
59
60 // These are only called on the single standard site, or in the network admin of the multisite - they run across all available sites
61 public static function plugin_uninstall() {
62 $database = Red_Database::get_latest_database();
63
64 Red_Database::apply_to_sites( function() use ( $database ) {
65 $database->remove();
66 } );
67 }
68
69 public function update_nag() {
70 if ( ! Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_OPTION_MANAGE ) ) {
71 return;
72 }
73
74 $status = new Red_Database_Status();
75
76 $message = false;
77 if ( $status->needs_installing() ) {
78 /* translators: 1: URL to plugin page */
79 $message = sprintf( __( 'Please complete your <a href="%s">Redirection setup</a> to activate the plugin.', 'redirection' ), esc_url( $this->get_plugin_url() ) );
80 } elseif ( $status->needs_updating() ) {
81 /* translators: 1: URL to plugin page, 2: current version, 3: target version */
82 $message = sprintf( __( 'Redirection\'s database needs to be updated - <a href="%1$1s">click to update</a>.', 'redirection' ), esc_url( $this->get_plugin_url() ) );
83 }
84
85 if ( ! $message || strpos( Redirection_Request::get_request_url(), 'page=redirection.php' ) !== false ) {
86 return;
87 }
88
89 // Known HTML and so isn't escaped
90 // phpcs:ignore
91 echo '<div class="update-nag">' . $message . '</div>';
92 }
93
94 // So it finally came to this... some plugins include their JS in all pages, whether they are needed or not. If there is an error
95 // then this can prevent Redirection running and it's a little sensitive about that. We use the nuclear option here to disable
96 // all other JS while viewing Redirection
97 public function flying_solo( $src, $handle ) {
98 if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], 'page=redirection.php' ) !== false ) {
99 if ( substr( $src, 0, 4 ) === 'http' && $handle !== 'redirection' && strpos( $src, 'plugins' ) !== false ) {
100 if ( $this->ignore_this_plugin( $src ) ) {
101 return false;
102 }
103 }
104 }
105
106 return $src;
107 }
108
109 private function ignore_this_plugin( $src ) {
110 $ignore = array(
111 'mootools',
112 'wp-seo-',
113 'authenticate',
114 'wordpress-seo',
115 'yikes',
116 );
117
118 foreach ( $ignore as $text ) {
119 if ( strpos( $src, $text ) !== false ) {
120 return true;
121 }
122 }
123
124 return false;
125 }
126
127 public function flush_schedule( $options ) {
128 Red_Flusher::schedule();
129 return $options;
130 }
131
132 public function set_per_page( $status, $option, $value ) {
133 if ( $option === 'redirection_log_per_page' ) {
134 return max( 1, min( intval( $value, 10 ), RED_MAX_PER_PAGE ) );
135 }
136
137 return $status;
138 }
139
140 public function plugin_settings( $links ) {
141 $status = new Red_Database_Status();
142 if ( $status->needs_updating() ) {
143 array_unshift( $links, '<a style="color: red" href="' . esc_url( $this->get_plugin_url() ) . '&amp;sub=support">' . __( 'Upgrade Database', 'redirection' ) . '</a>' );
144 }
145
146 array_unshift( $links, '<a href="' . esc_url( $this->get_plugin_url() ) . '&amp;sub=options">' . __( 'Settings', 'redirection' ) . '</a>' );
147 return $links;
148 }
149
150 public function plugin_row_meta( $plugin_meta, $plugin_file, $plugin_data, $status ) {
151 if ( $plugin_file === basename( dirname( REDIRECTION_FILE ) ) . '/' . basename( REDIRECTION_FILE ) ) {
152 $plugin_data['Description'] .= '<p>' . __( 'Please upgrade your database', 'redirection' ) . '</p>';
153 }
154
155 return $plugin_meta;
156 }
157
158 private function get_plugin_url() {
159 return admin_url( 'tools.php?page=' . basename( REDIRECTION_FILE ) );
160 }
161
162 private function get_first_available_page_url() {
163 $pages = Redirection_Capabilities::get_available_pages();
164
165 if ( count( $pages ) > 0 ) {
166 return $this->get_plugin_url() . ( $pages[0] === 'redirect' ? '' : '&sub=' . rawurlencode( $pages[0] ) );
167 }
168
169 return admin_url();
170 }
171
172 public function redirection_head() {
173 global $wp_version;
174
175 // Does user have access to this page?
176 if ( $this->get_current_page() === false ) {
177 // Redirect to root plugin page
178 wp_safe_redirect( $this->get_first_available_page_url() );
179 die();
180 }
181
182 if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'wp_rest' ) ) {
183 if ( $_REQUEST['action'] === 'fixit' ) {
184 $this->run_fixit();
185 } elseif ( $_REQUEST['action'] === 'rest_api' ) {
186 $this->set_rest_api( intval( $_REQUEST['rest_api'], 10 ) );
187 }
188 }
189
190 $build = REDIRECTION_VERSION . '-' . REDIRECTION_BUILD;
191 $preload = $this->get_preload_data();
192 $options = red_get_options();
193 $versions = array(
194 'Plugin: ' . REDIRECTION_VERSION,
195 'WordPress: ' . $wp_version . ' (' . ( is_multisite() ? 'multi' : 'single' ) . ')',
196 'PHP: ' . phpversion(),
197 'Browser: ' . Redirection_Request::get_user_agent(),
198 'JavaScript: ' . plugin_dir_url( REDIRECTION_FILE ) . 'redirection.js',
199 'REST API: ' . red_get_rest_api(),
200 );
201
202 $this->inject();
203
204 // Add contextual help to some pages
205 if ( in_array( $this->get_current_page(), [ 'redirects', 'log', '404s', 'groups' ], true ) ) {
206 add_screen_option( 'per_page', array(
207 /* translators: maximum number of log entries */
208 'label' => sprintf( __( 'Log entries (%d max)', 'redirection' ), RED_MAX_PER_PAGE ),
209 'default' => RED_DEFAULT_PER_PAGE,
210 'option' => 'redirection_log_per_page',
211 ) );
212 }
213
214 if ( defined( 'REDIRECTION_DEV_MODE' ) && REDIRECTION_DEV_MODE ) {
215 wp_enqueue_script( 'redirection', 'http://localhost:3312/redirection.js', array(), $build, true );
216 } else {
217 wp_enqueue_script( 'redirection', plugin_dir_url( REDIRECTION_FILE ) . 'redirection.js', array(), $build, true );
218 }
219
220 wp_enqueue_style( 'redirection', plugin_dir_url( REDIRECTION_FILE ) . 'redirection.css', array(), $build );
221
222 $status = new Red_Database_Status();
223 $status->check_tables_exist();
224
225 wp_localize_script( 'redirection', 'Redirectioni10n', array(
226 'api' => [
227 'WP_API_root' => esc_url_raw( red_get_rest_api() ),
228 'WP_API_nonce' => wp_create_nonce( 'wp_rest' ),
229 'current' => $options['rest_api'],
230 'routes' => [
231 REDIRECTION_API_JSON => red_get_rest_api( REDIRECTION_API_JSON ),
232 REDIRECTION_API_JSON_INDEX => red_get_rest_api( REDIRECTION_API_JSON_INDEX ),
233 REDIRECTION_API_JSON_RELATIVE => red_get_rest_api( REDIRECTION_API_JSON_RELATIVE ),
234 ],
235 ],
236 'pluginBaseUrl' => plugins_url( '', REDIRECTION_FILE ),
237 'pluginRoot' => $this->get_plugin_url(),
238 'per_page' => $this->get_per_page(),
239 'locale' => $this->get_i18n_data(),
240 'localeSlug' => get_locale(),
241 'settings' => $options,
242 'preload' => $preload,
243 'versions' => implode( "\n", $versions ),
244 'version' => REDIRECTION_VERSION,
245 'database' => $status->get_json(),
246 'caps' => [
247 'pages' => Redirection_Capabilities::get_available_pages(),
248 'capabilities' => Redirection_Capabilities::get_all_capabilities(),
249 ],
250 ) );
251
252 $this->add_help_tab();
253 }
254
255 // Some plugins misbehave, so this attempts to 'fix' them so Redirection can get on with it's work
256 private function run_hacks() {
257 add_filter( 'ip-geo-block-admin', array( $this, 'ip_geo_block' ) );
258 }
259
260 /*
261 * This works around the IP Geo Block plugin being very aggressive and breaking Redirection
262 */
263 public function ip_geo_block( $validate ) {
264 $url = Redirection_Request::get_request_url();
265 $override = array(
266 'tools.php?page=redirection.php',
267 'action=red_proxy&rest_path=redirection',
268 );
269
270 foreach ( $override as $path ) {
271 if ( strpos( $url, $path ) !== false ) {
272 return array(
273 'result' => 'passed',
274 'auth' => false,
275 'asn' => false,
276 'code' => false,
277 'ip' => false,
278 );
279 }
280 }
281
282 return $validate;
283 }
284
285 private function run_fixit() {
286 if ( Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_SUPPORT_MANAGE ) ) {
287 require_once dirname( REDIRECTION_FILE ) . '/models/fixer.php';
288
289 $fixer = new Red_Fixer();
290 $result = $fixer->fix( $fixer->get_status() );
291
292 if ( is_wp_error( $result ) ) {
293 $this->fixit_failed = $result;
294 }
295 }
296 }
297
298 private function set_rest_api( $api ) {
299 if ( $api >= 0 && $api <= REDIRECTION_API_JSON_RELATIVE ) {
300 red_set_options( array( 'rest_api' => intval( $api, 10 ) ) );
301 }
302 }
303
304 private function get_preload_data() {
305 $status = new Red_Database_Status();
306
307 if ( $status->needs_installing() ) {
308 include_once __DIR__ . '/models/importer.php';
309
310 return [
311 'importers' => Red_Plugin_Importer::get_plugins(),
312 ];
313 }
314
315 if ( $this->get_current_page() === 'support' ) {
316 require_once dirname( REDIRECTION_FILE ) . '/models/fixer.php';
317
318 $fixer = new Red_Fixer();
319
320 return array(
321 'pluginStatus' => $fixer->get_json(),
322 );
323 }
324
325 return [];
326 }
327
328 private function add_help_tab() {
329 /* translators: URL */
330 $content = sprintf( __( 'You can find full documentation about using Redirection on the <a href="%s" target="_blank">redirection.me</a> support site.', 'redirection' ), 'https://redirection.me/support/?utm_source=redirection&utm_medium=plugin&utm_campaign=context-help' );
331 $title = __( 'Redirection Support', 'redirection' );
332
333 $current_screen = get_current_screen();
334 $current_screen->add_help_tab( array(
335 'id' => 'redirection',
336 'title' => 'Redirection',
337 'content' => "<h2>$title</h2><p>$content</p>",
338 ) );
339 }
340
341 private function get_per_page() {
342 $per_page = intval( get_user_meta( get_current_user_id(), 'redirection_log_per_page', true ), 10 );
343
344 return $per_page > 0 ? max( 5, min( $per_page, RED_MAX_PER_PAGE ) ) : RED_DEFAULT_PER_PAGE;
345 }
346
347 private function get_i18n_data() {
348 $locale = get_locale();
349
350 // WP 4.7
351 if ( function_exists( 'get_user_locale' ) ) {
352 $locale = get_user_locale();
353 }
354
355 $i18n_json = dirname( REDIRECTION_FILE ) . '/locale/json/redirection-' . $locale . '.json';
356
357 if ( is_file( $i18n_json ) && is_readable( $i18n_json ) ) {
358 // phpcs:ignore
359 $locale_data = @file_get_contents( $i18n_json );
360
361 if ( $locale_data ) {
362 return json_decode( $locale_data );
363 }
364 }
365
366 // Return empty if we have nothing to return so it doesn't fail when parsed in JS
367 return array();
368 }
369
370 public function admin_menu() {
371 $hook = add_management_page( 'Redirection', 'Redirection', Redirection_Capabilities::get_plugin_access(), basename( REDIRECTION_FILE ), [ $this, 'admin_screen' ] );
372 add_action( 'load-' . $hook, [ $this, 'redirection_head' ] );
373 }
374
375 private function check_minimum_wp() {
376 $wp_version = get_bloginfo( 'version' );
377
378 if ( version_compare( $wp_version, REDIRECTION_MIN_WP, '<' ) ) {
379 return false;
380 }
381
382 return true;
383 }
384
385 public function set_default_group( $id, $redirect ) {
386 red_set_options( array( 'last_group_id' => $redirect->get_group_id() ) );
387 }
388
389 public function admin_screen() {
390 if ( count( Redirection_Capabilities::get_all_capabilities() ) === 0 ) {
391 die( 'You do not have sufficient permissions to access this page.' );
392 }
393
394 if ( $this->check_minimum_wp() === false ) {
395 return $this->show_minimum_wordpress();
396 }
397
398 if ( $this->fixit_failed ) {
399 $this->show_fixit_failed();
400 }
401
402 Red_Flusher::schedule();
403
404 $this->show_main();
405 }
406
407 private function show_fixit_failed() {
408 ?>
409 <div class="notice notice-error">
410 <h1><?php echo esc_html( $this->fixit_failed->get_error_message() ); ?></h1>
411 <p><?php echo esc_html( $this->fixit_failed->get_error_data() ); ?></p>
412 </div>
413 <?php
414 }
415
416 private function show_minimum_wordpress() {
417 global $wp_version;
418
419 /* translators: 1: Expected WordPress version, 2: Actual WordPress version */
420 $wp_requirement = sprintf( __( 'Redirection requires WordPress v%1$1s, you are using v%2$2s - please update your WordPress', 'redirection' ), REDIRECTION_MIN_WP, $wp_version );
421 ?>
422 <div class="react-error">
423 <h1><?php esc_html_e( 'Unable to load Redirection', 'redirection' ); ?></h1>
424 <p><?php echo esc_html( $wp_requirement ); ?></p>
425 </div>
426 <?php
427 }
428
429 private function show_load_fail() {
430 ?>
431 <div class="react-error" style="display: none">
432 <h1><?php esc_html_e( 'Unable to load Redirection ☹️', 'redirection' ); ?> v<?php echo esc_html( REDIRECTION_VERSION ); ?></h1>
433 <p><?php esc_html_e( "This may be caused by another plugin - look at your browser's error console for more details.", 'redirection' ); ?></p>
434 <p><?php esc_html_e( 'If you are using a page caching plugin or service (CloudFlare, OVH, etc) then you can also try clearing that cache.', 'redirection' ); ?></p>
435 <p><?php _e( 'Also check if your browser is able to load <code>redirection.js</code>:', 'redirection' ); ?></p>
436 <p><code><?php echo esc_html( plugin_dir_url( REDIRECTION_FILE ) . 'redirection.js?ver=' . rawurlencode( REDIRECTION_VERSION ) . '-' . rawurlencode( REDIRECTION_BUILD ) ); ?></code></p>
437 <p><?php esc_html_e( 'Please note that Redirection requires the WordPress REST API to be enabled. If you have disabled this then you won\'t be able to use Redirection', 'redirection' ); ?></p>
438 <p><?php _e( 'Please see the <a href="https://redirection.me/support/problems/">list of common problems</a>.', 'redirection' ); ?></p>
439 <p><?php esc_html_e( 'If you think Redirection is at fault then create an issue.', 'redirection' ); ?></p>
440 <p class="versions"><?php _e( '<code>Redirectioni10n</code> is not defined. This usually means another plugin is blocking Redirection from loading. Please disable all plugins and try again.', 'redirection' ); ?></p>
441 <p>
442 <a class="button-primary" target="_blank" href="https://github.com/johngodley/redirection/issues/new?title=Problem%20starting%20Redirection%20<?php echo esc_attr( REDIRECTION_VERSION ); ?>">
443 <?php esc_html_e( 'Create Issue', 'redirection' ); ?>
444 </a>
445 </p>
446 </div>
447 <?php
448 }
449
450 private function show_main() {
451 ?>
452 <div id="react-modal"></div>
453 <div id="react-ui">
454 <div class="react-loading">
455 <h1><?php esc_html_e( 'Loading, please wait...', 'redirection' ); ?></h1>
456
457 <span class="react-loading-spinner"></span>
458 </div>
459 <noscript><?php esc_html_e( 'Please enable JavaScript', 'redirection' ); ?></noscript>
460
461 <?php $this->show_load_fail(); ?>
462 </div>
463
464 <script>
465 var prevError = window.onerror;
466 var errors = [];
467 var timeout = 0;
468 var timer = setInterval( function() {
469 if ( isRedirectionLoaded() ) {
470 resetAll();
471 } else if ( errors.length > 0 || timeout++ === 5 ) {
472 showError();
473 }
474 }, 5000 );
475
476 function isRedirectionLoaded() {
477 return typeof redirection !== 'undefined';
478 }
479
480 function showError() {
481 var errorText = "";
482
483 if ( errors.length > 0 ) {
484 errorText = "```\n" + errors.join( ',' ) + "\n```\n\n";
485 }
486
487 resetAll();
488 document.querySelector( '.react-loading' ).style.display = 'none';
489 document.querySelector( '.react-error' ).style.display = 'block';
490
491 if ( typeof Redirectioni10n !== 'undefined' ) {
492 document.querySelector( '.versions' ).innerHTML = Redirectioni10n.versions.replace( /\n/g, '<br />' );
493 document.querySelector( '.react-error .button-primary' ).href += '&body=' + encodeURIComponent( errorText ) + encodeURIComponent( Redirectioni10n.versions );
494 }
495 }
496
497 function resetAll() {
498 clearInterval( timer );
499 window.onerror = prevError;
500 }
501
502 window.onerror = function( error, url, line ) {
503 console.error( error );
504 errors.push( error + ' ' + url + ' ' + line );
505 };
506 </script>
507 <?php
508 }
509
510 /**
511 * Get the current plugin page.
512 * Uses $_GET['sub'] to determine the current page unless a page is supplied.
513 *
514 * @param string $page Current page.
515 *
516 * @return string|boolean Current page, or false.
517 */
518 private function get_current_page( $page = false ) {
519 // $_GET['sub'] is validated below
520 // phpcs:ignore
521 if ( ! $page ) {
522 // phpcs:ignore
523 $page = isset( $_GET['sub'] ) ? $_GET['sub'] : 'redirect';
524 }
525
526 // Are we allowed to access this page?
527 if ( in_array( $page, Redirection_Capabilities::get_available_pages(), true ) ) {
528 // phpcs:ignore
529 return $page;
530 }
531
532 return false;
533 }
534
535 private function inject() {
536 // phpcs:ignore
537 if ( isset( $_GET['page'] ) && $this->get_current_page() !== 'redirect' && $_GET['page'] === 'redirection.php' ) {
538 $this->try_export_logs();
539 $this->try_export_redirects();
540 $this->try_export_rss();
541 }
542 }
543
544 public function try_export_rss() {
545 // phpcs:ignore
546 if ( isset( $_GET['token'] ) && isset( $_GET['sub'] ) && $_GET['sub'] === 'rss' && Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_REDIRECT_MANAGE ) ) {
547 $options = red_get_options();
548
549 // phpcs:ignore
550 if ( $_GET['token'] === $options['token'] && ! empty( $options['token'] ) ) {
551 // phpcs:ignore
552 $items = Red_Item::get_all_for_module( intval( $_GET['module'], 10 ) );
553
554 $exporter = Red_FileIO::create( 'rss' );
555 $exporter->force_download();
556
557 // phpcs:ignore
558 echo $exporter->get_data( $items, array() );
559 die();
560 }
561 }
562 }
563
564 private function try_export_logs() {
565 if ( Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_IO_MANAGE ) && isset( $_POST['export-csv'] ) && check_admin_referer( 'wp_rest' ) ) {
566 if ( $this->get_current_page() === 'log' ) {
567 RE_Log::export_to_csv();
568 } elseif ( $this->get_current_page() === '404s' ) {
569 RE_404::export_to_csv();
570 }
571
572 die();
573 }
574 }
575
576 private function try_export_redirects() {
577 // phpcs:ignore
578 if ( ! isset( $_GET['sub'] ) || $_GET['sub'] !== 'io' ) {
579 return;
580 }
581
582 if ( Redirection_Capabilities::has_access( Redirection_Capabilities::CAP_IO_MANAGE ) && isset( $_GET['exporter'] ) && isset( $_GET['export'] ) && check_admin_referer( 'wp_rest' ) ) {
583 $export = Red_FileIO::export( $_GET['export'], $_GET['exporter'] );
584
585 if ( $export !== false ) {
586 $export['exporter']->force_download();
587
588 // phpcs:ignore
589 echo $export['data'];
590 die();
591 }
592 }
593 }
594 }
595
596 register_activation_hook( REDIRECTION_FILE, array( 'Redirection_Admin', 'plugin_activated' ) );
597
598 add_action( 'init', array( 'Redirection_Admin', 'init' ) );
599
600 // This is causing a lot of problems with the REST API - disable qTranslate
601 add_filter( 'qtranslate_language_detect_redirect', function( $lang, $url ) {
602 $url = Redirection_Request::get_request_url();
603
604 if ( strpos( $url, '/wp-json/' ) !== false || strpos( $url, 'index.php?rest_route' ) !== false ) {
605 return false;
606 }
607
608 return $lang;
609 }, 10, 2 );
610