| @@ -3,17 +3,20 @@ | ||
| 3 | 3 | * Charitable Template Helpers. |
| 4 | 4 | * |
| 5 | 5 | * Functions used to assist with rendering templates. |
| 6 | 6 | * |
| 7 | - * @package Charitable/Functions/Templates | |
| 8 | - * @version 1.2.0 | |
| 9 | - * @author Eric Daams | |
| 10 | - * @copyright Copyright (c) 2018, Studio 164a | |
| 11 | - * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License | |
| 7 | + * @package Charitable/Functions/Templates | |
| 8 | + * @author David Bisset | |
| 9 | + * @copyright Copyright (c) 2023, WP Charitable LLC | |
| 10 | + * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License | |
| 11 | + * @since 1.0.0 | |
| 12 | + * @version 1.8.13 | |
| 12 | 13 | */ |
| 13 | 14 | |
| 14 | 15 | // Exit if accessed directly. |
| 15 | -if ( ! defined( 'ABSPATH' ) ) { exit; } | |
| 16 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 17 | + exit; | |
| 18 | +} | |
| 16 | 19 | |
| 17 | 20 | /** |
| 18 | 21 | * Displays a template. |
| 19 | 22 | * |
| @@ -64,9 +67,9 @@ | ||
| 64 | 67 | ob_start(); |
| 65 | 68 | |
| 66 | 69 | charitable_template( $template_name, $args, $classname ); |
| 67 | 70 | |
| 68 | - echo charitable_template_from_session_content( $template_key, $wrapper_args, ob_get_clean() ); | |
| 71 | + echo charitable_template_from_session_content( $template_key, $wrapper_args, ob_get_clean() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| 69 | 72 | } |
| 70 | 73 | |
| 71 | 74 | /** |
| 72 | 75 | * Returns a piece of content or an empty string, wrapped in the correct markup |
| @@ -88,9 +91,9 @@ | ||
| 88 | 91 | if ( charitable_get_session()->has_session_id() ) { |
| 89 | 92 | return $default; |
| 90 | 93 | } |
| 91 | 94 | |
| 92 | - $content = '<div class="charitable-session-content" data-template="' . esc_attr( $template_key ) . '" data-args="' . esc_attr( json_encode( $wrapper_args ) ) . '" style="display: none;">' . $default . '</div>'; | |
| 95 | + $content = '<div class="charitable-session-content" data-template="' . esc_attr( $template_key ) . '" data-args="' . esc_attr( wp_json_encode( $wrapper_args ) ) . '" style="display: none;">' . $default . '</div>'; | |
| 93 | 96 | $content .= '<noscript>' . $default . '</noscript>'; |
| 94 | 97 | |
| 95 | 98 | return $content; |
| 96 | 99 | } |
| @@ -99,13 +102,14 @@ | ||
| 99 | 102 | * Return the template path if the template exists. Otherwise, return default. |
| 100 | 103 | * |
| 101 | 104 | * @since 1.0.0 |
| 102 | 105 | * |
| 103 | - * @param string|string[] $template | |
| 106 | + * @param string|string[] $template The template to load. | |
| 107 | + * @param string $default The default to load if the template file doesn't exist. | |
| 104 | 108 | * @return string The template path if the template exists. Otherwise, return default. |
| 105 | 109 | */ |
| 106 | 110 | function charitable_get_template_path( $template, $default = '' ) { |
| 107 | - $t = new Charitable_Template( $template, false ); | |
| 111 | + $t = new Charitable_Template( $template, false ); | |
| 108 | 112 | $path = $t->locate_template(); |
| 109 | 113 | |
| 110 | 114 | if ( ! file_exists( $path ) ) { |
| 111 | 115 | $path = $default; |
| @@ -202,9 +206,10 @@ | ||
| 202 | 206 | * Filter the classes to be applied to the campaign loop. |
| 203 | 207 | * |
| 204 | 208 | * @since 1.5.7 |
| 205 | 209 | * |
| 206 | - * @param array $classes Array of classes. | |
| 210 | + * @param array $classes Array of classes. | |
| 211 | + * @param array $view_args View args. | |
| 207 | 212 | */ |
| 208 | 213 | $classes = apply_filters( 'charitable_campaign_loop_classes', $classes, $view_args ); |
| 209 | 214 | |
| 210 | 215 | return implode( ' ', $classes ); |
| @@ -230,14 +235,18 @@ | ||
| 230 | 235 | if ( ! array_key_exists( $attr, $field ) || array_key_exists( $attr, $field['attrs'] ) ) { |
| 231 | 236 | continue; |
| 232 | 237 | } |
| 233 | 238 | |
| 234 | - /* Skip required attribute unless it's true. */ | |
| 235 | - if ( 'required' == $attr && ! $field['required'] ) { | |
| 236 | - continue; | |
| 239 | + if ( 'required' == $attr ) { | |
| 240 | + /* Skip required attribute if it's not true. */ | |
| 241 | + if ( ! $field['required'] ) { | |
| 242 | + continue; | |
| 243 | + } | |
| 244 | + | |
| 245 | + $field['attrs'][ $attr ] = 'required'; | |
| 246 | + } else { | |
| 247 | + $field['attrs'][ $attr ] = $field[ $attr ]; | |
| 237 | 248 | } |
| 238 | - | |
| 239 | - $field['attrs'][ $attr ] = $field[ $attr ]; | |
| 240 | 249 | } |
| 241 | 250 | |
| 242 | 251 | $output = ''; |
| 243 | 252 | |
| @@ -256,26 +265,101 @@ | ||
| 256 | 265 | * Charitable's filters touching other the_content instances outside the main |
| 257 | 266 | * loop. |
| 258 | 267 | * |
| 259 | 268 | * @since 1.4.11 |
| 269 | + * @since 1.8.1 added 'charitable_is_main_loop' filter. | |
| 260 | 270 | * |
| 261 | - * @return boolean | |
| 271 | + * @return boolean | |
| 262 | 272 | */ |
| 263 | 273 | function charitable_is_main_loop() { |
| 264 | - return is_single() && in_the_loop() && is_main_query(); | |
| 274 | + $is_main_loop = is_single() && in_the_loop() && is_main_query(); | |
| 275 | + return apply_filters( 'charitable_is_main_loop', $is_main_loop ); | |
| 265 | 276 | } |
| 266 | 277 | |
| 267 | 278 | /** |
| 279 | + * Re-encodes a raw query string so that every key and value is percent-encoded. | |
| 280 | + * | |
| 281 | + * The query string arrives from the request untrusted and unencoded. Rebuilding it | |
| 282 | + * pair by pair guarantees the result contains no character that could break out of | |
| 283 | + * the HTML attribute, JavaScript string or URL context it is later printed into -- | |
| 284 | + * including HTML entities such as `'` that survive esc_url() and are decoded | |
| 285 | + * back into a quote by the browser's HTML parser. | |
| 286 | + * | |
| 287 | + * Query strings are application/x-www-form-urlencoded, so urldecode()/urlencode() | |
| 288 | + * is the correct pair here: it round-trips `+` as a space the way the request did. | |
| 289 | + * | |
| 290 | + * @since 1.8.13 | |
| 291 | + * | |
| 292 | + * @param string $query_string Raw query string, without the leading '?'. | |
| 293 | + * @return string Re-encoded query string, or an empty string if nothing usable remains. | |
| 294 | + */ | |
| 295 | +function charitable_sanitize_query_string( $query_string ) { | |
| 296 | + $pairs = array(); | |
| 297 | + | |
| 298 | + foreach ( explode( '&', (string) $query_string ) as $pair ) { | |
| 299 | + if ( '' === $pair ) { | |
| 300 | + continue; | |
| 301 | + } | |
| 302 | + | |
| 303 | + $parts = explode( '=', $pair, 2 ); | |
| 304 | + $key = urlencode( urldecode( $parts[0] ) ); | |
| 305 | + | |
| 306 | + if ( '' === $key ) { | |
| 307 | + continue; | |
| 308 | + } | |
| 309 | + | |
| 310 | + $pairs[] = isset( $parts[1] ) ? $key . '=' . urlencode( urldecode( $parts[1] ) ) : $key; | |
| 311 | + } | |
| 312 | + | |
| 313 | + return implode( '&', $pairs ); | |
| 314 | +} | |
| 315 | + | |
| 316 | +/** | |
| 268 | 317 | * Returns the current URL. |
| 269 | 318 | * |
| 270 | 319 | * @see https://gist.github.com/leereamsnyder/fac3b9ccb6b99ab14f36 |
| 271 | 320 | * |
| 272 | 321 | * @since 1.0.0 |
| 322 | + * @since 1.8.13 The path and query string are both percent-encoded instead of being | |
| 323 | + * reflected raw, and the trailing slash is applied to the path rather | |
| 324 | + * than to the end of the query string. | |
| 273 | 325 | * |
| 326 | + * @global WP $wp | |
| 274 | 327 | * @return string |
| 275 | 328 | */ |
| 276 | 329 | function charitable_get_current_url() { |
| 277 | - return home_url( add_query_arg( null, null ) ); | |
| 330 | + global $wp; | |
| 331 | + | |
| 332 | + $path = isset( $wp->request ) ? (string) $wp->request : ''; | |
| 333 | + | |
| 334 | + /* | |
| 335 | + * The requested path is reflected here too, and WordPress hands it over exactly as | |
| 336 | + * it arrived: any %XX escapes are already inert, but characters that arrived | |
| 337 | + * literally are not. Encoding '&' closes off every HTML entity form at once | |
| 338 | + * (', ', ', ", ...), which is what a URL escaper cannot do | |
| 339 | + * because those entities are made of characters it considers legal; the quotes and | |
| 340 | + * angle brackets cover the literal cases. Only these five are touched, so existing | |
| 341 | + * %XX escapes and ordinary slugs pass through unchanged and the result is idempotent. | |
| 342 | + */ | |
| 343 | + $path = str_replace( | |
| 344 | + array( '&', "'", '"', '<', '>' ), | |
| 345 | + array( '%26', '%27', '%22', '%3C', '%3E' ), | |
| 346 | + $path | |
| 347 | + ); | |
| 348 | + | |
| 349 | + $url = trailingslashit( home_url( $path ) ); | |
| 350 | + | |
| 351 | + if ( empty( $_SERVER['QUERY_STRING'] ) ) { | |
| 352 | + return $url; | |
| 353 | + } | |
| 354 | + | |
| 355 | + $query_string = charitable_sanitize_query_string( wp_unslash( $_SERVER['QUERY_STRING'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 356 | + | |
| 357 | + if ( '' === $query_string ) { | |
| 358 | + return $url; | |
| 359 | + } | |
| 360 | + | |
| 361 | + return $url . '?' . $query_string; | |
| 278 | 362 | } |
| 279 | 363 | |
| 280 | 364 | /** |
| 281 | 365 | * Returns the URL to which the user should be redirected after signing on or registering an account. |
| @@ -284,10 +368,10 @@ | ||
| 284 | 368 | * |
| 285 | 369 | * @return string |
| 286 | 370 | */ |
| 287 | 371 | function charitable_get_login_redirect_url() { |
| 288 | - if ( isset( $_REQUEST['redirect_to'] ) ) { | |
| 289 | - $redirect = $_REQUEST['redirect_to']; | |
| 372 | + if ( isset( $_REQUEST['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 373 | + $redirect = $_REQUEST['redirect_to']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 290 | 374 | } elseif ( charitable_get_permalink( 'profile_page' ) ) { |
| 291 | 375 | $redirect = charitable_get_permalink( 'profile_page' ); |
| 292 | 376 | } else { |
| 293 | 377 | $redirect = home_url(); |
| @@ -314,16 +398,84 @@ | ||
| 314 | 398 | /** |
| 315 | 399 | * Return the email verification link. |
| 316 | 400 | * |
| 317 | 401 | * @since 1.5.0 |
| 402 | + * @since 1.6.32 Added $force_send argument. | |
| 318 | 403 | * |
| 319 | 404 | * @param WP_User $user An instance of `WP_User`. |
| 320 | 405 | * @param string|false $redirect_url The URL to redirect to after verification. |
| 406 | + * @param boolean $force_send Whether the link should include an argument to force | |
| 407 | + * resending the email, even if it was sent recently. | |
| 321 | 408 | * @return string |
| 322 | 409 | */ |
| 323 | -function charitable_get_email_verification_link( WP_User $user, $redirect_url = false ) { | |
| 324 | - return add_query_arg( array( | |
| 325 | - 'charitable_action' => 'verify_email', | |
| 326 | - 'user' => $user->ID, | |
| 327 | - 'redirect_url' => $redirect_url, | |
| 328 | - ) ); | |
| 410 | +function charitable_get_email_verification_link( WP_User $user, $redirect_url = false, $force_send = false ) { | |
| 411 | + return add_query_arg( | |
| 412 | + array( | |
| 413 | + 'charitable_action' => 'verify_email', | |
| 414 | + 'user' => $user->ID, | |
| 415 | + 'redirect_url' => $redirect_url, | |
| 416 | + 'force_send' => (int) $force_send, | |
| 417 | + ) | |
| 418 | + ); | |
| 329 | 419 | } |
| 420 | + | |
| 421 | +/** | |
| 422 | + * Get the button classes for a Charitable button. | |
| 423 | + * | |
| 424 | + * @since 1.6.29 | |
| 425 | + * @version 1.8.3.6 Added $disabled parameter. | |
| 426 | + * | |
| 427 | + * @param string $button Which button we're showing. | |
| 428 | + * @return string | |
| 429 | + */ | |
| 430 | +function charitable_get_button_class( $button, $disabled = false ) { | |
| 431 | + $classes = [ 'button', 'charitable-button' ]; | |
| 432 | + | |
| 433 | + switch ( $button ) { | |
| 434 | + case 'donate': | |
| 435 | + $classes[] = 'donate-button'; | |
| 436 | + $classes[] = 'button-primary'; | |
| 437 | + break; | |
| 438 | + | |
| 439 | + default: | |
| 440 | + $classes[] = $button . '-button'; | |
| 441 | + } | |
| 442 | + | |
| 443 | + if ( $disabled ) { | |
| 444 | + $classes[] = 'charitable-disabled'; | |
| 445 | + } | |
| 446 | + | |
| 447 | + /** | |
| 448 | + * Filter the button classes. | |
| 449 | + * | |
| 450 | + * @since 1.6.29 | |
| 451 | + * | |
| 452 | + * @param array $classes The button classes. | |
| 453 | + * @param string $button The specific button we're showing. | |
| 454 | + */ | |
| 455 | + $classes = apply_filters( 'charitable_button_class', $classes, $button ); | |
| 456 | + | |
| 457 | + return implode( ' ', $classes ); | |
| 458 | +} | |
| 459 | + | |
| 460 | +/** | |
| 461 | + * Get a unique donation form ID. | |
| 462 | + * | |
| 463 | + * @since 1.6.57 | |
| 464 | + * | |
| 465 | + * @return string | |
| 466 | + */ | |
| 467 | +function charitable_get_donation_form_id() { | |
| 468 | + static $form_number; | |
| 469 | + | |
| 470 | + if ( ! isset( $form_number ) ) { | |
| 471 | + $form_id = 'charitable-donation-form'; | |
| 472 | + $form_number = 1; | |
| 473 | + } else { | |
| 474 | + $form_id = 'charitable-donation-form-' . $form_number; | |
| 475 | + $form_number += 1; | |
| 476 | + } | |
| 477 | + | |
| 478 | + // $form_number = ; | |
| 479 | + | |
| 480 | + return $form_id; | |
| 481 | +} | |