$qty ) { $qtys[ absint( $item_id ) ] = absint( $qty ); } $result = Workflow::receive( $request, $qtys, ! empty( $_POST['restock'] ) ); $flash = 'received'; break; case 'refund': $amount = isset( $_POST['amount'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['amount'] ) ) ) : ''; $result = Workflow::refund( $request, array( 'amount' => '' === $amount ? null : (float) $amount, 'restock' => ! empty( $_POST['restock'] ), 'gateway' => ! empty( $_POST['gateway'] ), 'shipping' => ! empty( $_POST['shipping'] ), 'message' => $message, ) ); $flash = 'refunded'; break; case 'complete': $result = Workflow::complete( $request, $message ); $flash = 'completed'; break; case 'note': $to_customer = ! empty( $_POST['to_customer'] ); if ( '' === $message ) { self::back( $url, '', __( 'Write a note first.', 'returnkit' ) ); } Workflow::note( $request, $message, $to_customer ); $flash = $to_customer ? 'messaged' : 'noted'; break; case 'link': $result = self::link_order( $request ); $flash = 'linked'; break; case 'delete': if ( $request->is_withdrawal() && $request->order_id() ) { // A withdrawal linked to an order is a legal record. self::back( $url, '', __( 'Withdrawals linked to an order can\'t be deleted: they are the record of the customer\'s statement.', 'returnkit' ) ); } Requests::delete( $request->id() ); $order = $request->order(); if ( $order ) { /* translators: %s: request number. */ $order->add_order_note( sprintf( __( 'ReturnKit: request %s was deleted by staff.', 'returnkit' ), $request->number() ) ); } self::back( Admin::url( 'requests' ), 'deleted' ); break; default: /** * Handle a custom request action (Pro: issue credit, create exchange order, labels). * Return a WP_Error, a Request, or a string flash key. * * @param mixed $result Null when unhandled. * @param string $do Action. * @param \ReturnKit\Domain\Request $request Request. */ $result = apply_filters( 'rtkt_admin_request_action', null, $do, $request ); if ( is_string( $result ) ) { $flash = $result; $result = null; } } if ( is_wp_error( $result ) ) { self::back( $url, '', $result->get_error_message() ); } self::back( $url, $flash ); } /** * Link an unmatched withdrawal to an order the staff member identified. * * @param \ReturnKit\Domain\Request $request Request. * @return \ReturnKit\Domain\Request|\WP_Error */ private static function link_order( $request ) { $number = isset( $_POST['order_number'] ) ? sanitize_text_field( wp_unslash( $_POST['order_number'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in handle_request(). // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce's own hook. $id = absint( apply_filters( 'woocommerce_shortcode_order_tracking_order_id', ltrim( $number, '#' ) ) ); $order = $id ? wc_get_order( $id ) : null; if ( ! $order instanceof \WC_Order || $order instanceof \WC_Order_Refund ) { return new \WP_Error( 'rtkt_order', __( 'No order with that number.', 'returnkit' ) ); } if ( $request->order_id() ) { return new \WP_Error( 'rtkt_linked', __( 'This request is already linked to an order.', 'returnkit' ) ); } $flags = array_diff( $request->flags(), array( 'unmatched' ) ); Requests::update( $request->id(), array( 'order_id' => $order->get_id(), 'customer_id' => (int) $order->get_customer_id(), 'flags' => implode( ',', $flags ), ) ); /* translators: %s: order number. */ Log::add( $request->id(), 'linked', sprintf( __( 'Linked to order #%s by staff.', 'returnkit' ), $order->get_order_number() ), 'admin' ); /* translators: %s: request number. */ $order->add_order_note( sprintf( __( 'ReturnKit: withdrawal %s linked to this order.', 'returnkit' ), $request->number() ) ); return Requests::get( $request->id() ); } /** * CSV export of requests (current filters). * * @return void */ public static function handle_export() { self::authorize(); check_admin_referer( 'rtkt_export' ); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- verified above. $args = array( 'status' => isset( $_GET['status'] ) ? sanitize_key( wp_unslash( $_GET['status'] ) ) : '', 'type' => isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : '', 'search' => isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '', 'flagged' => ! empty( $_GET['flagged'] ), ); // phpcs:enable nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=returnkit-requests-' . gmdate( 'Y-m-d' ) . '.csv' ); $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- UTF-8 BOM for Excel. self::csv_row( $out, array( 'request', 'type', 'status', 'created_utc', 'order', 'customer', 'email', 'items', 'reasons', 'value', 'refunded', 'credited', 'resolution', 'flags', 'source' ) ); $offset = 0; do { $batch = Requests::query( $args, 500, $offset ); foreach ( $batch as $request ) { $items = array(); $reasons = array(); foreach ( $request->items() as $item ) { $items[] = (int) $item['qty'] . ' x ' . $item['name']; $reasons[] = Reasons::label( $item['reason'] ); } $order = $request->order(); self::csv_row( $out, array( $request->number(), $request->type(), $request->status(), $request->created_gmt(), $order ? $order->get_order_number() : '', $request->name(), $request->email(), implode( '; ', $items ), implode( '; ', array_unique( array_filter( $reasons ) ) ), wc_format_decimal( $request->total(), wc_get_price_decimals() ), wc_format_decimal( $request->refunded(), wc_get_price_decimals() ), wc_format_decimal( $request->credited(), wc_get_price_decimals() ), $request->resolution(), implode( ', ', array_map( array( Labels::class, 'flag' ), $request->flags() ) ), $request->get( 'source' ), ) ); } $offset += 500; } while ( count( $batch ) === 500 ); fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } /** * Write a CSV row, neutralizing spreadsheet formulas. * * @param resource $out Handle. * @param array $fields Fields. * @return void */ public static function csv_row( $out, array $fields ) { $fields = array_map( static function ( $value ) { $value = (string) $value; return preg_match( '/^[=+\-@\t\r]/', $value ) ? "'" . $value : $value; }, $fields ); fputcsv( $out, $fields, ',', '"', '\\' ); } /** * Capability check. * * @return void */ private static function authorize() { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'returnkit' ), 403 ); } } /** * Redirect back with a message, and stop. * * @param string $url URL. * @param string $msg Flash key. * @param string $error Error text. * @return void */ private static function back( $url, $msg, $error = '' ) { if ( '' !== $error ) { // Kept server-side, so a crafted link can't put words on our screens. set_transient( 'rtkt_flash_' . get_current_user_id(), $error, 5 * MINUTE_IN_SECONDS ); $url = add_query_arg( 'rtkt_msg', 'error', $url ); } elseif ( '' !== $msg ) { $url = add_query_arg( 'rtkt_msg', $msg, $url ); } wp_safe_redirect( $url ); exit; } }