| @@ -113,102 +113,41 @@ | ||
| 113 | 113 | |
| 114 | 114 | break; |
| 115 | 115 | |
| 116 | 116 | case 'return': |
| 117 | - // if the user is returning from a redirect from a login or back link detect that and redirect them to the welcome/continue page. | |
| 118 | - // Only good way so far would be to detect POST data. | |
| 119 | - if ( empty( $_POST ) && is_admin() ) { // phpcs:ignore | |
| 120 | - wp_safe_redirect( admin_url( 'admin.php?page=charitable&wpchar_lite=lite&setup=welcome&resume=true&lostconnection=1' ) ); | |
| 117 | + // The wizard finishes by POSTing the user's choices from the | |
| 118 | + // hosted app straight into this admin, cross-site and | |
| 119 | + // top-level. Browsers drop that body when a SameSite=Lax auth | |
| 120 | + // cookie forces a trip through wp-login.php, or when a | |
| 121 | + // canonical / http-to-https / security-plugin redirect turns | |
| 122 | + // the POST into a GET. The user is logged in the whole time; | |
| 123 | + // the cookie simply did not ride along. | |
| 124 | + | |
| 125 | + // Applying onboarding choices writes plugin options and | |
| 126 | + // third-party activation flags, so it is an administrator | |
| 127 | + // action. This previously ran for any user who could reach | |
| 128 | + // wp-admin. | |
| 129 | + if ( ! charitable_current_user_can( 'administrator' ) ) { | |
| 130 | + wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) ); | |
| 121 | 131 | exit; |
| 122 | 132 | } |
| 123 | 133 | |
| 124 | - $plugins = isset( $_POST['plugins'] ) ? base64_decode( sanitize_text_field( wp_unslash( $_POST['plugins'] ) ) ) : ''; // phpcs:ignore | |
| 125 | - $features = isset( $_POST['features'] ) ? base64_decode( sanitize_text_field( wp_unslash( $_POST['features'] ) ) ) : ''; // phpcs:ignore | |
| 126 | - $meta = isset( $_POST['meta'] ) ? base64_decode( sanitize_text_field( wp_unslash( $_POST['meta'] ) ) ) : ''; // phpcs:ignore | |
| 127 | - $pm = isset( $_POST['pm'] ) ? base64_decode( sanitize_text_field( wp_unslash( $_POST['pm'] ) ) ) : ''; // phpcs:ignore | |
| 128 | - $license_key = isset( $_POST['license_key'] ) ? base64_decode( sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) ) : ''; // phpcs:ignore | |
| 134 | + $charitable_return_data = wp_unslash( $_POST ); // phpcs:ignore | |
| 129 | 135 | |
| 130 | - // process meta. | |
| 131 | - if ( ! empty( $meta ) ) { | |
| 132 | - $meta = str_replace( '\"', '"', $meta ); | |
| 133 | - if ( ! empty( $meta ) ) { | |
| 134 | - $meta = json_decode( $meta, true ); | |
| 135 | - } | |
| 136 | + if ( empty( $charitable_return_data ) && is_admin() ) { | |
| 137 | + // Body dropped in transit. Recover the choices with a | |
| 138 | + // server-to-server read: no browser, no cookies, no | |
| 139 | + // redirects, so nothing that drops the POST applies. | |
| 140 | + $charitable_return_data = $this->pull_onboarding_return_data(); | |
| 136 | 141 | } |
| 137 | - // process payment methods. | |
| 138 | - if ( ! empty( $plugins ) ) { | |
| 139 | - $plugins = json_decode( $plugins ); | |
| 140 | - $plugins = str_replace( array( '"', '[', ']' ), '', $plugins ); | |
| 141 | - if ( ! empty( $plugins ) ) { | |
| 142 | - $plugins = explode( ',', $plugins ); | |
| 143 | - } | |
| 144 | - } | |
| 145 | 142 | |
| 146 | - // process plugins. | |
| 147 | - if ( ! empty( $pm ) ) { | |
| 148 | - $pm = json_decode( $pm ); | |
| 149 | - if ( $pm ) { | |
| 150 | - $pm = str_replace( array( '"', '[', ']' ), '', $pm ); | |
| 151 | - if ( ! empty( $plugins ) ) { | |
| 152 | - $pm = explode( ',', $pm ); | |
| 153 | - } | |
| 154 | - } | |
| 143 | + if ( empty( $charitable_return_data ) ) { | |
| 144 | + wp_safe_redirect( admin_url( 'admin.php?page=charitable&wpchar_lite=lite&setup=welcome&resume=true&lostconnection=1' ) ); | |
| 145 | + exit; | |
| 155 | 146 | } |
| 156 | 147 | |
| 157 | - // process features. | |
| 158 | - if ( ! empty( $features ) ) { | |
| 159 | - $features = json_decode( $features ); | |
| 160 | - $features = str_replace( array( '"', '[', ']' ), '', $features ); | |
| 161 | - if ( ! empty( $features ) ) { | |
| 162 | - $features = explode( ',', $features ); | |
| 163 | - } | |
| 164 | - } | |
| 148 | + $this->process_onboarding_return_data( $charitable_return_data ); | |
| 165 | 149 | |
| 166 | - // clean license (remove quotes). | |
| 167 | - $license_key = str_replace( '"', '', $license_key ); | |
| 168 | - | |
| 169 | - // process campaign. | |
| 170 | - $campaign_template = isset( $_POST['template'] ) && ! empty( $_POST['template'] ) ? sanitize_text_field( wp_unslash( $_POST['template'] ) ) : ''; // phpcs:ignore | |
| 171 | - $campaign_title = isset( $_POST['campaign_title'] ) && ! empty( $_POST['campaign_title'] ) ? sanitize_text_field( wp_unslash( $_POST['campaign_title'] ) ) : ''; // phpcs:ignore | |
| 172 | - $campaign_description = isset( $_POST['campaign_description'] ) && ! empty( $_POST['campaign_description'] ) ? sanitize_text_field( wp_unslash( $_POST['campaign_description'] ) ) : ''; // phpcs:ignore | |
| 173 | - $campaign_goal = isset( $_POST['campaign_goal'] ) && ! empty( $_POST['campaign_goal'] ) ? sanitize_text_field( wp_unslash( $_POST['campaign_goal'] ) ) : ''; // phpcs:ignore | |
| 174 | - $campaign_end_date = isset( $_POST['campaign_end_date'] ) && ! empty( $_POST['campaign_end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['campaign_end_date'] ) ) : ''; // phpcs:ignore | |
| 175 | - $campaign = array( | |
| 176 | - 'template' => $campaign_template, | |
| 177 | - 'title' => $campaign_title, | |
| 178 | - 'description' => $campaign_description, | |
| 179 | - 'goal' => $campaign_goal, | |
| 180 | - 'end_date' => $campaign_end_date, | |
| 181 | - ); | |
| 182 | - | |
| 183 | - // store server side setups. | |
| 184 | - $serverside = new Charitable_Setup(); | |
| 185 | - | |
| 186 | - $serverside->store_meta( $meta ); | |
| 187 | - $serverside->store_plugins( $plugins ); | |
| 188 | - $serverside->store_features( $features ); | |
| 189 | - $serverside->store_payment_methods( $pm ); | |
| 190 | - $serverside->store_campaign( $campaign ); | |
| 191 | - if ( $license_key ) { | |
| 192 | - $serverside->store_license_key( $license_key ); | |
| 193 | - } | |
| 194 | - | |
| 195 | - // If this is a select few addons that we need to disable their auto-activation, do so. | |
| 196 | - if ( ! empty( $plugins ) ) { | |
| 197 | - foreach ( $plugins as $plugin ) { | |
| 198 | - if ( $plugin === 'all-in-one-seo-pack' ) { | |
| 199 | - update_option( 'aioseo_activation_redirect', true ); | |
| 200 | - } elseif ( $plugin === 'wp-mail-smtp' ) { | |
| 201 | - update_option( 'wp_mail_smtp_activation_prevent_redirect', true ); | |
| 202 | - } | |
| 203 | - } | |
| 204 | - } | |
| 205 | - | |
| 206 | - // add a transient to indicate that the server side onboarding has moved to the plugin. | |
| 207 | - set_transient( 'charitable_ss_onboarding', 1, 0 ); | |
| 208 | - | |
| 209 | - delete_option( 'charitable_started_onboarding' ); | |
| 210 | - | |
| 211 | 150 | wp_safe_redirect( admin_url( 'admin.php?page=charitable-setup&setup=1' ) ); |
| 212 | 151 | exit; |
| 213 | 152 | |
| 214 | 153 | case 'cancelled': |
| @@ -238,8 +177,185 @@ | ||
| 238 | 177 | exit; |
| 239 | 178 | |
| 240 | 179 | } |
| 241 | 180 | } |
| 181 | + } | |
| 182 | + | |
| 183 | + /** | |
| 184 | + * Apply a set of onboarding choices handed back by the setup wizard. | |
| 185 | + * | |
| 186 | + * Extracted verbatim from the `setup=return` branch so the same logic | |
| 187 | + * serves both routes into it: the cross-site POST the wizard sends, and | |
| 188 | + * the server-to-server pull used when that POST body is dropped. Keeping | |
| 189 | + * one implementation is the point - a second decoder would drift. | |
| 190 | + * | |
| 191 | + * Expects the same keys and encodings as the POST: `meta`, `pm`, | |
| 192 | + * `features`, `plugins` and `license_key` base64-encoded, and `template` | |
| 193 | + * plus the `campaign_*` fields plain. Every field is optional. | |
| 194 | + * | |
| 195 | + * @since 1.8.12.4 | |
| 196 | + * | |
| 197 | + * @param array $data Onboarding fields, already unslashed. | |
| 198 | + * @return void | |
| 199 | + */ | |
| 200 | + private function process_onboarding_return_data( $data ) { | |
| 201 | + | |
| 202 | + if ( ! is_array( $data ) ) { | |
| 203 | + return; | |
| 204 | + } | |
| 205 | + | |
| 206 | + $charitable_field = function ( $key ) use ( $data ) { | |
| 207 | + return isset( $data[ $key ] ) && ! is_array( $data[ $key ] ) | |
| 208 | + ? sanitize_text_field( $data[ $key ] ) | |
| 209 | + : ''; | |
| 210 | + }; | |
| 211 | + | |
| 212 | + $plugins = '' !== $charitable_field( 'plugins' ) ? base64_decode( $charitable_field( 'plugins' ) ) : ''; | |
| 213 | + $features = '' !== $charitable_field( 'features' ) ? base64_decode( $charitable_field( 'features' ) ) : ''; | |
| 214 | + $meta = '' !== $charitable_field( 'meta' ) ? base64_decode( $charitable_field( 'meta' ) ) : ''; | |
| 215 | + $pm = '' !== $charitable_field( 'pm' ) ? base64_decode( $charitable_field( 'pm' ) ) : ''; | |
| 216 | + $license_key = '' !== $charitable_field( 'license_key' ) ? base64_decode( $charitable_field( 'license_key' ) ) : ''; | |
| 217 | + | |
| 218 | + // process meta. | |
| 219 | + if ( ! empty( $meta ) ) { | |
| 220 | + $meta = str_replace( '\\"', '"', $meta ); | |
| 221 | + if ( ! empty( $meta ) ) { | |
| 222 | + $meta = json_decode( $meta, true ); | |
| 223 | + } | |
| 224 | + } | |
| 225 | + // process plugins. | |
| 226 | + if ( ! empty( $plugins ) ) { | |
| 227 | + $plugins = json_decode( $plugins ); | |
| 228 | + $plugins = str_replace( array( '"', '[', ']' ), '', $plugins ); | |
| 229 | + if ( ! empty( $plugins ) ) { | |
| 230 | + $plugins = explode( ',', $plugins ); | |
| 231 | + } | |
| 232 | + } | |
| 233 | + | |
| 234 | + // process payment methods. | |
| 235 | + // NOTE: the inner guard tests $plugins, not $pm. That is how this has | |
| 236 | + // always behaved, so it is preserved here rather than quietly changed | |
| 237 | + // during a refactor whose whole value is being behaviour-identical. | |
| 238 | + // Worth fixing on its own, separately. | |
| 239 | + if ( ! empty( $pm ) ) { | |
| 240 | + $pm = json_decode( $pm ); | |
| 241 | + if ( $pm ) { | |
| 242 | + $pm = str_replace( array( '"', '[', ']' ), '', $pm ); | |
| 243 | + if ( ! empty( $plugins ) ) { | |
| 244 | + $pm = explode( ',', $pm ); | |
| 245 | + } | |
| 246 | + } | |
| 247 | + } | |
| 248 | + | |
| 249 | + // process features. | |
| 250 | + if ( ! empty( $features ) ) { | |
| 251 | + $features = json_decode( $features ); | |
| 252 | + $features = str_replace( array( '"', '[', ']' ), '', $features ); | |
| 253 | + if ( ! empty( $features ) ) { | |
| 254 | + $features = explode( ',', $features ); | |
| 255 | + } | |
| 256 | + } | |
| 257 | + | |
| 258 | + // clean license (remove quotes). | |
| 259 | + $license_key = str_replace( '"', '', $license_key ); | |
| 260 | + | |
| 261 | + // process campaign. | |
| 262 | + $campaign = array( | |
| 263 | + 'template' => $charitable_field( 'template' ), | |
| 264 | + 'title' => $charitable_field( 'campaign_title' ), | |
| 265 | + 'description' => $charitable_field( 'campaign_description' ), | |
| 266 | + 'goal' => $charitable_field( 'campaign_goal' ), | |
| 267 | + 'end_date' => $charitable_field( 'campaign_end_date' ), | |
| 268 | + ); | |
| 269 | + | |
| 270 | + // store server side setups. | |
| 271 | + $serverside = new Charitable_Setup(); | |
| 272 | + | |
| 273 | + $serverside->store_meta( $meta ); | |
| 274 | + $serverside->store_plugins( $plugins ); | |
| 275 | + $serverside->store_features( $features ); | |
| 276 | + $serverside->store_payment_methods( $pm ); | |
| 277 | + $serverside->store_campaign( $campaign ); | |
| 278 | + // Guarded, so a payload without a licence key never overwrites one | |
| 279 | + // already stored. The pull payload deliberately never carries one. | |
| 280 | + if ( $license_key ) { | |
| 281 | + $serverside->store_license_key( $license_key ); | |
| 282 | + } | |
| 283 | + | |
| 284 | + // If this is a select few addons that we need to disable their auto-activation, do so. | |
| 285 | + if ( ! empty( $plugins ) && is_array( $plugins ) ) { | |
| 286 | + foreach ( $plugins as $plugin ) { | |
| 287 | + if ( $plugin === 'all-in-one-seo-pack' ) { | |
| 288 | + update_option( 'aioseo_activation_redirect', true ); | |
| 289 | + } elseif ( $plugin === 'wp-mail-smtp' ) { | |
| 290 | + update_option( 'wp_mail_smtp_activation_prevent_redirect', true ); | |
| 291 | + } | |
| 292 | + } | |
| 293 | + } | |
| 294 | + | |
| 295 | + // add a transient to indicate that the server side onboarding has moved to the plugin. | |
| 296 | + set_transient( 'charitable_ss_onboarding', 1, 0 ); | |
| 297 | + | |
| 298 | + delete_option( 'charitable_started_onboarding' ); | |
| 299 | + } | |
| 300 | + | |
| 301 | + /** | |
| 302 | + * Read the onboarding choices back from the wizard, server to server. | |
| 303 | + * | |
| 304 | + * Used only when the wizard's cross-site POST arrived with no body. A | |
| 305 | + * plain server-side GET has no browser, no cookies and no redirects, so | |
| 306 | + * none of the mechanisms that drop that POST can affect it. | |
| 307 | + * | |
| 308 | + * Returns an empty array on any doubt at all - disabled, missing | |
| 309 | + * helpers, transport error, non-200, unparseable body, or a payload that | |
| 310 | + * does not say `status: ok`. The caller then falls back to exactly the | |
| 311 | + * behaviour that shipped before this existed. | |
| 312 | + * | |
| 313 | + * @since 1.8.12.4 | |
| 314 | + * | |
| 315 | + * @return array Onboarding fields, or an empty array. | |
| 316 | + */ | |
| 317 | + private function pull_onboarding_return_data() { | |
| 318 | + | |
| 319 | + // Field kill switch: both of these turn the fallback off without | |
| 320 | + // needing a release, reverting to the previous behaviour exactly. | |
| 321 | + if ( ! apply_filters( 'charitable_onboarding_pull_enabled', true ) ) { | |
| 322 | + return array(); | |
| 323 | + } | |
| 324 | + | |
| 325 | + if ( defined( 'CHARITABLE_ONBOARDING_DISABLE_PULL' ) && CHARITABLE_ONBOARDING_DISABLE_PULL ) { | |
| 326 | + return array(); | |
| 327 | + } | |
| 328 | + | |
| 329 | + if ( ! function_exists( 'charitable_onboarding_app_url' ) || ! function_exists( 'charitable_get_site_token' ) ) { | |
| 330 | + return array(); | |
| 331 | + } | |
| 332 | + | |
| 333 | + $site_token = charitable_get_site_token(); | |
| 334 | + | |
| 335 | + if ( empty( $site_token ) ) { | |
| 336 | + return array(); | |
| 337 | + } | |
| 338 | + | |
| 339 | + $response = wp_remote_get( | |
| 340 | + charitable_onboarding_app_url() . '/setup-wizard-charitable_lite/pull/' . rawurlencode( $site_token ), | |
| 341 | + array( | |
| 342 | + 'timeout' => 10, | |
| 343 | + 'sslverify' => true, | |
| 344 | + ) | |
| 345 | + ); | |
| 346 | + | |
| 347 | + if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { | |
| 348 | + return array(); | |
| 349 | + } | |
| 350 | + | |
| 351 | + $body = json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 352 | + | |
| 353 | + if ( ! is_array( $body ) || empty( $body['status'] ) || 'ok' !== $body['status'] ) { | |
| 354 | + return array(); | |
| 355 | + } | |
| 356 | + | |
| 357 | + return $body; | |
| 242 | 358 | } |
| 243 | 359 | |
| 244 | 360 | /** |
| 245 | 361 | * Returns and/or create the single instance of this class. |