PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 1.7.0.9 All 211 releases
← All changes | includes/admin/class-charitable-admin.php +208 -85 1.8.11 → 1.8.13 View file →
@@ -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':
@@ -241,8 +180,185 @@
241 180 }
242 181 }
243 182
244 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;
358 + }
359 +
360 + /**
245 361 * Returns and/or create the single instance of this class.
246 362 *
247 363 * @since 1.2.0
248 364 *
@@ -425,8 +541,15 @@
425 541 $version
426 542 );
427 543
428 544 wp_enqueue_style( 'charitable-admin-2.0' );
545 +
546 + wp_enqueue_style(
547 + 'charitable-notifications-v2',
548 + $assets_dir . 'css/admin/notifications-v2' . $min . '.css',
549 + array(),
550 + $version
551 + );
429 552
430 553 wp_register_style(
431 554 'charitable-admin',
432 555 $assets_dir . 'css/admin/charitable-admin-legacy' . $min . '.css',