PluginProbe
Route ‑ Shipping Protection / 2.4.19
Route ‑ Shipping Protection v2.4.19
2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.2.4 2.2.5 2.2.6 2.2.8 2.2.9 2.3.0 2.3.1 All 153 releases
← All changes | includes/class-routeapp-setup.php +323 -10 2.2.4 → 2.4.19 View file →
@@ -38,8 +38,12 @@
38 38 const DASHBOARD = 'https://dashboard.route.com/login?redirect=onboarding';
39 39 const DASHBOARD_STAGE = 'https://dashboard-stage.route.com/login?redirect=onboarding';
40 40 const REDIRECTED = '2';
41 41 const MODULE_INSTALLED = '1';
42 + /**
43 + * After duplicate-user / login-conflict notice auto-redirect runs once, show manual link only (avoids redirect loops).
44 + */
45 + const USER_CONFLICT_AUTO_REDIRECT_DONE_OPTION = 'routeapp_user_conflict_auto_redirect_done';
42 46 const ACTIVATION_LINK = 'activation_link';
43 47 const SETUP_CHECK_WIDGET_INSTALLATION_DATE = 'route_setup_check_widget_installation_date';
44 48 const SETUP_CHECK_WIDGET_API_CALL = 'route_setup_check_widget_api_call';
45 49 const SETUP_CHECK_WIDGET_PHP = 'route_setup_check_widget_php';
@@ -45,11 +49,77 @@
45 49 const SETUP_CHECK_WIDGET_PHP = 'route_setup_check_widget_php';
46 50 const SETUP_CHECK_WIDGET_JS = 'route_setup_check_widget_js';
47 51 const ACTIVE = 'Active';
48 52
53 + /**
54 + * Stable per-site identifier for ownership / OTP callbacks (used in the ownership-resolution URL path).
55 + *
56 + * @var string
57 + */
58 + const STORE_HASH_OPTION = 'routeapp_store_hash';
59 +
60 + /**
61 + * Short alphanumeric id length (10–12) for /app/ownership-resolution/{hash}.
62 + */
63 + const STORE_HASH_LENGTH = 12;
64 +
65 + /**
66 + * Register rewrite, query vars, and front controller for /app/ownership-resolution/{storeHash}.
67 + */
68 + public static function init_ownership_routing() {
69 + add_action( 'init', array( __CLASS__, 'register_ownership_rewrite_rule' ), 5 );
70 + add_filter( 'query_vars', array( __CLASS__, 'ownership_query_vars' ) );
71 + add_action( 'template_redirect', array( __CLASS__, 'template_redirect_ownership_resolution' ), 1 );
72 + }
73 +
74 + /**
75 + * @param array $vars Query vars.
76 + * @return array
77 + */
78 + public static function ownership_query_vars( $vars ) {
79 + $vars[] = 'routeapp_ownership';
80 + $vars[] = 'routeapp_store_hash';
81 + return $vars;
82 + }
83 +
84 + /**
85 + * Pretty URL: {site}/app/ownership-resolution/{storeHash} on the store origin.
86 + */
87 + public static function register_ownership_rewrite_rule() {
88 + add_rewrite_rule(
89 + '^app/ownership-resolution/([a-zA-Z0-9]{10,12})/?$',
90 + 'index.php?routeapp_ownership=1&routeapp_store_hash=$matches[1]',
91 + 'top'
92 + );
93 + }
94 +
95 + /**
96 + * Handle GET /app/ownership-resolution/{storeHash}?token= / ?oneTimeToken=
97 + */
98 + public static function template_redirect_ownership_resolution() {
99 + if ( ! (int) get_query_var( 'routeapp_ownership' ) ) {
100 + return;
101 + }
102 + $path_hash = get_query_var( 'routeapp_store_hash' );
103 + $token = '';
104 + if ( isset( $_GET['oneTimeToken'] ) ) {
105 + $token = sanitize_text_field( wp_unslash( $_GET['oneTimeToken'] ) );
106 + }
107 + if ( $token === '' && isset( $_GET['token'] ) ) {
108 + $token = sanitize_text_field( wp_unslash( $_GET['token'] ) );
109 + }
110 + self::ownership_resolution_handle( $path_hash, $token );
111 + }
112 +
49 113 public static function init()
50 114 {
51 - if (!self::is_fresh_new_installation()) {
115 + self::ensure_store_hash();
116 +
117 + if ( ! self::get_route_public_instance() ) {
118 + return;
119 + }
120 +
121 + if ( ! self::is_fresh_new_installation() ) {
52 122 self::update_merchant_status();
53 123 return;
54 124 }
55 125
@@ -55,15 +125,86 @@
55 125
56 126 self::create_user();
57 127 }
58 128
129 + /**
130 + * True when activation should run create_user() / merchant onboarding against the Route API.
131 + *
132 + * Gated only on missing valid merchant credentials. Requiring an empty routeapp_route_registration_step
133 + * used to block re-onboarding after tokens were cleared while the step option stayed set.
134 + *
135 + * @return bool
136 + */
59 137 public static function is_fresh_new_installation()
60 138 {
61 - return !self::has_valid_merchant_tokens() &&
62 - !self::registration_step();
139 + return ! self::has_valid_merchant_tokens();
63 140 }
64 141
65 142 /**
143 + * Public/secret missing but we have prior onboarding state — user should use Dashboard login + OTP or POST user_login.
144 + *
145 + * @return bool
146 + */
147 + public static function should_prompt_merchant_token_recovery()
148 + {
149 + if ( self::has_merchant_tokens() ) {
150 + return false;
151 + }
152 + return ! empty( get_option( self::USER_TOKEN_OPTION ) )
153 + || ! empty( self::registration_step() )
154 + || ! empty( get_option( 'routeapp_merchant_id' ) );
155 + }
156 +
157 + /**
158 + * Create and persist a unique store hash for this WordPress installation (used in ownership-resolution URL).
159 + *
160 + * @return string
161 + */
162 + /**
163 + * Whether the stored hash matches the short format (10–12 alnum).
164 + *
165 + * @param string $hash Hash.
166 + * @return bool
167 + */
168 + private static function is_valid_store_hash_format( $hash ) {
169 + return is_string( $hash ) && (bool) preg_match( '/^[a-zA-Z0-9]{10,12}$/', $hash );
170 + }
171 +
172 + public static function ensure_store_hash() {
173 + $hash = get_option( self::STORE_HASH_OPTION );
174 + if ( self::is_valid_store_hash_format( $hash ) ) {
175 + return $hash;
176 + }
177 + if ( function_exists( 'wp_generate_password' ) ) {
178 + $hash = wp_generate_password( self::STORE_HASH_LENGTH, false, false );
179 + } else {
180 + $hash = substr( bin2hex( random_bytes( 8 ) ), 0, self::STORE_HASH_LENGTH );
181 + }
182 + update_option( self::STORE_HASH_OPTION, $hash, false );
183 + return $hash;
184 + }
185 +
186 + /**
187 + * Stored store hash (must exist before building platformUrl for Route Dashboard).
188 + *
189 + * @return string
190 + */
191 + public static function get_store_hash() {
192 + return (string) get_option( self::STORE_HASH_OPTION, '' );
193 + }
194 +
195 + /**
196 + * Callback URL for Route Dashboard platformUrl: {store-web-url}/app/ownership-resolution/{storeHash}
197 + * (avoids wp-json / rest_route encoding on the merchant origin).
198 + *
199 + * @return string
200 + */
201 + public static function get_ownership_resolution_platform_url() {
202 + $store_hash = self::ensure_store_hash();
203 + return home_url( '/app/ownership-resolution/' . $store_hash );
204 + }
205 +
206 + /**
66 207 * Prepare user data, call user create request
67 208 * and handle response
68 209 *
69 210 * @return bool
@@ -95,8 +236,9 @@
95 236 }
96 237
97 238 if (self::register_user($response)) {
98 239 self::set_registration_failed_as(self::REGISTRATION_FIXED);
240 + self::clear_user_conflict_auto_redirect_flag();
99 241 self::set_as_installed();
100 242
101 243 return true;
102 244 }
@@ -129,8 +271,9 @@
129 271 }
130 272
131 273 if (self::register_user($response, true)) {
132 274 self::set_registration_failed_as(self::REGISTRATION_STEP_USER_LOGIN_SUCCESS);
275 + self::clear_user_conflict_auto_redirect_flag();
133 276
134 277 self::set_as_installed();
135 278
136 279 return true;
@@ -200,17 +343,23 @@
200 343 }
201 344
202 345 public static function has_valid_merchant_tokens(){
203 346 $routeapp_public = self::get_route_public_instance();
347 + if ( ! $routeapp_public ) {
348 + return false;
349 + }
204 350 return self::has_merchant_tokens() &&
205 - !empty($routeapp_public->routeapp_api_client->get_merchant());
351 + ! empty( $routeapp_public->routeapp_api_client->get_merchant() );
206 352 }
207 353
208 354 public static function has_merchant_tokens(){
209 355 $routeapp_public = self::get_route_public_instance();
356 + if ( ! $routeapp_public ) {
357 + return false;
358 + }
210 359 return
211 - !empty($routeapp_public->routeapp_get_public_token()) &&
212 - !empty($routeapp_public->routeapp_get_secret_token());
360 + ! empty( $routeapp_public->routeapp_get_public_token() ) &&
361 + ! empty( $routeapp_public->routeapp_get_secret_token() );
213 362 }
214 363
215 364 public static function get_route_public_instance(){
216 365 global $routeapp_public;
@@ -459,16 +608,23 @@
459 608 return [$site];
460 609 }
461 610
462 611 /**
612 + * Retrieve the blog name for a given site.
613 + *
614 + * Falls back to the site domain if the blog name is empty
615 + * to prevent merchant creation failures on the Route API.
616 + *
463 617 * @param $site
464 - * @return mixed
618 + * @return string The blog name or domain as fallback.
465 619 */
466 620 private static function get_blog_name($site)
467 621 {
468 - if (is_multisite())
469 - return get_blog_option($site->blog_id, 'blogname');
470 - return get_option('blogname');
622 + $name = is_multisite()
623 + ? get_blog_option($site->blog_id, 'blogname')
624 + : get_option('blogname');
625 +
626 + return empty($name) ? $site->domain : $name;
471 627 }
472 628
473 629 /**
474 630 * @param $site
@@ -505,8 +661,15 @@
505 661 private static function set_registration_failed_as($step){
506 662 update_option(self::FAILED_REGISTRATION , $step);
507 663 }
508 664
665 + /**
666 + * Reset so the next duplicate-user / login-conflict flow can auto-redirect once again.
667 + */
668 + public static function clear_user_conflict_auto_redirect_flag() {
669 + delete_option( self::USER_CONFLICT_AUTO_REDIRECT_DONE_OPTION );
670 + }
671 +
509 672 private static function get_registration_failed_as(){
510 673 return get_option(self::FAILED_REGISTRATION);
511 674 }
512 675
@@ -638,6 +801,156 @@
638 801
639 802 if (property_exists($merchant, 'status')) {
640 803 $routeapp_public->routeapp_api_client->update_merchant_status('Active');
641 804 }
805 + }
806 +
807 + /**
808 + * Route Dashboard base URL (no path), respecting stage env like other helpers.
809 + *
810 + * @return string
811 + */
812 + public static function get_dashboard_root_url() {
813 + $custom_env = getenv( 'ROUTEAPP_ENVIRONMENT_ENDPOINT' );
814 + if ( is_null( $custom_env ) || ! $custom_env ) {
815 + $custom_env = isset( $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] ) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
816 + }
817 + return ( 'stage' === $custom_env ) ? 'https://dashboard-stage.route.com' : 'https://dashboard.route.com';
818 + }
819 +
820 + /**
821 + * Route Dashboard login URL with platformUrl pointing at this site's OTP callback.
822 + *
823 + * @return string
824 + */
825 + public static function get_dashboard_ownership_login_url() {
826 + $platform_url = self::get_ownership_resolution_platform_url();
827 + return self::get_dashboard_root_url() . '/login?platformUrl=' . rawurlencode( $platform_url );
828 + }
829 +
830 + /**
831 + * Dashboard login URL for Auth0 / post-activation flows (not legacy create-account).
832 + *
833 + * @return string
834 + */
835 + public static function get_dashboard_login_url() {
836 + return self::get_dashboard_root_url() . '/login';
837 + }
838 +
839 + /**
840 + * Whether the OTP merchant record matches this WordPress site's host.
841 + *
842 + * @param string $shop_domain Host from get_site_url().
843 + * @param array $merchant Result from verify_otp.
844 + * @return bool
845 + */
846 + public static function merchant_store_domain_matches_otp_merchant( $shop_domain, array $merchant ) {
847 + if ( empty( $merchant['store_domain'] ) || ! is_string( $merchant['store_domain'] ) ) {
848 + return false;
849 + }
850 + $m_domain = $merchant['store_domain'];
851 + if ( 0 === strpos( $shop_domain, 'www.' ) ) {
852 + return false !== strpos( $shop_domain, $m_domain ) || false !== strpos( $m_domain, $shop_domain );
853 + }
854 + return $m_domain === $shop_domain;
855 + }
856 +
857 + /**
858 + * Persist merchant credentials after successful OTP and mark onboarding complete.
859 + *
860 + * @param array $merchant Result from Routeapp_API_Client::verify_otp.
861 + */
862 + public static function apply_ownership_verified_merchant( array $merchant ) {
863 + $blog_id = is_multisite() ? get_current_blog_id() : null;
864 + self::set_public_key( $merchant['public_api_key'], $blog_id );
865 + self::set_secret_key( $merchant['prod_api_secret'], $blog_id );
866 + $client = Routeapp_API_Client::getInstance();
867 + $client->set_public_token( $merchant['public_api_key'] );
868 + $client->set_secret_token( $merchant['prod_api_secret'] );
869 + $client->set_merchant_id( $merchant['id'], $blog_id );
870 + self::set_registration_failed_as( self::REGISTRATION_FIXED );
871 + self::clear_user_conflict_auto_redirect_flag();
872 + self::set_as_installed();
873 +
874 + global $routeapp_public;
875 + if ( $routeapp_public && isset( $routeapp_public->routeapp_api_client ) ) {
876 + $routeapp_public->routeapp_api_client = new Routeapp_API_Client( $merchant['public_api_key'], $merchant['prod_api_secret'] );
877 + $routeapp_public->routeapp_api_client->set_merchant_id( $merchant['id'], $blog_id );
878 + $m = $routeapp_public->routeapp_api_client->get_merchant();
879 + if ( $m && is_object( $m ) && property_exists( $m, 'status' ) ) {
880 + $routeapp_public->routeapp_api_client->update_merchant_status( 'Active' );
881 + }
882 + }
883 + }
884 +
885 + /**
886 + * Shared handler: path {storeHash} must match DB; verifies OTP and links merchant.
887 + *
888 + * @param string $path_hash Segment from URL.
889 + * @param string $token OTP from ?token= or ?oneTimeToken=.
890 + */
891 + public static function ownership_resolution_handle( $path_hash, $token ) {
892 + $expected = self::get_store_hash();
893 + if ( $expected === '' ) {
894 + self::ensure_store_hash();
895 + $expected = self::get_store_hash();
896 + }
897 + if ( empty( $path_hash ) || $path_hash !== $expected ) {
898 + wp_safe_redirect(
899 + add_query_arg(
900 + 'route_ownership_error',
901 + rawurlencode( 'invalid_store' ),
902 + self::get_dashboard_login_url()
903 + )
904 + );
905 + exit;
906 + }
907 +
908 + if ( empty( $token ) ) {
909 + wp_safe_redirect(
910 + add_query_arg(
911 + 'route_ownership_error',
912 + rawurlencode( 'missing_token' ),
913 + self::get_dashboard_login_url()
914 + )
915 + );
916 + exit;
917 + }
918 +
919 + $merchant = Routeapp_API_Client::getInstance()->verify_otp( $token );
920 + if ( is_wp_error( $merchant ) ) {
921 + wp_safe_redirect(
922 + add_query_arg(
923 + 'route_ownership_error',
924 + rawurlencode( 'otp_verify_failed' ),
925 + self::get_dashboard_login_url()
926 + )
927 + );
928 + exit;
929 + }
930 +
931 + $shop_host = parse_url( get_site_url(), PHP_URL_HOST );
932 + if ( ! self::merchant_store_domain_matches_otp_merchant( $shop_host, $merchant ) ) {
933 + wp_safe_redirect(
934 + add_query_arg(
935 + 'route_ownership_error',
936 + rawurlencode( 'domain_mismatch' ),
937 + self::get_dashboard_login_url()
938 + )
939 + );
940 + exit;
941 + }
942 +
943 + self::apply_ownership_verified_merchant( $merchant );
944 +
945 + if ( ! class_exists( 'Routeapp_Webhooks', false ) ) {
946 + require_once dirname( dirname( __FILE__ ) ) . '/admin/class-routeapp-webhooks.php';
947 + }
948 + if ( class_exists( 'Routeapp_Webhooks' ) ) {
949 + $webhooks = new Routeapp_Webhooks();
950 + $webhooks->upsert_webhooks();
951 + }
952 +
953 + wp_safe_redirect( self::get_dashboard_root_url() );
954 + exit;
642 955 }
643 956 }