PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.16.9
GiveWP – Donation Plugin and Fundraising Platform v4.16.9
4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 All 255 releases
← All changes | src/EventTickets/Repositories/EventTicketRepository.php +42 -1 4.16.64.16.9 View file →
@@ -8,8 +8,9 @@
8 8 use Give\EventTickets\Models\EventTicket;
9 9 use Give\Framework\Database\DB;
10 10 use Give\Framework\Exceptions\Primitives\Exception;
11 11 use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
12 +use Give\Framework\Exceptions\Primitives\RuntimeException;
12 13 use Give\Framework\Models\ModelQueryBuilder;
13 14 use Give\Framework\Support\Facades\DateTime\Temporal;
14 15 use Give\Framework\Support\ValueObjects\Money;
15 16 use Give\Helpers\Hooks;
@@ -58,12 +59,13 @@
58 59 ->where('id', $id);
59 60 }
60 61
61 62 /**
63 + * @since 4.16.8.1 Enforce the ticket type's remaining capacity atomically with the insert (locking the ticket type row and re-counting under that lock), closing a race that let concurrent purchases jointly oversell it.
62 64 * @since 3.20.0 Add "amount" column to the insert statement
63 65 * @since 3.6.0
64 66 *
65 - * @throws Exception|InvalidArgumentException
67 + * @throws Exception|InvalidArgumentException|RuntimeException
66 68 */
67 69 public function insert(EventTicket $eventTicket)
68 70 {
69 71 if (!$this->isFeatureActive()) {
@@ -77,8 +79,14 @@
77 79 $createdDateTime = Temporal::withoutMicroseconds($eventTicket->createdAt ?: Temporal::getCurrentDateTime());
78 80
79 81 DB::query('START TRANSACTION');
80 82
83 + if (!$this->hasRemainingCapacity($eventTicket)) {
84 + DB::query('ROLLBACK');
85 +
86 + throw new RuntimeException('Ticket type has no remaining capacity');
87 + }
88 +
81 89 try {
82 90 DB::table('give_event_tickets')
83 91 ->insert([
84 92 'event_id' => $eventTicket->eventId,
@@ -104,8 +112,41 @@
104 112
105 113 DB::query('COMMIT');
106 114
107 115 Hooks::doAction('givewp_events_event_ticket_created', $eventTicket);
116 + }
117 +
118 + /**
119 + * Locks the ticket type's row and checks its remaining capacity against a fresh ticket count taken
120 + * under that lock. Must only be called after DB::query('START TRANSACTION') — the lock it takes is
121 + * what makes the check-then-insert in insert() atomic across concurrent requests for the same
122 + * ticket type; called on its own, outside a transaction, it would just be another stale read.
123 + *
124 + * @since 4.16.8.1
125 + */
126 + private function hasRemainingCapacity(EventTicket $eventTicket): bool
127 + {
128 + global $wpdb;
129 +
130 + $capacity = DB::get_var(
131 + DB::prepare(
132 + "SELECT capacity FROM {$wpdb->give_event_ticket_types} WHERE id = %d FOR UPDATE",
133 + $eventTicket->ticketTypeId
134 + )
135 + );
136 +
137 + if ($capacity === null) {
138 + return false;
139 + }
140 +
141 + $ticketCount = (int)DB::get_var(
142 + DB::prepare(
143 + "SELECT COUNT(*) FROM {$wpdb->give_event_tickets} WHERE ticket_type_id = %d",
144 + $eventTicket->ticketTypeId
145 + )
146 + );
147 +
148 + return $ticketCount < (int)$capacity;
108 149 }
109 150
110 151 /**
111 152 * @since 3.20.0 Add "amount" column to the update statement