| @@ -42,8 +42,18 @@ | ||
| 42 | 42 | */ |
| 43 | 43 | private static $tl_search_items; |
| 44 | 44 | |
| 45 | 45 | /** |
| 46 | + * Customer id of the visitor this request actually belongs to, captured | |
| 47 | + * on 'init' before anything can reassign WPSC_Current_User::$current_user | |
| 48 | + * (change_current_user() is called from a number of places, one of them | |
| 49 | + * driven by request input). Used to record and check attachment ownership. | |
| 50 | + * | |
| 51 | + * @var int | |
| 52 | + */ | |
| 53 | + private static $session_customer_id = 0; | |
| 54 | + | |
| 55 | + /** | |
| 46 | 56 | * Initialize this class |
| 47 | 57 | * |
| 48 | 58 | * @return void |
| 49 | 59 | */ |
| @@ -51,8 +61,11 @@ | ||
| 51 | 61 | |
| 52 | 62 | // Apply schema for this model. |
| 53 | 63 | add_action( 'init', array( __CLASS__, 'apply_schema' ), 2 ); |
| 54 | 64 | |
| 65 | + // Capture whose session this request is, before anything reassigns it. | |
| 66 | + add_action( 'init', array( __CLASS__, 'snapshot_session_customer' ), 11 ); | |
| 67 | + | |
| 55 | 68 | // Get object of this class. |
| 56 | 69 | add_filter( 'wpsc_load_ref_classes', array( __CLASS__, 'load_ref_class' ) ); |
| 57 | 70 | |
| 58 | 71 | // File upload. |
| @@ -185,8 +198,13 @@ | ||
| 185 | 198 | 'has_ref' => false, |
| 186 | 199 | 'ref_class' => '', |
| 187 | 200 | 'has_multiple_val' => false, |
| 188 | 201 | ), |
| 202 | + 'uploaded_by' => array( | |
| 203 | + 'has_ref' => false, | |
| 204 | + 'ref_class' => '', | |
| 205 | + 'has_multiple_val' => false, | |
| 206 | + ), | |
| 189 | 207 | ); |
| 190 | 208 | self::$schema = apply_filters( 'wpsc_attachment_schema', $schema ); |
| 191 | 209 | |
| 192 | 210 | // Prevent modify. |
| @@ -543,9 +561,9 @@ | ||
| 543 | 561 | */ |
| 544 | 562 | public static function file_upload() { |
| 545 | 563 | |
| 546 | 564 | if ( check_ajax_referer( 'wpsc_file_upload', '_ajax_nonce', false ) != 1 ) { |
| 547 | - wp_send_json_error( 'Unauthorised request!', 401 ); | |
| 565 | + wp_send_json_error( 'Unauthorized request!', 401 ); | |
| 548 | 566 | } |
| 549 | 567 | |
| 550 | 568 | if ( ! WPSC_Text_Editor::is_allow_attachments() ) { |
| 551 | 569 | wp_send_json_error( new WP_Error( '001', 'Unauthorized' ), 401 ); |
| @@ -551,9 +569,9 @@ | ||
| 551 | 569 | wp_send_json_error( new WP_Error( '001', 'Unauthorized' ), 401 ); |
| 552 | 570 | } |
| 553 | 571 | |
| 554 | 572 | $recaptcha = get_option( 'wpsc-recaptcha-settings' ); |
| 555 | - if ( $recaptcha['allow-recaptcha'] === 1 && $recaptcha['recaptcha-version'] == 3 && $recaptcha['recaptcha-site-key'] && $recaptcha['recaptcha-secret-key'] ) { | |
| 573 | + if ( $recaptcha['captcha-provider'] === 'google-recaptcha' && $recaptcha['recaptcha-version'] == 3 && $recaptcha['recaptcha-site-key'] && $recaptcha['recaptcha-secret-key'] ) { | |
| 556 | 574 | WPSC_MS_Recaptcha::validate( 'file_upload' ); |
| 557 | 575 | } |
| 558 | 576 | |
| 559 | 577 | $file = isset( $_FILES['wpscFileAttachment'] ) ? $_FILES['wpscFileAttachment'] : false; // phpcs:ignore |
| @@ -626,10 +644,19 @@ | ||
| 626 | 644 | if ( ! $attachment->id ) { |
| 627 | 645 | wp_send_json_error( 'Something went wrong!', 500 ); |
| 628 | 646 | } |
| 629 | 647 | |
| 630 | - wp_send_json( array( 'id' => $attachment->id ) ); | |
| 648 | + // Record who uploaded it, and hand them a per-attachment token, | |
| 649 | + // so that only they can later bind it to a ticket. | |
| 650 | + self::set_uploader( $attachment ); | |
| 631 | 651 | |
| 652 | + wp_send_json( | |
| 653 | + array( | |
| 654 | + 'id' => $attachment->id, | |
| 655 | + 'token' => wp_create_nonce( self::get_img_editor_tmp_nonce_action( $attachment->id ) ), | |
| 656 | + ) | |
| 657 | + ); | |
| 658 | + | |
| 632 | 659 | } else { |
| 633 | 660 | |
| 634 | 661 | $error_message = isset( $uploaded_file['error'] ) ? $uploaded_file['error'] : 'Something went wrong!'; |
| 635 | 662 | wp_send_json_error( $error_message, 500 ); |
| @@ -650,9 +677,17 @@ | ||
| 650 | 677 | } else { |
| 651 | 678 | $attachments = self::find( |
| 652 | 679 | array( |
| 653 | 680 | 'search' => $search, |
| 654 | - 'items_per_page' => 0, | |
| 681 | + 'items_per_page' => 500, | |
| 682 | + 'meta_query' => array( | |
| 683 | + 'relation' => 'AND', | |
| 684 | + array( | |
| 685 | + 'slug' => 'is_active', | |
| 686 | + 'compare' => '=', | |
| 687 | + 'val' => '1', | |
| 688 | + ), | |
| 689 | + ), | |
| 655 | 690 | ) |
| 656 | 691 | )['results']; |
| 657 | 692 | if ( $attachments ) { |
| 658 | 693 | foreach ( $attachments as $attachment ) { |
| @@ -672,9 +707,9 @@ | ||
| 672 | 707 | */ |
| 673 | 708 | public static function add_custom_image_tinymce() { |
| 674 | 709 | |
| 675 | 710 | if ( check_ajax_referer( 'wpsc_add_custom_image_tinymce', '_ajax_nonce', false ) != 1 ) { |
| 676 | - wp_send_json_error( 'Unauthorised request!', 401 ); | |
| 711 | + wp_send_json_error( 'Unauthorized request!', 401 ); | |
| 677 | 712 | } |
| 678 | 713 | |
| 679 | 714 | $title = esc_attr__( 'Insert/edit image', 'supportcandy' ); |
| 680 | 715 | |
| @@ -829,9 +864,9 @@ | ||
| 829 | 864 | */ |
| 830 | 865 | public static function edit_custom_image_tinymce() { |
| 831 | 866 | |
| 832 | 867 | if ( check_ajax_referer( 'wpsc_edit_custom_image_tinymce', '_ajax_nonce', false ) != 1 ) { |
| 833 | - wp_send_json_error( 'Unauthorised request!', 401 ); | |
| 868 | + wp_send_json_error( 'Unauthorized request!', 401 ); | |
| 834 | 869 | } |
| 835 | 870 | |
| 836 | 871 | $title = esc_attr__( 'Edit image', 'supportcandy' ); |
| 837 | 872 | |
| @@ -1047,9 +1082,8 @@ | ||
| 1047 | 1082 | if ( ! $attachment->id ) { |
| 1048 | 1083 | return; |
| 1049 | 1084 | } |
| 1050 | 1085 | $auth_code = isset($_REQUEST['auth_code']) ? sanitize_text_field( $_REQUEST['auth_code'] ) : ''; // phpcs:ignore |
| 1051 | - $advanced = get_option( 'wpsc-ms-advanced-settings' ); | |
| 1052 | 1086 | switch ( $attachment->source ) { |
| 1053 | 1087 | |
| 1054 | 1088 | case 'cf': |
| 1055 | 1089 | $cf = new WPSC_Custom_Field( $attachment->source_id ); |
| @@ -1068,9 +1102,9 @@ | ||
| 1068 | 1102 | } |
| 1069 | 1103 | |
| 1070 | 1104 | if ( ! $has_auth ) { |
| 1071 | 1105 | $auth_code = isset( $_REQUEST['auth_code'] ) ? sanitize_text_field( $_REQUEST['auth_code'] ) : ''; // phpcs:ignore |
| 1072 | - if ( ! $auth_code || $auth_code != $ticket->auth_code ) { | |
| 1106 | + if ( ! $auth_code || ! $ticket->auth_code || ! hash_equals( (string) $ticket->auth_code, $auth_code ) ) { | |
| 1073 | 1107 | wp_send_json_error( 'Unauthorized!', 401 ); |
| 1074 | 1108 | } |
| 1075 | 1109 | } |
| 1076 | 1110 | |
| @@ -1075,9 +1109,9 @@ | ||
| 1075 | 1109 | } |
| 1076 | 1110 | |
| 1077 | 1111 | if ( is_a( $ticket, 'WPSC_Ticket' ) || is_a( $ticket, 'WPSC_Archive_Ticket' ) ) { |
| 1078 | 1112 | // Determine correct class based on ticket type. |
| 1079 | - $ticket_class = is_a( $ticket, 'WPSC_Ticket' ) && ! empty( $ticket->id ) | |
| 1113 | + $ticket_class = is_a( $ticket, 'WPSC_Ticket' ) | |
| 1080 | 1114 | ? 'WPSC_Individual_Ticket' |
| 1081 | 1115 | : 'WPSC_Individual_Archive_Ticket'; |
| 1082 | 1116 | |
| 1083 | 1117 | // Assign current ticket to class static property. |
| @@ -1084,9 +1118,9 @@ | ||
| 1084 | 1118 | $ticket_class::$ticket = $ticket; |
| 1085 | 1119 | $has_access = ( |
| 1086 | 1120 | ( $current_user->is_agent && $ticket_class::has_ticket_cap( 'view' ) ) || |
| 1087 | 1121 | $ticket_class::is_customer() || |
| 1088 | - ( ! $advanced['ticket-url-auth'] && $ticket->auth_code === $auth_code ) | |
| 1122 | + ( $ticket->auth_code && hash_equals( (string) $ticket->auth_code, $auth_code ) ) | |
| 1089 | 1123 | ); |
| 1090 | 1124 | |
| 1091 | 1125 | if ( ! $has_access ) { |
| 1092 | 1126 | wp_send_json_error( __( 'Unauthorized!', 'supportcandy' ), 401 ); |
| @@ -1123,9 +1157,9 @@ | ||
| 1123 | 1157 | } |
| 1124 | 1158 | |
| 1125 | 1159 | if ( ! $has_auth ) { |
| 1126 | 1160 | $auth_code = isset( $_REQUEST['auth_code'] ) ? sanitize_text_field( $_REQUEST['auth_code'] ) : ''; // phpcs:ignore |
| 1127 | - if ( ! $auth_code || $auth_code != $ticket->auth_code ) { | |
| 1161 | + if ( ! $auth_code || ! $ticket->auth_code || ! hash_equals( (string) $ticket->auth_code, $auth_code ) ) { | |
| 1128 | 1162 | wp_send_json_error( 'Unauthorized!', 401 ); |
| 1129 | 1163 | } |
| 1130 | 1164 | } |
| 1131 | 1165 | |
| @@ -1130,9 +1164,9 @@ | ||
| 1130 | 1164 | } |
| 1131 | 1165 | |
| 1132 | 1166 | if ( is_a( $ticket, 'WPSC_Ticket' ) || is_a( $ticket, 'WPSC_Archive_Ticket' ) ) { |
| 1133 | 1167 | // Determine correct class based on ticket type. |
| 1134 | - $ticket_class = is_a( $ticket, 'WPSC_Ticket' ) && ! empty( $ticket->id ) | |
| 1168 | + $ticket_class = is_a( $ticket, 'WPSC_Ticket' ) | |
| 1135 | 1169 | ? 'WPSC_Individual_Ticket' |
| 1136 | 1170 | : 'WPSC_Individual_Archive_Ticket'; |
| 1137 | 1171 | |
| 1138 | 1172 | // Assign current ticket to class static property. |
| @@ -1139,9 +1173,9 @@ | ||
| 1139 | 1173 | $ticket_class::$ticket = $ticket; |
| 1140 | 1174 | $has_access = ( |
| 1141 | 1175 | ( $current_user->is_agent && $ticket_class::has_ticket_cap( 'view' ) ) || |
| 1142 | 1176 | $ticket_class::is_customer() || |
| 1143 | - ( ! $advanced['ticket-url-auth'] && $ticket->auth_code === $auth_code ) | |
| 1177 | + ( $ticket->auth_code && hash_equals( (string) $ticket->auth_code, $auth_code ) ) | |
| 1144 | 1178 | ); |
| 1145 | 1179 | |
| 1146 | 1180 | if ( ! $has_access ) { |
| 1147 | 1181 | wp_send_json_error( __( 'Unauthorized!', 'supportcandy' ), 401 ); |
| @@ -1160,9 +1194,9 @@ | ||
| 1160 | 1194 | } |
| 1161 | 1195 | |
| 1162 | 1196 | if ( ! $has_auth ) { |
| 1163 | 1197 | $auth_code = isset( $_REQUEST['auth_code'] ) ? sanitize_text_field( $_REQUEST['auth_code'] ) : ''; // phpcs:ignore |
| 1164 | - if ( ! $auth_code || $auth_code != $ticket->auth_code ) { | |
| 1198 | + if ( ! $auth_code || ! $ticket->auth_code || ! hash_equals( (string) $ticket->auth_code, $auth_code ) ) { | |
| 1165 | 1199 | wp_send_json_error( 'Unauthorized!', 401 ); |
| 1166 | 1200 | } |
| 1167 | 1201 | } |
| 1168 | 1202 | |
| @@ -1168,9 +1202,9 @@ | ||
| 1168 | 1202 | |
| 1169 | 1203 | if ( is_a( $ticket, 'WPSC_Ticket' ) || is_a( $ticket, 'WPSC_Archive_Ticket' ) ) { |
| 1170 | 1204 | |
| 1171 | 1205 | // Determine correct handler class dynamically. |
| 1172 | - $ticket_class = is_a( $ticket, 'WPSC_Ticket' ) && ! empty( $ticket->id ) | |
| 1206 | + $ticket_class = is_a( $ticket, 'WPSC_Ticket' ) | |
| 1173 | 1207 | ? 'WPSC_Individual_Ticket' |
| 1174 | 1208 | : 'WPSC_Individual_Archive_Ticket'; |
| 1175 | 1209 | |
| 1176 | 1210 | // Assign the ticket. |
| @@ -1194,9 +1228,9 @@ | ||
| 1194 | 1228 | |
| 1195 | 1229 | if ( is_a( $ticket, 'WPSC_Ticket' ) || is_a( $ticket, 'WPSC_Archive_Ticket' ) ) { |
| 1196 | 1230 | |
| 1197 | 1231 | // Determine class dynamically. |
| 1198 | - $ticket_class = ( is_a( $ticket, 'WPSC_Ticket' ) && ! empty( $ticket->id ) ) | |
| 1232 | + $ticket_class = is_a( $ticket, 'WPSC_Ticket' ) | |
| 1199 | 1233 | ? 'WPSC_Individual_Ticket' |
| 1200 | 1234 | : 'WPSC_Individual_Archive_Ticket'; |
| 1201 | 1235 | |
| 1202 | 1236 | $ticket_class::$ticket = $ticket; |
| @@ -1203,9 +1237,9 @@ | ||
| 1203 | 1237 | if ( |
| 1204 | 1238 | ! ( |
| 1205 | 1239 | ( $current_user->is_agent && $ticket_class::has_ticket_cap( 'view' ) ) || |
| 1206 | 1240 | $ticket_class::is_customer() || |
| 1207 | - ( $ticket->auth_code == $auth_code ) | |
| 1241 | + ( $ticket->auth_code && $auth_code && hash_equals( (string) $ticket->auth_code, $auth_code ) ) | |
| 1208 | 1242 | ) |
| 1209 | 1243 | ) { |
| 1210 | 1244 | wp_send_json_error( 'Unauthorized!', 401 ); |
| 1211 | 1245 | } |
| @@ -1214,8 +1248,20 @@ | ||
| 1214 | 1248 | wp_send_json_error( __( 'Invalid ticket type.', 'supportcandy' ), 400 ); |
| 1215 | 1249 | } |
| 1216 | 1250 | break; |
| 1217 | 1251 | case 'img_editor_tmp': |
| 1252 | + // Not yet attached to any ticket/thread, so there is no auth-code | |
| 1253 | + // to check it against. Serve it only to whoever uploaded it, | |
| 1254 | + // proven either by the recorded uploader identity or by the | |
| 1255 | + // per-attachment nonce handed out in the upload response. | |
| 1256 | + // The identity check also keeps the image loading in the editor | |
| 1257 | + // when a guest logs in or registers mid-compose, which changes | |
| 1258 | + // the user the nonce is bound to. | |
| 1259 | + $view_nonce = isset( $_REQUEST['wpsc_nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['wpsc_nonce'] ) ) : ''; // phpcs:ignore | |
| 1260 | + if ( ! self::is_uploaded_by_current_user( $attachment ) && | |
| 1261 | + ( ! $view_nonce || ! wp_verify_nonce( $view_nonce, self::get_img_editor_tmp_nonce_action( $attachment->id ) ) ) ) { | |
| 1262 | + wp_send_json_error( 'Unauthorized!', 401 ); | |
| 1263 | + } | |
| 1218 | 1264 | self::file_download( $attachment ); |
| 1219 | 1265 | } |
| 1220 | 1266 | } |
| 1221 | 1267 | } |
| @@ -1265,9 +1311,9 @@ | ||
| 1265 | 1311 | */ |
| 1266 | 1312 | public static function tinymce_upload_file() { |
| 1267 | 1313 | |
| 1268 | 1314 | if ( check_ajax_referer( 'wpsc_tinymce_upload_file', '_ajax_nonce', false ) != 1 ) { |
| 1269 | - wp_send_json_error( 'Unauthorised request!', 401 ); | |
| 1315 | + wp_send_json_error( 'Unauthorized request!', 401 ); | |
| 1270 | 1316 | } |
| 1271 | 1317 | |
| 1272 | 1318 | $file = isset( $_FILES['file'] ) ? $_FILES['file'] : false; // phpcs:ignore |
| 1273 | 1319 | if ( ! $file ) { |
| @@ -1325,15 +1371,218 @@ | ||
| 1325 | 1371 | if ( ! $attachment->id ) { |
| 1326 | 1372 | wp_send_json_error( 'Something went wrong!', 500 ); |
| 1327 | 1373 | } |
| 1328 | 1374 | |
| 1329 | - wp_send_json( array( 'imgURL' => home_url( '/' ) . '?wpsc_attachment=' . $attachment->id ) ); | |
| 1375 | + // Temporary in-editor images have no ticket yet to check an auth-code | |
| 1376 | + // against, so gate access with a per-attachment nonce instead, known | |
| 1377 | + // only to whoever just uploaded it via this response. The uploader | |
| 1378 | + // is also recorded so the later claim can be checked server side. | |
| 1379 | + self::set_uploader( $attachment ); | |
| 1380 | + $view_nonce = wp_create_nonce( self::get_img_editor_tmp_nonce_action( $attachment->id ) ); | |
| 1330 | 1381 | |
| 1382 | + wp_send_json( array( 'imgURL' => home_url( '/' ) . '?wpsc_attachment=' . $attachment->id . '&wpsc_nonce=' . $view_nonce ) ); | |
| 1383 | + | |
| 1331 | 1384 | } else { |
| 1332 | 1385 | |
| 1333 | 1386 | $error_message = isset( $uploaded_file['error'] ) ? $uploaded_file['error'] : 'Something went wrong!'; |
| 1334 | 1387 | wp_send_json_error( $error_message, 500 ); |
| 1335 | 1388 | } |
| 1389 | + } | |
| 1390 | + | |
| 1391 | + /** | |
| 1392 | + * Capture the customer this request's session genuinely belongs to. | |
| 1393 | + * Runs on 'init' right after the current user is resolved, so that a | |
| 1394 | + * later change_current_user() (used when acting on behalf of someone | |
| 1395 | + * else, and in one case driven by request input) cannot influence | |
| 1396 | + * attachment ownership checks. | |
| 1397 | + * | |
| 1398 | + * @return void | |
| 1399 | + */ | |
| 1400 | + public static function snapshot_session_customer() { | |
| 1401 | + | |
| 1402 | + $current_user = WPSC_Current_User::$current_user; | |
| 1403 | + self::$session_customer_id = ( $current_user && $current_user->is_customer && $current_user->customer && $current_user->customer->id ) | |
| 1404 | + ? intval( $current_user->customer->id ) | |
| 1405 | + : 0; | |
| 1406 | + } | |
| 1407 | + | |
| 1408 | + /** | |
| 1409 | + * All identities that can legitimately be claimed by whoever is making | |
| 1410 | + * the current request. A visitor can hold more than one at a time, and | |
| 1411 | + * can move between them mid-compose (a guest who attaches a file and | |
| 1412 | + * then logs in or registers before submitting the ticket), so a claim | |
| 1413 | + * is accepted when the recorded uploader matches any one of them. | |
| 1414 | + * | |
| 1415 | + * Identities: | |
| 1416 | + * c:<customer id> anyone SupportCandy knows as a customer, which | |
| 1417 | + * covers both registered WordPress users (a customer | |
| 1418 | + * record is created for them on first sight) and | |
| 1419 | + * guest-login (OTP) customers; | |
| 1420 | + * g:<token> a random token in a cookie, issued on first upload, | |
| 1421 | + * for a plain guest who has no customer record yet - | |
| 1422 | + * a first time visitor only gets one when the ticket | |
| 1423 | + * is finally submitted. | |
| 1424 | + * | |
| 1425 | + * @param boolean $create - whether to issue a guest token if none exists. | |
| 1426 | + * @return array | |
| 1427 | + */ | |
| 1428 | + public static function get_current_uploader_ids( $create = false ) { | |
| 1429 | + | |
| 1430 | + $ids = array(); | |
| 1431 | + | |
| 1432 | + // Resolve the logged-in user's customer here rather than relying on | |
| 1433 | + // the 'init' snapshot alone: on REST requests an application | |
| 1434 | + // password is not authenticated until after 'init', so the snapshot | |
| 1435 | + // is still empty at that point. get_current_user_id() reflects | |
| 1436 | + // WordPress's own auth state and is unaffected by SupportCandy's | |
| 1437 | + // change_current_user(), so it stays safe to trust. | |
| 1438 | + $user_id = get_current_user_id(); | |
| 1439 | + if ( $user_id ) { | |
| 1440 | + | |
| 1441 | + $customer = WPSC_Customer::get_by_user_id( $user_id ); | |
| 1442 | + if ( ! $customer || ! $customer->id ) { | |
| 1443 | + $user = get_userdata( $user_id ); | |
| 1444 | + $customer = $user ? WPSC_Customer::get_by_email( $user->user_email ) : false; | |
| 1445 | + } | |
| 1446 | + | |
| 1447 | + if ( $customer && $customer->id ) { | |
| 1448 | + $ids[] = 'c:' . $customer->id; | |
| 1449 | + } | |
| 1450 | + } | |
| 1451 | + | |
| 1452 | + // Guest login (OTP) keeps a customer record without a WordPress | |
| 1453 | + // user, and is captured on 'init' before it can be reassigned. | |
| 1454 | + if ( self::$session_customer_id && ! in_array( 'c:' . self::$session_customer_id, $ids, true ) ) { | |
| 1455 | + $ids[] = 'c:' . self::$session_customer_id; | |
| 1456 | + } | |
| 1457 | + | |
| 1458 | + $token = isset( $_COOKIE['wpsc_uploader'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['wpsc_uploader'] ) ) : ''; | |
| 1459 | + $token = preg_match( '/^[a-f0-9]{32}$/', $token ) ? $token : ''; | |
| 1460 | + | |
| 1461 | + if ( ! $token && $create ) { | |
| 1462 | + | |
| 1463 | + $token = md5( wp_generate_password( 32, false ) . microtime() ); | |
| 1464 | + setcookie( 'wpsc_uploader', $token, time() + DAY_IN_SECONDS, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, is_ssl(), true ); | |
| 1465 | + $_COOKIE['wpsc_uploader'] = $token; | |
| 1466 | + } | |
| 1467 | + | |
| 1468 | + if ( $token ) { | |
| 1469 | + $ids[] = 'g:' . $token; | |
| 1470 | + } | |
| 1471 | + | |
| 1472 | + return $ids; | |
| 1473 | + } | |
| 1474 | + | |
| 1475 | + /** | |
| 1476 | + * Record the current requester as the uploader of the given attachment. | |
| 1477 | + * | |
| 1478 | + * @param WPSC_Attachment $attachment - attachment object. | |
| 1479 | + * @return void | |
| 1480 | + */ | |
| 1481 | + public static function set_uploader( $attachment ) { | |
| 1482 | + | |
| 1483 | + if ( ! $attachment || ! $attachment->id ) { | |
| 1484 | + return; | |
| 1485 | + } | |
| 1486 | + | |
| 1487 | + // Always issue the cookie too, so a visitor who changes identity | |
| 1488 | + // mid-compose (guest who then logs in or registers) still holds the | |
| 1489 | + // identity that was recorded here. | |
| 1490 | + $uploader_ids = self::get_current_uploader_ids( true ); | |
| 1491 | + if ( ! $uploader_ids ) { | |
| 1492 | + return; | |
| 1493 | + } | |
| 1494 | + | |
| 1495 | + $attachment->uploaded_by = $uploader_ids[0]; | |
| 1496 | + $attachment->save(); | |
| 1497 | + } | |
| 1498 | + | |
| 1499 | + /** | |
| 1500 | + * Whether the current requester is the one who uploaded this attachment. | |
| 1501 | + * | |
| 1502 | + * @param WPSC_Attachment $attachment - attachment object. | |
| 1503 | + * @return boolean | |
| 1504 | + */ | |
| 1505 | + public static function is_uploaded_by_current_user( $attachment ) { | |
| 1506 | + | |
| 1507 | + if ( ! $attachment || ! $attachment->id || ! $attachment->uploaded_by ) { | |
| 1508 | + return false; | |
| 1509 | + } | |
| 1510 | + | |
| 1511 | + foreach ( self::get_current_uploader_ids() as $uploader ) { | |
| 1512 | + if ( hash_equals( (string) $attachment->uploaded_by, $uploader ) ) { | |
| 1513 | + return true; | |
| 1514 | + } | |
| 1515 | + } | |
| 1516 | + | |
| 1517 | + return false; | |
| 1518 | + } | |
| 1519 | + | |
| 1520 | + /** | |
| 1521 | + * Single gate for binding (claiming) an attachment to a ticket. | |
| 1522 | + * | |
| 1523 | + * An attachment may only be bound when all of the following hold: | |
| 1524 | + * - it exists; | |
| 1525 | + * - it does not already carry a ticket id. Note this is deliberately | |
| 1526 | + * independent of is_active: deleting a thread clears is_active but | |
| 1527 | + * leaves ticket_id in place, and such an attachment must still be | |
| 1528 | + * refused rather than becoming claimable again; | |
| 1529 | + * - the requester is the one who uploaded it, proven either by the | |
| 1530 | + * recorded uploader identity or by the per-attachment nonce handed | |
| 1531 | + * out in the upload response. | |
| 1532 | + * | |
| 1533 | + * Every path that binds an attachment to a ticket must go through this. | |
| 1534 | + * | |
| 1535 | + * @param WPSC_Attachment $attachment - attachment object. | |
| 1536 | + * @param string $nonce - optional per-attachment upload nonce. | |
| 1537 | + * @return boolean | |
| 1538 | + */ | |
| 1539 | + public static function can_claim( $attachment, $nonce = '' ) { | |
| 1540 | + | |
| 1541 | + if ( ! $attachment || ! $attachment->id || $attachment->ticket_id ) { | |
| 1542 | + return false; | |
| 1543 | + } | |
| 1544 | + | |
| 1545 | + if ( self::is_uploaded_by_current_user( $attachment ) ) { | |
| 1546 | + return true; | |
| 1547 | + } | |
| 1548 | + | |
| 1549 | + // Fall back to the upload nonce, which is only ever handed to the | |
| 1550 | + // uploader in the response to their own upload request. | |
| 1551 | + $nonce = $nonce ? $nonce : self::get_submitted_upload_nonce( $attachment->id ); | |
| 1552 | + return $nonce && wp_verify_nonce( $nonce, self::get_img_editor_tmp_nonce_action( $attachment->id ) ) ? true : false; | |
| 1553 | + } | |
| 1554 | + | |
| 1555 | + /** | |
| 1556 | + * Read the upload nonce submitted alongside an attachment id, if any. | |
| 1557 | + * Sent by the upload widgets as wpsc_attachment_token[<id>]. | |
| 1558 | + * | |
| 1559 | + * @param int $attachment_id - attachment id. | |
| 1560 | + * @return string | |
| 1561 | + */ | |
| 1562 | + public static function get_submitted_upload_nonce( $attachment_id ) { | |
| 1563 | + | |
| 1564 | + if ( ! isset( $_REQUEST['wpsc_attachment_token'] ) || ! is_array( $_REQUEST['wpsc_attachment_token'] ) ) { // phpcs:ignore | |
| 1565 | + return ''; | |
| 1566 | + } | |
| 1567 | + | |
| 1568 | + $tokens = wp_unslash( $_REQUEST['wpsc_attachment_token'] ); // phpcs:ignore | |
| 1569 | + return isset( $tokens[ $attachment_id ] ) ? sanitize_text_field( $tokens[ $attachment_id ] ) : ''; | |
| 1570 | + } | |
| 1571 | + | |
| 1572 | + /** | |
| 1573 | + * Nonce action used to gate access to a not-yet-attached (img_editor_tmp) | |
| 1574 | + * in-editor image, since it isn't yet tied to any ticket/auth-code. | |
| 1575 | + * Also used, once the attachment is referenced from a reply/ticket body, | |
| 1576 | + * as proof that the requester is the one who uploaded it before it is | |
| 1577 | + * bound (claimed) to that ticket. Public: shared by reply/new-ticket | |
| 1578 | + * handlers in other classes that perform that claim. | |
| 1579 | + * | |
| 1580 | + * @param int $attachment_id - attachment id. | |
| 1581 | + * @return string | |
| 1582 | + */ | |
| 1583 | + public static function get_img_editor_tmp_nonce_action( $attachment_id ) { | |
| 1584 | + return 'wpsc_view_img_editor_tmp_' . $attachment_id; | |
| 1336 | 1585 | } |
| 1337 | 1586 | } |
| 1338 | 1587 | endif; |
| 1339 | 1588 | |