PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 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 All 257 releases
← All changes | src/EventTickets/Repositories/EventTicketRepository.php +48 -4 4.16.3 → 4.18.0 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,14 @@
58 59 ->where('id', $id);
59 60 }
60 61
61 62 /**
63 + * @since 4.18.0 Keep the original error as the previous exception when the write fails.
64 + * @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 65 * @since 3.20.0 Add "amount" column to the insert statement
63 66 * @since 3.6.0
64 67 *
65 - * @throws Exception|InvalidArgumentException
68 + * @throws Exception|InvalidArgumentException|RuntimeException
66 69 */
67 70 public function insert(EventTicket $eventTicket)
68 71 {
69 72 if (!$this->isFeatureActive()) {
@@ -77,8 +80,14 @@
77 80 $createdDateTime = Temporal::withoutMicroseconds($eventTicket->createdAt ?: Temporal::getCurrentDateTime());
78 81
79 82 DB::query('START TRANSACTION');
80 83
84 + if (!$this->hasRemainingCapacity($eventTicket)) {
85 + DB::query('ROLLBACK');
86 +
87 + throw new RuntimeException('Ticket type has no remaining capacity');
88 + }
89 +
81 90 try {
82 91 DB::table('give_event_tickets')
83 92 ->insert([
84 93 'event_id' => $eventTicket->eventId,
@@ -94,9 +103,9 @@
94 103 DB::query('ROLLBACK');
95 104
96 105 Log::error('Failed creating an event ticket', compact('eventTicket'));
97 106
98 - throw new $exception('Failed creating an event ticket');
107 + throw new Exception('Failed creating an event ticket', 0, $exception);
99 108 }
100 109
101 110 $eventTicket->id = $eventTicketId;
102 111 $eventTicket->createdAt = $createdDateTime;
@@ -107,8 +116,42 @@
107 116 Hooks::doAction('givewp_events_event_ticket_created', $eventTicket);
108 117 }
109 118
110 119 /**
120 + * Locks the ticket type's row and checks its remaining capacity against a fresh ticket count taken
121 + * under that lock. Must only be called after DB::query('START TRANSACTION') — the lock it takes is
122 + * what makes the check-then-insert in insert() atomic across concurrent requests for the same
123 + * ticket type; called on its own, outside a transaction, it would just be another stale read.
124 + *
125 + * @since 4.16.8.1
126 + */
127 + private function hasRemainingCapacity(EventTicket $eventTicket): bool
128 + {
129 + global $wpdb;
130 +
131 + $capacity = DB::get_var(
132 + DB::prepare(
133 + "SELECT capacity FROM {$wpdb->give_event_ticket_types} WHERE id = %d FOR UPDATE",
134 + $eventTicket->ticketTypeId
135 + )
136 + );
137 +
138 + if ($capacity === null) {
139 + return false;
140 + }
141 +
142 + $ticketCount = (int)DB::get_var(
143 + DB::prepare(
144 + "SELECT COUNT(*) FROM {$wpdb->give_event_tickets} WHERE ticket_type_id = %d",
145 + $eventTicket->ticketTypeId
146 + )
147 + );
148 +
149 + return $ticketCount < (int)$capacity;
150 + }
151 +
152 + /**
153 + * @since 4.18.0 Keep the original error as the previous exception when the write fails.
111 154 * @since 3.20.0 Add "amount" column to the update statement
112 155 * @since 3.6.0
113 156 *
114 157 * @throws Exception|InvalidArgumentException
@@ -142,9 +185,9 @@
142 185 DB::query('ROLLBACK');
143 186
144 187 Log::error('Failed updating an event ticket', compact('eventTicket'));
145 188
146 - throw new $exception('Failed updating an event ticket');
189 + throw new Exception('Failed updating an event ticket', 0, $exception);
147 190 }
148 191
149 192 $eventTicket->updatedAt = $updatedDateTime;
150 193
@@ -153,8 +196,9 @@
153 196 Hooks::doAction('givewp_events_event_ticket_updated', $eventTicket);
154 197 }
155 198
156 199 /**
200 + * @since 4.18.0 Keep the original error as the previous exception when the write fails.
157 201 * @since 3.6.0
158 202 *
159 203 * @throws Exception
160 204 */
@@ -176,9 +220,9 @@
176 220 DB::query('ROLLBACK');
177 221
178 222 Log::error('Failed deleting an event ticket', compact('eventTicket'));
179 223
180 - throw new $exception('Failed deleting an event ticket');
224 + throw new Exception('Failed deleting an event ticket', 0, $exception);
181 225 }
182 226
183 227 DB::query('COMMIT');
184 228