Changeset 3799773
- Timestamp:
- 11/13/2026 06:38:04 AM (37 hours ago)
- Location:
- gatecraft
- Files:
-
- 24 edited
- 1 copied
-
tags/2026.1.3.12 (copied) (copied from gatecraft/trunk )
-
tags/2026.1.3.12/gatecraft.php (modified) (2 diffs)
-
tags/2026.1.3.12/includes/eventclone.php (modified) (1 diff)
-
tags/2026.1.3.12/includes/eventpack.php (modified) (1 diff)
-
tags/2026.1.3.12/includes/helpers.php (modified) (1 diff)
-
tags/2026.1.3.12/includes/http/admin.php (modified) (1 diff)
-
tags/2026.1.3.12/includes/http/public.php (modified) (1 diff)
-
tags/2026.1.3.12/includes/orders.php (modified) (1 diff)
-
tags/2026.1.3.12/includes/router.php (modified) (1 diff)
-
tags/2026.1.3.12/includes/security.php (modified) (1 diff)
-
tags/2026.1.3.12/languages/ja.php (modified) (1 diff)
-
tags/2026.1.3.12/readme.txt (modified) (2 diffs)
-
tags/2026.1.3.12/templates/admin/event_form.php (modified) (1 diff)
-
trunk/gatecraft.php (modified) (2 diffs)
-
trunk/includes/eventclone.php (modified) (1 diff)
-
trunk/includes/eventpack.php (modified) (1 diff)
-
trunk/includes/helpers.php (modified) (1 diff)
-
trunk/includes/http/admin.php (modified) (1 diff)
-
trunk/includes/http/public.php (modified) (1 diff)
-
trunk/includes/orders.php (modified) (1 diff)
-
trunk/includes/router.php (modified) (1 diff)
-
trunk/includes/security.php (modified) (1 diff)
-
trunk/languages/ja.php (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/templates/admin/event_form.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
gatecraft/tags/2026.1.3.12/gatecraft.php
r3797341 r3799773 3 3 * Plugin Name: Gatecraft 4 4 * Description: Sell tickets for your own events: priced categories, card and offline payments, QR tickets by email, and a phone camera door scanner. 5 * Version: 2026.1.3.1 15 * Version: 2026.1.3.12 6 6 * Requires at least: 6.7 7 7 * Requires PHP: 8.1 … … 26 26 } 27 27 28 define('GATECRAFT_VERSION', '2026.1.3.1 1');28 define('GATECRAFT_VERSION', '2026.1.3.12'); 29 29 define('GATECRAFT_DB_VERSION', 18); 30 30 define('GATECRAFT_FILE', __FILE__); -
gatecraft/tags/2026.1.3.12/includes/eventclone.php
r3795649 r3799773 45 45 $row['title'] = mb_substr((string)($o['title'] ?? $src['title']), 0, 190); 46 46 $row['slug'] = eventpack_unique_slug((string)($o['slug'] ?? $src['slug'])); 47 // A copy of an unlisted event gets its own private address part, never the original's. 48 if (($row['visibility'] ?? 'public') === 'unlisted') $row['slug'] = event_private_slug(event_slug_base(event_slug_base($row['slug']))); 47 49 $row['status'] = in_array($o['status'] ?? 'draft', ['draft', 'published'], true) ? ($o['status'] ?? 'draft') : 'draft'; 48 50 $row['starts_at'] = ldate('Y-m-d H:i:s', lstrtotime((string)$o['starts_at'])); -
gatecraft/tags/2026.1.3.12/includes/eventpack.php
r3795649 r3799773 257 257 if (isset($e['description'])) $e['description'] = rich_clean((string)$e['description']); 258 258 $e['slug'] = eventpack_unique_slug((string)($e['slug'] ?? $e['title'])); 259 if (($e['visibility'] ?? 'public') === 'unlisted') $e['slug'] = event_private_slug(event_slug_base($e['slug'])); 259 260 if ($e['slug'] !== ($pkg['event']['slug'] ?? '')) $notes[] = 'The page address was already in use, so this event is at "' . $e['slug'] . '".'; 260 261 $e['status'] = 'draft'; $e['venue_id'] = $venueId; $e['cover_image'] = eventpack_save_image($pkg, $e['cover_image'] ?? null); -
gatecraft/tags/2026.1.3.12/includes/helpers.php
r3797341 r3799773 687 687 } 688 688 689 /** 690 * Unlisted events are reached only by their address, so the address must not be guessable from the title. 691 * event_private_slug() ends it with 12 random hex characters (48 bits); event_slug_base() removes that part again, 692 * so a copy of an unlisted event never carries the original's private part. 693 */ 694 function event_slug_base(string $slug): string 695 { 696 return (string)preg_replace('/-[a-f0-9]{12}$/', '', $slug); 697 } 698 699 function event_private_slug(string $slug): string 700 { 701 if (preg_match('/-[a-f0-9]{12}$/', $slug)) return $slug; 702 return rtrim(substr($slug, 0, 100), '-') . '-' . bin2hex(random_bytes(6)); 703 } 704 689 705 /** Crockford base32 (no I, L, O, U): readable, unambiguous codes. */ 690 706 function random_code(int $len): string -
gatecraft/tags/2026.1.3.12/includes/http/admin.php
r3795460 r3799773 227 227 $title = str_in('title', 190, true, 'Title'); 228 228 $slug = slugify(str_in('slug', 120) ?: $title); 229 if (qv('SELECT 1 FROM {events} WHERE slug = ? AND id <> ?', [$slug, $id])) $slug = substr($slug, 0, 110) . '-' . strtolower(random_code(4)); 229 // An unlisted event's address gets a private part, so it cannot be guessed from the title. 230 if (post('visibility') === 'unlisted') $slug = event_private_slug($slug); 231 if (qv('SELECT 1 FROM {events} WHERE slug = ? AND id <> ?', [$slug, $id])) $slug = post('visibility') === 'unlisted' ? event_private_slug(event_slug_base($slug)) : substr($slug, 0, 110) . '-' . strtolower(random_code(4)); 230 232 $start = dt_in(post('starts_at')); 231 233 if (!$start) fail('Start date and time are required.'); -
gatecraft/tags/2026.1.3.12/includes/http/public.php
r3795460 r3799773 125 125 $data = event_page_data($slug) ?? abort(404, 'This event could not be found.'); 126 126 language_context($data['ev']['language'] ?? null, true); 127 if (($data['ev']['visibility'] ?? 'public') === 'unlisted') { 128 // Unlisted: keep the page out of search engines, and do not pass its private address on to other sites. 129 header('X-Robots-Tag: noindex, nofollow'); 130 header('Referrer-Policy: no-referrer'); 131 add_filter('wp_robots', fn(array $r) => ['noindex' => true, 'nofollow' => true] + $r); 132 $data['noindex'] = true; 133 } 127 134 render('public/event', $data); 128 135 } -
gatecraft/tags/2026.1.3.12/includes/orders.php
r3795460 r3799773 102 102 if ($event['starts_at'] <= now()) fail('Ticket sales for this event have closed.'); 103 103 104 // Limit open (unpaid or unapproved) orders per email, so one person cannot hold the whole event. 105 $open = (int)qv("SELECT COUNT(*) FROM {orders} WHERE event_id = ? AND buyer_email = ? AND status IN ('awaiting_approval','pending_payment') AND expires_at > ?", [$eventId, $buyer['email'], now()]); 104 // Limit open (unpaid or unapproved) orders per email, so one person cannot hold the whole event. Only bookings 105 // from this visitor count (same connection, or same signed-in account): bookings someone else made with this 106 // email cannot lock its owner out, and the message reveals nothing about bookings made elsewhere. 107 $open = (int)qv("SELECT COUNT(*) FROM {orders} WHERE event_id = ? AND buyer_email = ? AND status IN ('awaiting_approval','pending_payment') AND expires_at > ? 108 AND (ip_hash = ? OR (user_id IS NOT NULL AND user_id = ?))", [$eventId, $buyer['email'], now(), $ipHash, (int)($user['id'] ?? 0)]); 106 109 if ($open >= 2) fail('You already have open bookings for this event. Complete or cancel them first, or wait for them to expire.'); 107 110 // And per network address (hashed, never stored as an address), so changing the email does not let one person hold every seat. -
gatecraft/tags/2026.1.3.12/includes/router.php
r3795460 r3799773 48 48 // 2. Routes limited to staff check the capability with current_user_can() (require_perm()). 49 49 if ($perm !== null) require_perm($perm); 50 $args = array_map(fn($v) => ctype_digit($v) ? (int)$v : $v, array_slice($mm, 1)); 50 // Only {id} parts become numbers. A slug or token can be all digits ("2027") and must stay text. 51 preg_match_all('/\{([a-z]+)\}/', $pattern, $names); 52 $args = []; 53 foreach (array_slice($mm, 1) as $i => $v) $args[] = ($names[1][$i] ?? '') === 'id' ? (int)$v : $v; 51 54 try { 52 55 $fn = str_contains($handler, '\\') ? $handler : __NAMESPACE__ . '\\' . $handler; -
gatecraft/tags/2026.1.3.12/includes/security.php
r3795460 r3799773 162 162 header('Cross-Origin-Opener-Policy: same-origin'); 163 163 header('Cross-Origin-Resource-Policy: same-origin'); 164 if (is_https()) header('Strict-Transport-Security: max-age=63072000; includeSubDomains'); 164 // This site only: a plugin must not set the HTTPS rule for the owner's other subdomains. 165 if (is_https()) header('Strict-Transport-Security: max-age=31536000'); 165 166 header_remove('X-Powered-By'); 166 167 } -
gatecraft/tags/2026.1.3.12/languages/ja.php
r3795651 r3799773 2676 2676 'Unlimited' => '無制限', 2677 2677 'Unlisted, link only' => '非公開(リンクを知っている人のみ)', 2678 'Unlisted events get a private code at the end of their address, so only people with the link can find them. Share the address shown after you save.' => '非公開のイベントでは、アドレスの末尾に推測できないコードが付き、リンクを知っている人だけが開けます。保存後に表示されるアドレスを共有してください。', 2678 2679 'Unmatched card payment for booking' => '予約と一致しないクレジットカード支払い:', 2679 2680 'Unread' => '未読', -
gatecraft/tags/2026.1.3.12/readme.txt
r3797341 r3799773 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.1 7 Stable tag: 2026.1.3.1 17 Stable tag: 2026.1.3.12 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 129 129 == Changelog == 130 130 131 = 2026.1.3.12 = 132 * Changed: Unlisted events get a private code at the end of their address, so only people with the link can find them. They are also kept out of search engines. 133 * Fixed: An event whose address was only numbers could not be opened. 134 * Improved: Security and reliability improvements. 135 131 136 = 2026.1.3.11 = 132 137 * Improved: Gatecraft now runs in WordPress Playground, so you can try it in your browser with Live Preview. -
gatecraft/tags/2026.1.3.12/templates/admin/event_form.php
r3795460 r3799773 38 38 <div class="card stack"> 39 39 <?php echo wp_kses( field('select', 'status', 'Status', $ev['status'], ['options' => ['draft' => 'Draft (hidden)', 'published' => 'Published', 'archived' => 'Archived'] + ($ev['status'] === 'cancelled' ? ['cancelled' => 'Cancelled'] : [])]), allowed_html() ); ?> 40 <?php echo wp_kses( field('select', 'visibility', 'Visibility', $ev['visibility'], ['options' => ['public' => 'Public, listed on the home page', 'unlisted' => 'Unlisted, link only'] ]), allowed_html() ); ?>40 <?php echo wp_kses( field('select', 'visibility', 'Visibility', $ev['visibility'], ['options' => ['public' => 'Public, listed on the home page', 'unlisted' => 'Unlisted, link only'], 'help' => 'Unlisted events get a private code at the end of their address, so only people with the link can find them. Share the address shown after you save.']), allowed_html() ); ?> 41 41 <?php slot('event_form_visibility', ['ev' => $ev]); ?> 42 42 </div> -
gatecraft/trunk/gatecraft.php
r3797341 r3799773 3 3 * Plugin Name: Gatecraft 4 4 * Description: Sell tickets for your own events: priced categories, card and offline payments, QR tickets by email, and a phone camera door scanner. 5 * Version: 2026.1.3.1 15 * Version: 2026.1.3.12 6 6 * Requires at least: 6.7 7 7 * Requires PHP: 8.1 … … 26 26 } 27 27 28 define('GATECRAFT_VERSION', '2026.1.3.1 1');28 define('GATECRAFT_VERSION', '2026.1.3.12'); 29 29 define('GATECRAFT_DB_VERSION', 18); 30 30 define('GATECRAFT_FILE', __FILE__); -
gatecraft/trunk/includes/eventclone.php
r3795649 r3799773 45 45 $row['title'] = mb_substr((string)($o['title'] ?? $src['title']), 0, 190); 46 46 $row['slug'] = eventpack_unique_slug((string)($o['slug'] ?? $src['slug'])); 47 // A copy of an unlisted event gets its own private address part, never the original's. 48 if (($row['visibility'] ?? 'public') === 'unlisted') $row['slug'] = event_private_slug(event_slug_base(event_slug_base($row['slug']))); 47 49 $row['status'] = in_array($o['status'] ?? 'draft', ['draft', 'published'], true) ? ($o['status'] ?? 'draft') : 'draft'; 48 50 $row['starts_at'] = ldate('Y-m-d H:i:s', lstrtotime((string)$o['starts_at'])); -
gatecraft/trunk/includes/eventpack.php
r3795649 r3799773 257 257 if (isset($e['description'])) $e['description'] = rich_clean((string)$e['description']); 258 258 $e['slug'] = eventpack_unique_slug((string)($e['slug'] ?? $e['title'])); 259 if (($e['visibility'] ?? 'public') === 'unlisted') $e['slug'] = event_private_slug(event_slug_base($e['slug'])); 259 260 if ($e['slug'] !== ($pkg['event']['slug'] ?? '')) $notes[] = 'The page address was already in use, so this event is at "' . $e['slug'] . '".'; 260 261 $e['status'] = 'draft'; $e['venue_id'] = $venueId; $e['cover_image'] = eventpack_save_image($pkg, $e['cover_image'] ?? null); -
gatecraft/trunk/includes/helpers.php
r3797341 r3799773 687 687 } 688 688 689 /** 690 * Unlisted events are reached only by their address, so the address must not be guessable from the title. 691 * event_private_slug() ends it with 12 random hex characters (48 bits); event_slug_base() removes that part again, 692 * so a copy of an unlisted event never carries the original's private part. 693 */ 694 function event_slug_base(string $slug): string 695 { 696 return (string)preg_replace('/-[a-f0-9]{12}$/', '', $slug); 697 } 698 699 function event_private_slug(string $slug): string 700 { 701 if (preg_match('/-[a-f0-9]{12}$/', $slug)) return $slug; 702 return rtrim(substr($slug, 0, 100), '-') . '-' . bin2hex(random_bytes(6)); 703 } 704 689 705 /** Crockford base32 (no I, L, O, U): readable, unambiguous codes. */ 690 706 function random_code(int $len): string -
gatecraft/trunk/includes/http/admin.php
r3795460 r3799773 227 227 $title = str_in('title', 190, true, 'Title'); 228 228 $slug = slugify(str_in('slug', 120) ?: $title); 229 if (qv('SELECT 1 FROM {events} WHERE slug = ? AND id <> ?', [$slug, $id])) $slug = substr($slug, 0, 110) . '-' . strtolower(random_code(4)); 229 // An unlisted event's address gets a private part, so it cannot be guessed from the title. 230 if (post('visibility') === 'unlisted') $slug = event_private_slug($slug); 231 if (qv('SELECT 1 FROM {events} WHERE slug = ? AND id <> ?', [$slug, $id])) $slug = post('visibility') === 'unlisted' ? event_private_slug(event_slug_base($slug)) : substr($slug, 0, 110) . '-' . strtolower(random_code(4)); 230 232 $start = dt_in(post('starts_at')); 231 233 if (!$start) fail('Start date and time are required.'); -
gatecraft/trunk/includes/http/public.php
r3795460 r3799773 125 125 $data = event_page_data($slug) ?? abort(404, 'This event could not be found.'); 126 126 language_context($data['ev']['language'] ?? null, true); 127 if (($data['ev']['visibility'] ?? 'public') === 'unlisted') { 128 // Unlisted: keep the page out of search engines, and do not pass its private address on to other sites. 129 header('X-Robots-Tag: noindex, nofollow'); 130 header('Referrer-Policy: no-referrer'); 131 add_filter('wp_robots', fn(array $r) => ['noindex' => true, 'nofollow' => true] + $r); 132 $data['noindex'] = true; 133 } 127 134 render('public/event', $data); 128 135 } -
gatecraft/trunk/includes/orders.php
r3795460 r3799773 102 102 if ($event['starts_at'] <= now()) fail('Ticket sales for this event have closed.'); 103 103 104 // Limit open (unpaid or unapproved) orders per email, so one person cannot hold the whole event. 105 $open = (int)qv("SELECT COUNT(*) FROM {orders} WHERE event_id = ? AND buyer_email = ? AND status IN ('awaiting_approval','pending_payment') AND expires_at > ?", [$eventId, $buyer['email'], now()]); 104 // Limit open (unpaid or unapproved) orders per email, so one person cannot hold the whole event. Only bookings 105 // from this visitor count (same connection, or same signed-in account): bookings someone else made with this 106 // email cannot lock its owner out, and the message reveals nothing about bookings made elsewhere. 107 $open = (int)qv("SELECT COUNT(*) FROM {orders} WHERE event_id = ? AND buyer_email = ? AND status IN ('awaiting_approval','pending_payment') AND expires_at > ? 108 AND (ip_hash = ? OR (user_id IS NOT NULL AND user_id = ?))", [$eventId, $buyer['email'], now(), $ipHash, (int)($user['id'] ?? 0)]); 106 109 if ($open >= 2) fail('You already have open bookings for this event. Complete or cancel them first, or wait for them to expire.'); 107 110 // And per network address (hashed, never stored as an address), so changing the email does not let one person hold every seat. -
gatecraft/trunk/includes/router.php
r3795460 r3799773 48 48 // 2. Routes limited to staff check the capability with current_user_can() (require_perm()). 49 49 if ($perm !== null) require_perm($perm); 50 $args = array_map(fn($v) => ctype_digit($v) ? (int)$v : $v, array_slice($mm, 1)); 50 // Only {id} parts become numbers. A slug or token can be all digits ("2027") and must stay text. 51 preg_match_all('/\{([a-z]+)\}/', $pattern, $names); 52 $args = []; 53 foreach (array_slice($mm, 1) as $i => $v) $args[] = ($names[1][$i] ?? '') === 'id' ? (int)$v : $v; 51 54 try { 52 55 $fn = str_contains($handler, '\\') ? $handler : __NAMESPACE__ . '\\' . $handler; -
gatecraft/trunk/includes/security.php
r3795460 r3799773 162 162 header('Cross-Origin-Opener-Policy: same-origin'); 163 163 header('Cross-Origin-Resource-Policy: same-origin'); 164 if (is_https()) header('Strict-Transport-Security: max-age=63072000; includeSubDomains'); 164 // This site only: a plugin must not set the HTTPS rule for the owner's other subdomains. 165 if (is_https()) header('Strict-Transport-Security: max-age=31536000'); 165 166 header_remove('X-Powered-By'); 166 167 } -
gatecraft/trunk/languages/ja.php
r3795651 r3799773 2676 2676 'Unlimited' => '無制限', 2677 2677 'Unlisted, link only' => '非公開(リンクを知っている人のみ)', 2678 'Unlisted events get a private code at the end of their address, so only people with the link can find them. Share the address shown after you save.' => '非公開のイベントでは、アドレスの末尾に推測できないコードが付き、リンクを知っている人だけが開けます。保存後に表示されるアドレスを共有してください。', 2678 2679 'Unmatched card payment for booking' => '予約と一致しないクレジットカード支払い:', 2679 2680 'Unread' => '未読', -
gatecraft/trunk/readme.txt
r3797341 r3799773 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.1 7 Stable tag: 2026.1.3.1 17 Stable tag: 2026.1.3.12 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 129 129 == Changelog == 130 130 131 = 2026.1.3.12 = 132 * Changed: Unlisted events get a private code at the end of their address, so only people with the link can find them. They are also kept out of search engines. 133 * Fixed: An event whose address was only numbers could not be opened. 134 * Improved: Security and reliability improvements. 135 131 136 = 2026.1.3.11 = 132 137 * Improved: Gatecraft now runs in WordPress Playground, so you can try it in your browser with Live Preview. -
gatecraft/trunk/templates/admin/event_form.php
r3795460 r3799773 38 38 <div class="card stack"> 39 39 <?php echo wp_kses( field('select', 'status', 'Status', $ev['status'], ['options' => ['draft' => 'Draft (hidden)', 'published' => 'Published', 'archived' => 'Archived'] + ($ev['status'] === 'cancelled' ? ['cancelled' => 'Cancelled'] : [])]), allowed_html() ); ?> 40 <?php echo wp_kses( field('select', 'visibility', 'Visibility', $ev['visibility'], ['options' => ['public' => 'Public, listed on the home page', 'unlisted' => 'Unlisted, link only'] ]), allowed_html() ); ?>40 <?php echo wp_kses( field('select', 'visibility', 'Visibility', $ev['visibility'], ['options' => ['public' => 'Public, listed on the home page', 'unlisted' => 'Unlisted, link only'], 'help' => 'Unlisted events get a private code at the end of their address, so only people with the link can find them. Share the address shown after you save.']), allowed_html() ); ?> 41 41 <?php slot('event_form_visibility', ['ev' => $ev]); ?> 42 42 </div>
Note:
See TracChangeset
for help on using the changeset viewer.