PluginProbe ʕ •ᴥ•ʔ
Event Tickets with Ticket Scanner / 3.1.2
Event Tickets with Ticket Scanner v3.1.2
3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 trunk 2.6.0 2.7.0 2.7.1 2.7.10 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 2.9.8 2.9.9 3.0.0 3.0.1 3.0.2 3.0.3
event-tickets-with-ticket-scanner / includes / congress / class-congress-admin.php
event-tickets-with-ticket-scanner / includes / congress Last commit date
class-congress-admin.php 1 week ago class-congress-api.php 1 week ago class-congress-page.php 1 week ago class-congress-repository.php 1 week ago class-template-variables.php 1 week ago
class-congress-admin.php
398 lines
1 <?php
2 if (!defined('ABSPATH')) exit;
3
4 class sasoEventtickets_CongressAdmin {
5
6 private sasoEventtickets $MAIN;
7
8 public function __construct(sasoEventtickets $main) {
9 $this->MAIN = $main;
10 }
11
12 public function registerMenu(): void {
13 // Register as hidden admin page (no sidebar entry) — accessible via the plugin's own nav button
14 add_submenu_page(
15 null,
16 __('Congresses', 'event-tickets-with-ticket-scanner'),
17 __('Congresses', 'event-tickets-with-ticket-scanner'),
18 'manage_options',
19 'saso-et-congresses',
20 [$this, 'renderPage']
21 );
22 }
23
24 public function renderPage(): void {
25 if (!current_user_can('manage_options')) wp_die('Forbidden');
26 wp_enqueue_script(
27 'saso-et-congress-admin',
28 plugin_dir_url(dirname(dirname(__FILE__))) . 'js/congress-admin.js',
29 ['jquery', 'jquery-ui-sortable', 'wp-i18n'],
30 SASO_EVENTTICKETS_PLUGIN_VERSION,
31 true
32 );
33 // congress-admin.css relies on the shared --et-* design tokens defined in
34 // styles_backend.css; load it first so borders/colors render on this standalone page too.
35 $backend_handle = $this->MAIN->getPrefix() . '_backendcss';
36 wp_enqueue_style(
37 $backend_handle,
38 plugin_dir_url(dirname(dirname(__FILE__))) . 'css/styles_backend.css',
39 [],
40 SASO_EVENTTICKETS_PLUGIN_VERSION
41 );
42 wp_set_script_translations(
43 'saso-et-congress-admin',
44 'event-tickets-with-ticket-scanner',
45 plugin_dir_path(dirname(dirname(__FILE__))) . 'languages'
46 );
47 wp_enqueue_style(
48 'saso-et-congress-admin',
49 plugin_dir_url(dirname(dirname(__FILE__))) . 'css/congress-admin.css',
50 [$backend_handle],
51 SASO_EVENTTICKETS_PLUGIN_VERSION
52 );
53 require_once dirname(__FILE__) . '/class-template-variables.php';
54 wp_localize_script('saso-et-congress-admin', 'sasoEtCongress', [
55 'ajaxUrl' => admin_url('admin-ajax.php'),
56 'nonce' => wp_create_nonce('sasoEventtickets'),
57 'variables' => sasoEventtickets_TemplateVariables::getList(),
58 ]);
59 echo '<div id="saso-et-congress-app"></div>';
60 }
61
62 public function handleAjax(): void {
63 check_ajax_referer('sasoEventtickets');
64 if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
65
66 $congress_action = sanitize_key($_POST['congress_action'] ?? '');
67 $repo = $this->MAIN->getCongressRepository();
68
69 switch ($congress_action) {
70
71 case 'list':
72 $all = $repo->getAll();
73 $data = [];
74 foreach ($all as $c) {
75 $product_ids = $repo->getProductIds((int)$c['id']);
76 $expired = !empty($c['access_expires_at']) && strtotime($c['access_expires_at']) < time();
77 $row = [
78 'id' => (int)$c['id'],
79 'title' => esc_html($c['title']),
80 'label' => esc_html($c['label'] ?? ''),
81 'slug' => esc_html($c['slug']),
82 'product_count' => count($product_ids),
83 'updated_at' => esc_html($c['updated_at']),
84 'access_expires_at' => esc_html($c['access_expires_at'] ?? ''),
85 'status' => $expired ? 'expired' : 'active',
86 ];
87 // Generic seam: premium can enrich each admin list row (e.g. add an export_url).
88 $row = apply_filters($this->MAIN->_add_filter_prefix.'congress_admin_list_row', $row, $c);
89 $data[] = $row;
90 }
91 // Raw JSON for DataTables server-side processing (no WP success wrapper)
92 wp_send_json(['draw' => 1, 'recordsTotal' => count($data), 'recordsFiltered' => count($data), 'data' => $data]);
93 break;
94
95 case 'get':
96 $id = (int)($_POST['id'] ?? 0);
97 if (!$id) wp_send_json_error('missing_id');
98 $congress = $repo->getById($id);
99 if (!$congress) wp_send_json_error('not_found');
100 $congress['product_ids'] = $repo->getProductIds($id);
101 $congress['sections'] = $repo->getSections($id);
102 $congress['landing_cards'] = $repo->getCongressMeta($congress)['landing_cards'];
103 // Generic seam: premium can enrich the editor payload with its own fields.
104 $congress = apply_filters($this->MAIN->_add_filter_prefix.'congress_admin_get', $congress);
105 wp_send_json_success($congress);
106 break;
107
108 case 'save':
109 $id = (int)($_POST['id'] ?? 0);
110 $slug = sanitize_title($_POST['slug'] ?? '');
111 $title = sanitize_text_field($_POST['title'] ?? '');
112 $expires = sanitize_text_field($_POST['access_expires_at'] ?? '');
113 $event_start = sanitize_text_field($_POST['event_start_at'] ?? '');
114 $event_end = sanitize_text_field($_POST['event_end_at'] ?? '');
115 $is_active = isset($_POST['is_active']) ? (int) (!empty($_POST['is_active']) && $_POST['is_active'] !== '0') : 1;
116 $label = sanitize_text_field($_POST['label'] ?? '');
117 $landing = !empty($_POST['landing_cards']) && $_POST['landing_cards'] !== '0';
118 if (empty($slug) || empty($title)) {
119 wp_send_json_error('missing_fields');
120 }
121 // Preserve existing meta, set landing_cards.
122 $existing = $id ? $repo->getById($id) : null;
123 $meta = ($existing && !empty($existing['meta'])) ? (json_decode($existing['meta'], true) ?: []) : [];
124 $meta['landing_cards'] = $landing;
125 // Generic seam: premium can read its own $_POST fields and add keys to $meta.
126 $meta = apply_filters($this->MAIN->_add_filter_prefix.'congress_save_meta', $meta, $_POST);
127 $saved_id = $repo->save([
128 'id' => $id ?: null,
129 'slug' => $slug,
130 'title' => $title,
131 'label' => $label,
132 'access_expires_at' => $expires ?: null,
133 'event_start_at' => $event_start ?: null,
134 'event_end_at' => $event_end ?: null,
135 'is_active' => $is_active,
136 'meta' => $meta,
137 ]);
138 if (!$saved_id) wp_send_json_error('save_failed');
139 wp_send_json_success(['id' => $saved_id]);
140 break;
141
142 case 'delete':
143 $id = (int)($_POST['id'] ?? 0);
144 if (!$id) wp_send_json_error('missing_id');
145 $repo->delete($id);
146 wp_send_json_success();
147 break;
148
149 case 'duplicate':
150 $id = (int)($_POST['id'] ?? 0);
151 $new_slug = sanitize_title($_POST['new_slug'] ?? '');
152 $new_title = sanitize_text_field($_POST['new_title'] ?? '');
153 if (!$id || !$new_slug) wp_send_json_error('missing_fields');
154 // Expire the original in 30 days
155 $orig = $repo->getById($id);
156 if ($orig) {
157 $orig['access_expires_at'] = date('Y-m-d H:i:s', strtotime('+30 days'));
158 if (!$repo->save($orig)) {
159 wp_send_json_error('expire_original_failed');
160 }
161 }
162 $new_id = $repo->duplicate($id, $new_slug, $new_title ?: null);
163 if (!$new_id) wp_send_json_error('duplicate_failed');
164 wp_send_json_success(['id' => $new_id]);
165 break;
166
167 case 'get_section':
168 $section_id = (int)($_POST['section_id'] ?? 0);
169 if (!$section_id) wp_send_json_error('missing_section_id');
170 $section = $repo->getSectionById($section_id);
171 if (!$section) wp_send_json_error('not_found');
172 wp_send_json_success($section);
173 break;
174
175 case 'save_section':
176 $section_id = (int)($_POST['section_id'] ?? 0);
177 $congress_id = (int)($_POST['congress_id'] ?? 0);
178 $page_id = (int)($_POST['page_id'] ?? 0);
179 $type = sanitize_key($_POST['type'] ?? 'info');
180 $title = sanitize_text_field($_POST['title'] ?? '');
181 $password = sanitize_text_field($_POST['password'] ?? '');
182 $sort_order = (int)($_POST['sort_order'] ?? 0);
183 $raw_content = stripslashes($_POST['content'] ?? '{}');
184 if (!$congress_id) wp_send_json_error('missing_congress_id');
185 // New section with no page given → drop it on the start page.
186 if (!$page_id) {
187 $start = $repo->getStartPage($congress_id);
188 $page_id = $start ? (int)$start['id'] : 0;
189 }
190 $content = $this->sanitizeSectionContent($type, $raw_content);
191 $sid = $repo->saveSection([
192 'id' => $section_id ?: null,
193 'congress_id' => $congress_id,
194 'page_id' => $page_id,
195 'type' => $type,
196 'title' => $title,
197 'password' => $password,
198 'sort_order' => $sort_order,
199 'content' => $content,
200 ]);
201 wp_send_json_success(['id' => $sid]);
202 break;
203
204 case 'delete_section':
205 $section_id = (int)($_POST['section_id'] ?? 0);
206 if (!$section_id) wp_send_json_error('missing_section_id');
207 $repo->deleteSection($section_id);
208 wp_send_json_success();
209 break;
210
211 case 'reorder_sections':
212 $congress_id = (int)($_POST['congress_id'] ?? 0);
213 $ordered_ids = array_map('intval', (array)($_POST['ordered_ids'] ?? []));
214 if (!$congress_id) wp_send_json_error('missing_congress_id');
215 $repo->reorderSections($congress_id, $ordered_ids);
216 wp_send_json_success();
217 break;
218
219 // ── Pages ──
220 case 'get_pages':
221 $congress_id = (int)($_POST['congress_id'] ?? 0);
222 if (!$congress_id) wp_send_json_error('missing_congress_id');
223 // Each page with its sections, so the admin editor can render the whole tree.
224 $pages = $repo->getPages($congress_id);
225 foreach ($pages as &$p) {
226 $p['sections'] = $repo->getSectionsForPage((int)$p['id']);
227 $meta = $repo->getPageMeta($p);
228 $p['icon'] = $meta['icon'];
229 $p['image_id'] = $meta['image_id'];
230 $p['description'] = $meta['description'];
231 $p['color'] = $meta['color'];
232 $p['image_url'] = $meta['image_id'] ? (wp_get_attachment_image_url($meta['image_id'], 'medium') ?: '') : '';
233 }
234 unset($p);
235 wp_send_json_success(['pages' => $pages]);
236 break;
237
238 case 'save_page':
239 $congress_id = (int)($_POST['congress_id'] ?? 0);
240 $page_id = (int)($_POST['page_id'] ?? 0);
241 $title = sanitize_text_field($_POST['title'] ?? '');
242 if (!$congress_id) wp_send_json_error('missing_congress_id');
243 $page_data = [
244 'id' => $page_id ?: null,
245 'congress_id' => $congress_id,
246 'title' => $title,
247 ];
248 // Only forward meta keys that were actually sent (merge-on-write in the repo).
249 if (array_key_exists('icon', $_POST)) $page_data['icon'] = sanitize_text_field($_POST['icon']);
250 if (array_key_exists('image_id', $_POST)) $page_data['image_id'] = (int) $_POST['image_id'];
251 if (array_key_exists('description', $_POST)) $page_data['description'] = sanitize_text_field($_POST['description']);
252 if (array_key_exists('color', $_POST)) $page_data['color'] = sanitize_hex_color($_POST['color']) ?: '';
253 $pid = $repo->savePage($page_data);
254 wp_send_json_success(['id' => $pid]);
255 break;
256
257 case 'delete_page':
258 $page_id = (int)($_POST['page_id'] ?? 0);
259 if (!$page_id) wp_send_json_error('missing_page_id');
260 // Refuses to delete the last remaining page (returns false).
261 if (!$repo->deletePage($page_id)) wp_send_json_error('cannot_delete_last_page');
262 wp_send_json_success();
263 break;
264
265 case 'reorder_pages':
266 $congress_id = (int)($_POST['congress_id'] ?? 0);
267 $ordered_ids = array_map('intval', (array)($_POST['ordered_ids'] ?? []));
268 if (!$congress_id) wp_send_json_error('missing_congress_id');
269 $repo->reorderPages($congress_id, $ordered_ids);
270 wp_send_json_success();
271 break;
272
273 default:
274 wp_send_json_error('unknown_action');
275 }
276 }
277
278 /**
279 * Sanitize section content by type. Strips all JavaScript (wp_kses_post).
280 */
281 public function sanitizeSectionContent(string $type, string $raw): array {
282 $data = json_decode($raw, true) ?? [];
283 switch ($type) {
284 case 'info':
285 case 'custom':
286 return ['html' => wp_kses_post($data['html'] ?? '')];
287
288 case 'program':
289 $days = [];
290 foreach ((array)($data['days'] ?? []) as $day) {
291 $slots = [];
292 foreach ((array)($day['slots'] ?? []) as $slot) {
293 $slots[] = [
294 'time' => sanitize_text_field($slot['time'] ?? ''),
295 'title' => sanitize_text_field($slot['title'] ?? ''),
296 'speaker' => sanitize_text_field($slot['speaker'] ?? ''),
297 'room' => sanitize_text_field($slot['room'] ?? ''),
298 ];
299 }
300 $days[] = ['date' => sanitize_text_field($day['date'] ?? ''), 'slots' => $slots];
301 }
302 return ['days' => $days];
303
304 case 'download':
305 $files = [];
306 foreach ((array)($data['files'] ?? []) as $f) {
307 $att_id = (int)($f['attachment_id'] ?? 0);
308 $url = $att_id ? (wp_get_attachment_url($att_id) ?: '') : esc_url_raw($f['url'] ?? '');
309 if (!$url) continue;
310 $files[] = [
311 'label' => sanitize_text_field($f['label'] ?? ''),
312 'attachment_id' => $att_id,
313 'filename' => sanitize_text_field($f['filename'] ?? ''),
314 'url' => $url,
315 'inline' => !empty($f['inline']), // true = open in browser, false = force download
316 ];
317 }
318 return ['files' => $files];
319
320 case 'url':
321 $urls = [];
322 foreach ((array)($data['urls'] ?? []) as $u) {
323 $href = esc_url_raw($u['url'] ?? '');
324 if (!$href) continue;
325 $urls[] = [
326 'label' => sanitize_text_field($u['label'] ?? $href),
327 'url' => $href,
328 'internal' => !empty($u['internal']),
329 ];
330 }
331 return ['urls' => $urls];
332
333 case 'media':
334 $items = [];
335 foreach ((array)($data['items'] ?? []) as $item) {
336 $att_id = (int)($item['attachment_id'] ?? 0);
337 $url = $att_id ? wp_get_attachment_url($att_id) : esc_url_raw($item['url'] ?? '');
338 if (!$url) continue;
339 $items[] = [
340 'attachment_id' => $att_id,
341 'caption' => sanitize_text_field($item['caption'] ?? ''),
342 'url' => $url,
343 ];
344 }
345 return ['items' => $items];
346
347 case 'speakers':
348 // Each speaker: image (id+url), name, title, plain-text bio (≤500 chars).
349 // bio is PLAIN text — sanitize_textarea_field, NOT wp_kses_post.
350 $speakers = [];
351 foreach ((array)($data['speakers'] ?? []) as $spk) {
352 $att_id = (int)($spk['image_id'] ?? 0);
353 $url = $att_id ? (wp_get_attachment_url($att_id) ?: '') : esc_url_raw($spk['image_url'] ?? '');
354 $name = sanitize_text_field($spk['name'] ?? '');
355 $title = sanitize_text_field($spk['title'] ?? '');
356 $bio = mb_substr(sanitize_textarea_field($spk['bio'] ?? ''), 0, 500);
357 if ($name === '' && $bio === '' && $url === '') continue; // skip empty rows
358 $speakers[] = ['image_id' => $att_id, 'image_url' => $url, 'name' => $name, 'title' => $title, 'bio' => $bio];
359 }
360 return ['speakers' => $speakers];
361
362 case 'image':
363 $att_id = (int)($data['attachment_id'] ?? 0);
364 $url = $att_id ? (wp_get_attachment_url($att_id) ?: '') : esc_url_raw($data['url'] ?? '');
365 $size = in_array(($data['size'] ?? 'full'), ['full', 'original', 'custom'], true) ? $data['size'] : 'full';
366 $unit = (($data['width_unit'] ?? 'px') === '%') ? '%' : 'px';
367 return [
368 'attachment_id' => $att_id,
369 'url' => $url,
370 'caption' => sanitize_text_field($data['caption'] ?? ''),
371 'size' => $size,
372 'width' => max(0, (int)($data['width'] ?? 0)),
373 'width_unit' => $unit,
374 'lightbox' => !empty($data['lightbox']),
375 ];
376
377 case 'video':
378 // provider: 'oembed' (YouTube/Vimeo etc.) or 'file' (self-hosted MP4 from media library).
379 $provider = ($data['provider'] ?? 'oembed') === 'file' ? 'file' : 'oembed';
380 $att_id = (int)($data['attachment_id'] ?? 0);
381 if ($provider === 'file') {
382 $url = $att_id ? (wp_get_attachment_url($att_id) ?: '') : esc_url_raw($data['url'] ?? '');
383 } else {
384 $url = esc_url_raw($data['url'] ?? '');
385 }
386 return [
387 'provider' => $provider,
388 'url' => $url,
389 'attachment_id' => $att_id,
390 // embed_html is generated server-side at response time (REST prepareContent), never stored.
391 ];
392
393 default:
394 return [];
395 }
396 }
397 }
398