PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.86
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.86
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / helpers / Woo_Builder / My_Account_Manager.php

My_Account_Manager.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.86, at includes/helpers/Woo_Builder/My_Account_Manager.php

540 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * My Account endpoints manager.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons\Woo_Builder;
9
10 use Elementor\Plugin as Elementor_Plugin;
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 /**
17 * Handles endpoint registration and template routing for My Account.
18 */
19 class My_Account_Manager
20 {
21 /**
22 * Option name for endpoints config.
23 */
24 private const OPTION_NAME = 'ka_woo_account_endpoints';
25
26 /**
27 * Cached endpoints.
28 *
29 * @var array<string,array<string,mixed>>
30 */
31 private array $endpoints = [];
32
33 /**
34 * Guards against re-entering get_endpoints() from our own menu filter.
35 *
36 * @var bool
37 */
38 private bool $resolving_endpoints = false;
39
40 /**
41 * Constructor.
42 */
43 public function __construct()
44 {
45 add_action('init', [$this, 'register_custom_endpoints']);
46 add_filter('woocommerce_account_menu_items', [$this, 'filter_menu_items'], 99);
47 add_filter('woocommerce_get_endpoint_url', [$this, 'filter_logout_url'], 10, 4);
48 add_action('template_redirect', [$this, 'maybe_render_endpoint_template'], 1);
49 // Use priority 15 to ensure the parent menu exists before adding this submenu
50 add_action('admin_menu', [$this, 'register_admin_page'], 15);
51 add_action('admin_init', [$this, 'register_settings']);
52 add_action('admin_post_king_addons_myaccount_endpoints_save', [$this, 'handle_admin_save']);
53 }
54
55 /**
56 * Register custom endpoints (rewrite).
57 *
58 * @return void
59 */
60 public function register_custom_endpoints(): void
61 {
62 foreach ($this->get_endpoints() as $slug => $data) {
63 if (!empty($data['is_custom']) && !empty($data['enabled'])) {
64 add_rewrite_endpoint($slug, EP_ROOT | EP_PAGES);
65 }
66 }
67 }
68
69 /**
70 * Filter My Account menu items based on config.
71 *
72 * @param array<string,string> $items Default menu.
73 *
74 * @return array<string,string>
75 */
76 public function filter_menu_items(array $items): array
77 {
78 $endpoints = $this->get_endpoints();
79
80 // get_endpoints() is what triggered this filter in the first place, so
81 // on the way in it has nothing to offer yet. Returning its empty list
82 // here would hand WooCommerce an empty account menu.
83 if (empty($endpoints)) {
84 return $items;
85 }
86
87 $output = [];
88
89 foreach ($endpoints as $slug => $data) {
90 if (empty($data['enabled'])) {
91 continue;
92 }
93
94 if (!$this->is_allowed_for_user($data)) {
95 continue;
96 }
97
98 $label = $data['label'] ?? ($items[$slug] ?? ucfirst(str_replace('-', ' ', $slug)));
99 $position = isset($data['position']) ? (int) $data['position'] : 20;
100
101 $output[sprintf('%05d:%s', $position, $slug)] = [$slug, $label];
102 }
103
104 ksort($output, SORT_NATURAL);
105
106 $sorted = [];
107 foreach ($output as $item) {
108 [$slug, $label] = $item;
109 $sorted[$slug] = $label;
110 }
111
112 return $sorted;
113 }
114
115 /**
116 * Render endpoint template if mapped; otherwise fall back.
117 *
118 * @return void
119 */
120 public function maybe_render_endpoint_template(): void
121 {
122 if (!function_exists('is_account_page') || !is_account_page()) {
123 return;
124 }
125 if (!class_exists(Elementor_Plugin::class)) {
126 return;
127 }
128
129 if (isset($_GET['ka_logout_confirm'])) {
130 $this->render_logout_confirm();
131 return;
132 }
133
134 $endpoints = $this->get_endpoints();
135 $current = $this->detect_current_endpoint(array_keys($endpoints));
136 if (!$current) {
137 return;
138 }
139
140 $config = $endpoints[$current] ?? [];
141 $template_id = isset($config['template_id']) ? (int) $config['template_id'] : 0;
142
143 if (!$this->is_allowed_for_user($config)) {
144 return;
145 }
146
147 if (!$template_id || !king_addons_can_use_pro()) {
148 return;
149 }
150
151 status_header(200);
152 nocache_headers();
153 echo '<div class="king-addons-woo-builder king-addons-woo-builder--my-account king-addons-woo-builder--endpoint-' . esc_attr($current) . '">';
154 echo Elementor_Plugin::$instance->frontend->get_builder_content_for_display($template_id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
155 echo '</div>';
156 exit;
157 }
158
159 /**
160 * Add confirm flag to logout URL if configured.
161 *
162 * @param string $url URL.
163 * @param string $endpoint Endpoint.
164 * @param string $value Value.
165 * @param string $permalink Permalink.
166 *
167 * @return string
168 */
169 public function filter_logout_url(string $url, string $endpoint, string $value, string $permalink): string
170 {
171 $endpoints = $this->get_endpoints();
172 if ('customer-logout' === $endpoint && !empty($endpoints['customer-logout']['confirm'])) {
173 $url = add_query_arg('ka_logout_confirm', '1', $url);
174 }
175 return $url;
176 }
177
178 /**
179 * Get endpoints config with defaults and filters.
180 *
181 * @return array<string,array<string,mixed>>
182 */
183 public function get_endpoints(): array
184 {
185 if (!empty($this->endpoints)) {
186 return $this->endpoints;
187 }
188
189 // wc_get_account_menu_items() applies woocommerce_account_menu_items,
190 // and filter_menu_items() below asks for the endpoints again - without
191 // this guard the pair recurse until PHP runs out of memory.
192 if ($this->resolving_endpoints) {
193 return [];
194 }
195 $this->resolving_endpoints = true;
196
197 $defaults = wc_get_account_menu_items();
198 $config = get_option(self::OPTION_NAME, []);
199
200 $endpoints = [];
201 $order = 10;
202 foreach ($defaults as $slug => $label) {
203 $endpoints[$slug] = [
204 'label' => $label,
205 'enabled' => true,
206 // Was a fixed 20 for every item, so sorting fell back to the
207 // slug and rearranged WooCommerce's menu alphabetically.
208 'position' => $order,
209 'template_id' => 0,
210 'is_custom' => false,
211 'confirm' => false,
212 ];
213 $order += 10;
214 }
215
216 // Merge saved config.
217 if (is_array($config)) {
218 foreach ($config as $slug => $data) {
219 $endpoints[$slug] = array_merge($endpoints[$slug] ?? [], $data);
220 }
221 }
222
223 /**
224 * Filter endpoints (add custom, adjust existing).
225 *
226 * @param array<string,array<string,mixed>> $endpoints Endpoints config.
227 */
228 $endpoints = apply_filters('king_addons/my_account/endpoints', $endpoints);
229
230 $this->endpoints = $endpoints;
231 $this->resolving_endpoints = false;
232
233 return $this->endpoints;
234 }
235
236 /**
237 * Detect current endpoint slug.
238 *
239 * @param array<int,string> $endpoints Endpoint slugs.
240 *
241 * @return string|null
242 */
243 private function detect_current_endpoint(array $endpoints): ?string
244 {
245 global $wp;
246 foreach ($endpoints as $endpoint) {
247 if (isset($wp->query_vars[$endpoint])) {
248 return $endpoint;
249 }
250 }
251 return null;
252 }
253
254 /**
255 * Register admin settings page.
256 *
257 * @return void
258 */
259 public function register_admin_page(): void
260 {
261 add_submenu_page(
262 king_addons_woo_admin_parent_slug(),
263 esc_html__('My Account Endpoints', 'king-addons'),
264 esc_html__('My Account Endpoints', 'king-addons'),
265 'manage_options',
266 'king-addons-myaccount-endpoints',
267 [$this, 'render_admin_page']
268 );
269 }
270
271 /**
272 * Register settings.
273 *
274 * @return void
275 */
276 public function register_settings(): void
277 {
278 register_setting(
279 'king_addons_myaccount_endpoints',
280 self::OPTION_NAME,
281 [
282 'type' => 'array',
283 'description' => 'King Addons My Account endpoints config',
284 'sanitize_callback' => [$this, 'sanitize_endpoints_option'],
285 'default' => [],
286 ]
287 );
288 }
289
290 /**
291 * Sanitize endpoints option (expects JSON string or array).
292 *
293 * @param mixed $value Raw value.
294 *
295 * @return array<string,mixed>
296 */
297 public function sanitize_endpoints_option($value): array
298 {
299 if (is_string($value)) {
300 $decoded = json_decode($value, true);
301 if (is_array($decoded)) {
302 return $decoded;
303 }
304 return [];
305 }
306 return is_array($value) ? $value : [];
307 }
308
309 /**
310 * Render admin page.
311 *
312 * @return void
313 */
314 public function render_admin_page(): void
315 {
316 if (!current_user_can('manage_options')) {
317 return;
318 }
319
320 $endpoints = $this->get_endpoints();
321 $is_pro = function_exists('king_addons_can_use_pro') && king_addons_can_use_pro();
322 $templates = $this->get_account_templates();
323 $roles = $this->get_editable_roles();
324
325 require KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/templates/endpoints-admin-page.php';
326 }
327
328 /**
329 * Elementor templates that can stand in for an endpoint's content.
330 *
331 * @return array<int,string>
332 */
333 private function get_account_templates(): array
334 {
335 $templates = [];
336
337 $posts = get_posts([
338 'post_type' => 'elementor_library',
339 'post_status' => 'publish',
340 'posts_per_page' => 100,
341 'orderby' => 'title',
342 'order' => 'ASC',
343 'suppress_filters' => false,
344 ]);
345
346 foreach ($posts as $post) {
347 $templates[(int) $post->ID] = $post->post_title !== ''
348 ? $post->post_title
349 : sprintf('#%d', (int) $post->ID);
350 }
351
352 return $templates;
353 }
354
355 /**
356 * Roles an endpoint can be limited to.
357 *
358 * @return array<string,string>
359 */
360 private function get_editable_roles(): array
361 {
362 if (!function_exists('get_editable_roles')) {
363 require_once ABSPATH . 'wp-admin/includes/user.php';
364 }
365
366 $roles = [];
367
368 foreach (get_editable_roles() as $slug => $role) {
369 $roles[(string) $slug] = translate_user_role($role['name'] ?? $slug);
370 }
371
372 return $roles;
373 }
374
375 /**
376 * Save the endpoints screen.
377 *
378 * @return void
379 */
380 public function handle_admin_save(): void
381 {
382 if (!current_user_can('manage_options')) {
383 wp_die(esc_html__('You are not allowed to do this.', 'king-addons'));
384 }
385
386 check_admin_referer('king_addons_myaccount_endpoints_save');
387
388 $raw = isset($_POST['ka_endpoint']) && is_array($_POST['ka_endpoint'])
389 ? wp_unslash($_POST['ka_endpoint']) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
390 : [];
391
392 $is_pro = function_exists('king_addons_can_use_pro') && king_addons_can_use_pro();
393 $known_roles = array_keys($this->get_editable_roles());
394 $existing = get_option(self::OPTION_NAME, []);
395 $existing = is_array($existing) ? $existing : [];
396
397 $clean = [];
398
399 foreach ($raw as $slug => $data) {
400 $slug = sanitize_key((string) $slug);
401 if ('' === $slug || !is_array($data)) {
402 continue;
403 }
404
405 $was_custom = !empty($existing[$slug]['is_custom']);
406
407 $clean[$slug] = [
408 'label' => sanitize_text_field((string) ($data['label'] ?? '')),
409 'enabled' => !empty($data['enabled']),
410 'position' => max(0, min(9999, (int) ($data['position'] ?? 10))),
411 'template_id' => $is_pro ? absint($data['template_id'] ?? 0) : 0,
412 'is_custom' => $was_custom,
413 'confirm' => !empty($data['confirm']),
414 'roles' => array_values(array_intersect(
415 array_map('sanitize_key', (array) ($data['roles'] ?? [])),
416 $known_roles
417 )),
418 ];
419 }
420
421 // A new custom endpoint, if one was filled in.
422 $new_slug = isset($_POST['ka_new_endpoint_slug'])
423 ? sanitize_title(wp_unslash($_POST['ka_new_endpoint_slug'])) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
424 : '';
425 $new_label = isset($_POST['ka_new_endpoint_label'])
426 ? sanitize_text_field(wp_unslash($_POST['ka_new_endpoint_label'])) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
427 : '';
428
429 $notice = 'saved';
430
431 if ('' !== $new_slug) {
432 if (!$is_pro) {
433 $notice = 'pro';
434 } elseif (isset($clean[$new_slug])) {
435 $notice = 'exists';
436 } else {
437 $clean[$new_slug] = [
438 'label' => '' !== $new_label ? $new_label : ucfirst(str_replace('-', ' ', $new_slug)),
439 'enabled' => true,
440 'position' => 100,
441 'template_id' => 0,
442 'is_custom' => true,
443 'confirm' => false,
444 'roles' => [],
445 ];
446 $notice = 'added';
447 }
448 }
449
450 // Removing a custom endpoint. Built-in ones are switched off, not deleted.
451 $remove = isset($_POST['ka_remove_endpoint'])
452 ? sanitize_key(wp_unslash($_POST['ka_remove_endpoint'])) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
453 : '';
454
455 if ('' !== $remove && !empty($clean[$remove]['is_custom'])) {
456 unset($clean[$remove]);
457 $notice = 'removed';
458 }
459
460 update_option(self::OPTION_NAME, $clean);
461
462 // Custom endpoints add rewrite rules, which only take effect once the
463 // rules are rebuilt.
464 flush_rewrite_rules(false);
465
466 wp_safe_redirect(add_query_arg(
467 ['page' => 'king-addons-myaccount-endpoints', 'ka-notice' => $notice],
468 admin_url('admin.php')
469 ));
470 exit;
471 }
472
473 /**
474 * Render logout confirmation page.
475 *
476 * @return void
477 */
478 private function render_logout_confirm(): void
479 {
480 if (!is_user_logged_in()) {
481 return;
482 }
483
484 $logout_url = wp_logout_url(wc_get_page_permalink('myaccount'));
485 $cancel_url = remove_query_arg('ka_logout_confirm');
486
487 status_header(200);
488 nocache_headers();
489
490 echo '<style>';
491 echo '.ka-logout-confirm{min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f8fafc;padding:24px;box-sizing:border-box;}';
492 echo '.ka-logout-confirm__card{max-width:420px;width:100%;background:#fff;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 10px 30px rgba(15,23,42,0.08);padding:28px;text-align:center;}';
493 echo '.ka-logout-confirm__title{margin:0 0 12px;font-size:24px;font-weight:700;color:#0f172a;}';
494 echo '.ka-logout-confirm__desc{margin:0 0 20px;font-size:15px;color:#334155;}';
495 echo '.ka-logout-confirm__actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}';
496 echo '.ka-logout-confirm__btn{display:inline-flex;align-items:center;justify-content:center;padding:10px 18px;border-radius:8px;border:1px solid transparent;font-weight:600;text-decoration:none;transition:all .15s ease;min-width:120px;}';
497 echo '.ka-logout-confirm__btn--primary{background:#2563eb;border-color:#2563eb;color:#fff;}';
498 echo '.ka-logout-confirm__btn--primary:hover{background:#1d4ed8;border-color:#1d4ed8;}';
499 echo '.ka-logout-confirm__btn--ghost{background:#fff;border-color:#cbd5e1;color:#0f172a;}';
500 echo '.ka-logout-confirm__btn--ghost:hover{border-color:#94a3b8;}';
501 echo '</style>';
502
503 echo '<div class="ka-logout-confirm">';
504 echo '<div class="ka-logout-confirm__card">';
505 echo '<h2 class="ka-logout-confirm__title">' . esc_html__('Confirm logout', 'king-addons') . '</h2>';
506 echo '<p class="ka-logout-confirm__desc">' . esc_html__('Are you sure you want to log out?', 'king-addons') . '</p>';
507 echo '<div class="ka-logout-confirm__actions">';
508 echo '<a class="ka-logout-confirm__btn ka-logout-confirm__btn--primary" href="' . esc_url($logout_url) . '">' . esc_html__('Yes, log out', 'king-addons') . '</a>';
509 echo '<a class="ka-logout-confirm__btn ka-logout-confirm__btn--ghost" href="' . esc_url($cancel_url) . '">' . esc_html__('Cancel', 'king-addons') . '</a>';
510 echo '</div>';
511 echo '</div>';
512 echo '</div>';
513 exit;
514 }
515
516 /**
517 * Check role-based access.
518 *
519 * @param array<string,mixed> $data Endpoint data.
520 *
521 * @return bool
522 */
523 private function is_allowed_for_user(array $data): bool
524 {
525 if (empty($data['roles']) || !is_array($data['roles'])) {
526 return true;
527 }
528 $user = wp_get_current_user();
529 if (!$user || empty($user->roles)) {
530 return false;
531 }
532 return (bool) array_intersect($user->roles, $data['roles']);
533 }
534 }
535
536
537
538
539
540