PluginProbe
SupportCandy – AI Customer Support Ticket System & Live Chatbot Agent / 3.5.4
SupportCandy – AI Customer Support Ticket System & Live Chatbot Agent v3.5.4
3.5.4 3.5.3 3.5.2 3.5.1 3.4.9 3.5.0 3.4.8 3.4.7 trunk 2.3.1 3.3.6 3.3.7 3.3.8 3.3.9 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6
← All changes | includes/models/class-wpsc-attachment.php +349 -49 3.3.7 → 3.5.4 View file →
@@ -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,13 +644,23 @@
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 - wp_send_json_error( 'Something went wrong!', 500 );
661 + $error_message = isset( $uploaded_file['error'] ) ? $uploaded_file['error'] : 'Something went wrong!';
662 + wp_send_json_error( $error_message, 500 );
635 663 }
636 664 }
637 665
638 666 /**
@@ -649,9 +677,17 @@
649 677 } else {
650 678 $attachments = self::find(
651 679 array(
652 680 'search' => $search,
653 - '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 + ),
654 690 )
655 691 )['results'];
656 692 if ( $attachments ) {
657 693 foreach ( $attachments as $attachment ) {
@@ -671,9 +707,9 @@
671 707 */
672 708 public static function add_custom_image_tinymce() {
673 709
674 710 if ( check_ajax_referer( 'wpsc_add_custom_image_tinymce', '_ajax_nonce', false ) != 1 ) {
675 - wp_send_json_error( 'Unauthorised request!', 401 );
711 + wp_send_json_error( 'Unauthorized request!', 401 );
676 712 }
677 713
678 714 $title = esc_attr__( 'Insert/edit image', 'supportcandy' );
679 715
@@ -828,9 +864,9 @@
828 864 */
829 865 public static function edit_custom_image_tinymce() {
830 866
831 867 if ( check_ajax_referer( 'wpsc_edit_custom_image_tinymce', '_ajax_nonce', false ) != 1 ) {
832 - wp_send_json_error( 'Unauthorised request!', 401 );
868 + wp_send_json_error( 'Unauthorized request!', 401 );
833 869 }
834 870
835 871 $title = esc_attr__( 'Edit image', 'supportcandy' );
836 872
@@ -1046,9 +1082,8 @@
1046 1082 if ( ! $attachment->id ) {
1047 1083 return;
1048 1084 }
1049 1085 $auth_code = isset($_REQUEST['auth_code']) ? sanitize_text_field( $_REQUEST['auth_code'] ) : ''; // phpcs:ignore
1050 - $advanced = get_option( 'wpsc-ms-advanced-settings' );
1051 1086 switch ( $attachment->source ) {
1052 1087
1053 1088 case 'cf':
1054 1089 $cf = new WPSC_Custom_Field( $attachment->source_id );
@@ -1059,27 +1094,39 @@
1059 1094 if ( in_array( $cf->field, array( 'ticket', 'agentonly' ) ) ) { // ticket field.
1060 1095
1061 1096 $ticket = new WPSC_Ticket( $attachment->ticket_id );
1062 1097 if ( ! $ticket->id ) {
1063 - wp_send_json_error( __( 'Bad request!', 'supportcandy' ), 400 );
1098 + $ticket = new WPSC_Archive_Ticket( $attachment->ticket_id );
1099 + if ( ! $ticket->id ) {
1100 + wp_send_json_error( __( 'Bad request!', 'supportcandy' ), 400 );
1101 + }
1064 1102 }
1065 1103
1066 1104 if ( ! $has_auth ) {
1067 1105 $auth_code = isset( $_REQUEST['auth_code'] ) ? sanitize_text_field( $_REQUEST['auth_code'] ) : ''; // phpcs:ignore
1068 - if ( ! $auth_code || $auth_code != $ticket->auth_code ) {
1106 + if ( ! $auth_code || ! $ticket->auth_code || ! hash_equals( (string) $ticket->auth_code, $auth_code ) ) {
1069 1107 wp_send_json_error( 'Unauthorized!', 401 );
1070 1108 }
1071 1109 }
1072 1110
1073 - WPSC_Individual_Ticket::$ticket = $ticket;
1074 - if ( ! (
1075 - ( $current_user->is_agent && WPSC_Individual_Ticket::has_ticket_cap( 'view' ) ) ||
1076 - WPSC_Individual_Ticket::is_customer() ||
1077 - ( ! $advanced['ticket-url-auth'] && $ticket->auth_code == $auth_code )
1078 - ) ) {
1079 - wp_send_json_error( 'Unauthorized!', 401 );
1111 + if ( is_a( $ticket, 'WPSC_Ticket' ) || is_a( $ticket, 'WPSC_Archive_Ticket' ) ) {
1112 + // Determine correct class based on ticket type.
1113 + $ticket_class = is_a( $ticket, 'WPSC_Ticket' )
1114 + ? 'WPSC_Individual_Ticket'
1115 + : 'WPSC_Individual_Archive_Ticket';
1116 +
1117 + // Assign current ticket to class static property.
1118 + $ticket_class::$ticket = $ticket;
1119 + $has_access = (
1120 + ( $current_user->is_agent && $ticket_class::has_ticket_cap( 'view' ) ) ||
1121 + $ticket_class::is_customer() ||
1122 + ( $ticket->auth_code && hash_equals( (string) $ticket->auth_code, $auth_code ) )
1123 + );
1124 +
1125 + if ( ! $has_access ) {
1126 + wp_send_json_error( __( 'Unauthorized!', 'supportcandy' ), 401 );
1127 + }
1080 1128 }
1081 -
1082 1129 self::file_download( $attachment );
1083 1130
1084 1131 } else { // customer field.
1085 1132
@@ -1102,27 +1149,39 @@
1102 1149 case 'reply':
1103 1150 case 'report':
1104 1151 $ticket = new WPSC_Ticket( $attachment->ticket_id );
1105 1152 if ( ! $ticket->id ) {
1106 - wp_send_json_error( __( 'Bad request!', 'supportcandy' ), 400 );
1153 + $ticket = new WPSC_Archive_Ticket( $attachment->ticket_id );
1154 + if ( ! $ticket->id ) {
1155 + wp_send_json_error( __( 'Bad request!', 'supportcandy' ), 400 );
1156 + }
1107 1157 }
1108 1158
1109 1159 if ( ! $has_auth ) {
1110 1160 $auth_code = isset( $_REQUEST['auth_code'] ) ? sanitize_text_field( $_REQUEST['auth_code'] ) : ''; // phpcs:ignore
1111 - if ( ! $auth_code || $auth_code != $ticket->auth_code ) {
1161 + if ( ! $auth_code || ! $ticket->auth_code || ! hash_equals( (string) $ticket->auth_code, $auth_code ) ) {
1112 1162 wp_send_json_error( 'Unauthorized!', 401 );
1113 1163 }
1114 1164 }
1115 1165
1116 - WPSC_Individual_Ticket::$ticket = $ticket;
1117 - if ( ! (
1118 - ( $current_user->is_agent && WPSC_Individual_Ticket::has_ticket_cap( 'view' ) ) ||
1119 - WPSC_Individual_Ticket::is_customer() ||
1120 - ( ! $advanced['ticket-url-auth'] && $ticket->auth_code == $auth_code )
1121 - ) ) {
1122 - wp_send_json_error( 'Unauthorized!', 401 );
1166 + if ( is_a( $ticket, 'WPSC_Ticket' ) || is_a( $ticket, 'WPSC_Archive_Ticket' ) ) {
1167 + // Determine correct class based on ticket type.
1168 + $ticket_class = is_a( $ticket, 'WPSC_Ticket' )
1169 + ? 'WPSC_Individual_Ticket'
1170 + : 'WPSC_Individual_Archive_Ticket';
1171 +
1172 + // Assign current ticket to class static property.
1173 + $ticket_class::$ticket = $ticket;
1174 + $has_access = (
1175 + ( $current_user->is_agent && $ticket_class::has_ticket_cap( 'view' ) ) ||
1176 + $ticket_class::is_customer() ||
1177 + ( $ticket->auth_code && hash_equals( (string) $ticket->auth_code, $auth_code ) )
1178 + );
1179 +
1180 + if ( ! $has_access ) {
1181 + wp_send_json_error( __( 'Unauthorized!', 'supportcandy' ), 401 );
1182 + }
1123 1183 }
1124 -
1125 1184 self::file_download( $attachment );
1126 1185 break;
1127 1186
1128 1187 case 'note':
@@ -1127,23 +1186,33 @@
1127 1186
1128 1187 case 'note':
1129 1188 $ticket = new WPSC_Ticket( $attachment->ticket_id );
1130 1189 if ( ! $ticket->id ) {
1131 - wp_send_json_error( __( 'Bad request!', 'supportcandy' ), 400 );
1190 + $ticket = new WPSC_Archive_Ticket( $attachment->ticket_id );
1191 + if ( ! $ticket->id ) {
1192 + wp_send_json_error( __( 'Bad request!', 'supportcandy' ), 400 );
1193 + }
1132 1194 }
1133 1195
1134 1196 if ( ! $has_auth ) {
1135 1197 $auth_code = isset( $_REQUEST['auth_code'] ) ? sanitize_text_field( $_REQUEST['auth_code'] ) : ''; // phpcs:ignore
1136 - if ( ! $auth_code || $auth_code != $ticket->auth_code ) {
1198 + if ( ! $auth_code || ! $ticket->auth_code || ! hash_equals( (string) $ticket->auth_code, $auth_code ) ) {
1137 1199 wp_send_json_error( 'Unauthorized!', 401 );
1138 1200 }
1139 1201 }
1140 1202
1141 - WPSC_Individual_Ticket::$ticket = $ticket;
1142 - if ( ! (
1143 - $current_user->is_agent && WPSC_Individual_Ticket::has_ticket_cap( 'pn' )
1144 - ) ) {
1145 - wp_send_json_error( 'Unauthorized!', 401 );
1203 + if ( is_a( $ticket, 'WPSC_Ticket' ) || is_a( $ticket, 'WPSC_Archive_Ticket' ) ) {
1204 +
1205 + // Determine correct handler class dynamically.
1206 + $ticket_class = is_a( $ticket, 'WPSC_Ticket' )
1207 + ? 'WPSC_Individual_Ticket'
1208 + : 'WPSC_Individual_Archive_Ticket';
1209 +
1210 + // Assign the ticket.
1211 + $ticket_class::$ticket = $ticket;
1212 + if ( ! ( $current_user->is_agent && $ticket_class::has_ticket_cap( 'pn' ) ) ) {
1213 + wp_send_json_error( __( 'Unauthorized!', 'supportcandy' ), 401 );
1214 + }
1146 1215 }
1147 1216
1148 1217 self::file_download( $attachment );
1149 1218 break;
@@ -1150,22 +1219,49 @@
1150 1219
1151 1220 case 'img_editor':
1152 1221 $ticket = new WPSC_Ticket( $attachment->ticket_id );
1153 1222 if ( ! $ticket->id ) {
1154 - wp_send_json_error( __( 'Bad request!', 'supportcandy' ), 400 );
1223 + $ticket = new WPSC_Archive_Ticket( $attachment->ticket_id );
1224 + if ( ! $ticket->id ) {
1225 + wp_send_json_error( __( 'Bad request!', 'supportcandy' ), 400 );
1226 + }
1155 1227 }
1156 1228
1157 - WPSC_Individual_Ticket::$ticket = $ticket;
1158 - if ( ! (
1159 - ( $current_user->is_agent && WPSC_Individual_Ticket::has_ticket_cap( 'view' ) ) ||
1160 - WPSC_Individual_Ticket::is_customer() ||
1161 - ( $ticket->auth_code == $auth_code )
1162 - ) ) {
1163 - wp_send_json_error( 'Unauthorized!', 401 );
1229 + if ( is_a( $ticket, 'WPSC_Ticket' ) || is_a( $ticket, 'WPSC_Archive_Ticket' ) ) {
1230 +
1231 + // Determine class dynamically.
1232 + $ticket_class = is_a( $ticket, 'WPSC_Ticket' )
1233 + ? 'WPSC_Individual_Ticket'
1234 + : 'WPSC_Individual_Archive_Ticket';
1235 +
1236 + $ticket_class::$ticket = $ticket;
1237 + if (
1238 + ! (
1239 + ( $current_user->is_agent && $ticket_class::has_ticket_cap( 'view' ) ) ||
1240 + $ticket_class::is_customer() ||
1241 + ( $ticket->auth_code && $auth_code && hash_equals( (string) $ticket->auth_code, $auth_code ) )
1242 + )
1243 + ) {
1244 + wp_send_json_error( 'Unauthorized!', 401 );
1245 + }
1246 + self::file_download( $attachment );
1247 + } else {
1248 + wp_send_json_error( __( 'Invalid ticket type.', 'supportcandy' ), 400 );
1164 1249 }
1165 - self::file_download( $attachment );
1166 1250 break;
1167 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 + }
1168 1264 self::file_download( $attachment );
1169 1265 }
1170 1266 }
1171 1267 }
@@ -1215,9 +1311,9 @@
1215 1311 */
1216 1312 public static function tinymce_upload_file() {
1217 1313
1218 1314 if ( check_ajax_referer( 'wpsc_tinymce_upload_file', '_ajax_nonce', false ) != 1 ) {
1219 - wp_send_json_error( 'Unauthorised request!', 401 );
1315 + wp_send_json_error( 'Unauthorized request!', 401 );
1220 1316 }
1221 1317
1222 1318 $file = isset( $_FILES['file'] ) ? $_FILES['file'] : false; // phpcs:ignore
1223 1319 if ( ! $file ) {
@@ -1266,23 +1362,227 @@
1266 1362 if ( $uploaded_file && empty( $uploaded_file['error'] ) ) {
1267 1363
1268 1364 $pos = strpos( $uploaded_file['file'], '/wpsc/' );
1269 1365 if ( $pos === false ) {
1270 - wp_send_json_error( 'Something went wrong 1!', 500 );
1366 + wp_send_json_error( 'Something went wrong!', 500 );
1271 1367 }
1272 1368 $new_path = substr( $uploaded_file['file'], $pos );
1273 1369 $data['file_path'] = $new_path;
1274 1370 $attachment = self::insert( $data );
1275 1371 if ( ! $attachment->id ) {
1276 - wp_send_json_error( 'Something went wrong 2!', 500 );
1372 + wp_send_json_error( 'Something went wrong!', 500 );
1277 1373 }
1278 1374
1279 - 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 ) );
1280 1381
1382 + wp_send_json( array( 'imgURL' => home_url( '/' ) . '?wpsc_attachment=' . $attachment->id . '&wpsc_nonce=' . $view_nonce ) );
1383 +
1281 1384 } else {
1282 1385
1283 - wp_send_json_error( 'Something went wrong 3!', 500 );
1386 + $error_message = isset( $uploaded_file['error'] ) ? $uploaded_file['error'] : 'Something went wrong!';
1387 + wp_send_json_error( $error_message, 500 );
1284 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;
1285 1585 }
1286 1586 }
1287 1587 endif;
1288 1588