PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 3.7.1
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v3.7.1
4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / admin / controllers / wizard / class-wizard-controller.php
superb-blocks / src / admin / controllers / wizard Last commit date
class-wizard-controller.php 5 months ago class-wizard-restorationpoint-controller.php 5 months ago class-wizard-template-preview-controller.php 5 months ago
class-wizard-controller.php
524 lines
1 <?php
2
3 namespace SuperbAddons\Admin\Controllers\Wizard;
4
5 use Exception;
6 use SuperbAddons\Admin\Controllers\DashboardController;
7 use SuperbAddons\Config\Capabilities;
8 use SuperbAddons\Data\Controllers\LogController;
9 use SuperbAddons\Data\Controllers\RestController;
10 use SuperbAddons\Data\Utils\ThemeInstaller;
11 use SuperbAddons\Data\Utils\ThemeInstallerException;
12 use SuperbAddons\Data\Utils\Wizard\AddonsPageTemplateUtil;
13 use SuperbAddons\Data\Utils\Wizard\WizardActionParameter;
14 use SuperbAddons\Data\Utils\Wizard\WizardException;
15 use SuperbAddons\Data\Utils\Wizard\WizardItemTypes;
16 use SuperbAddons\Data\Utils\Wizard\WizardMenuCreator;
17 use SuperbAddons\Data\Utils\Wizard\WizardPageCreator;
18 use SuperbAddons\Data\Utils\Wizard\WizardPartCreator;
19 use SuperbAddons\Data\Utils\Wizard\WizardStageTypes;
20 use SuperbAddons\Data\Utils\Wizard\WizardStageUtil;
21 use SuperbAddons\Gutenberg\Controllers\GutenbergController;
22 use WP_Error;
23 use WP_REST_Server;
24
25 defined('ABSPATH') || exit();
26
27 class WizardController
28 {
29 const WIZARD_ROUTE = '/wizard';
30 const ACTION_QUERY_PARAM = 'superbaddons-wizard-action';
31 const COMPLETED_QUERY_PARAM = 'superbaddons-wizard-completed';
32 const RECOMMENDER_TRANSIENT = 'superbaddons_wizard_recommender_transient';
33 const WOOCOMMERCE_TRANSIENT = 'superbaddons_wizard_woocommerce_transient';
34
35
36 public static function Initialize()
37 {
38 self::InitializeWizardRecommenderSwitchAction();
39 self::InitializeTemplateWizardEndpoints();
40 if (!GutenbergController::is_block_theme()) {
41 return;
42 }
43
44 self::InitializeWizardPageTemplates();
45 WizardTemplatePreviewController::InitializeTemplatePreview();
46 }
47
48 private static function GetTemplatePartObject($slug, $is_file_template = false)
49 {
50 switch ($slug) {
51 case 'header':
52 return AddonsPageTemplateUtil::GetAddonsHeaderTemplatePartObject($is_file_template);
53 case 'footer':
54 return AddonsPageTemplateUtil::GetAddonsFooterTemplatePartObject($is_file_template);
55 default:
56 return null;
57 }
58 }
59
60 private static function AddTemplatePartFallback($slug, $area)
61 {
62 add_filter('get_block_templates', function ($query_result, $query, $template_type) use ($slug, $area) {
63 if ($template_type !== WizardItemTypes::WP_TEMPLATE_PART) {
64 return $query_result;
65 }
66 if (
67 !empty($query) &&
68 (isset($query['slug__in']) && !in_array($slug, $query['slug__in'])) ||
69 (isset($query['slug__not_in']) && in_array($slug, $query['slug__not_in']))
70 ) {
71 return $query_result;
72 }
73
74 // Check query area, if set, must match
75 if (isset($query['area']) && $query['area'] !== $area) {
76 return $query_result;
77 }
78
79 // Only add fallback if no template exists
80 foreach ($query_result as $template) {
81 if ($template->slug === $slug) {
82 // Template exists, return original result
83 return $query_result;
84 }
85 }
86
87 // No template found, add our fallback
88 $query_result[] = self::GetTemplatePartObject($slug);
89
90 return $query_result;
91 }, 99, 3);
92
93 add_filter('get_block_file_template', function ($block_template, $id, $template_type) use ($slug) {
94 if ($template_type !== WizardItemTypes::WP_TEMPLATE_PART) {
95 return $block_template;
96 }
97
98 if ($id === get_stylesheet() . '//' . $slug) {
99 if ($block_template !== null && $block_template->source !== AddonsPageTemplateUtil::PLUGIN_SLUG) {
100 return $block_template;
101 }
102
103 // No template found, return our fallback if id matches
104 return self::GetTemplatePartObject($slug, true);
105 }
106
107 return $block_template;
108 }, 99, 3);
109 }
110
111 private static function InitializeWizardPageTemplates()
112 {
113 add_filter('get_block_templates', function ($query_result, $query, $template_type) {
114 if ($template_type !== WizardItemTypes::WP_TEMPLATE) {
115 return $query_result;
116 }
117
118 if (
119 !empty($query) &&
120 (isset($query['slug__in']) && !in_array(AddonsPageTemplateUtil::TEMPLATE_ID, $query['slug__in'])) ||
121 (isset($query['slug__not_in']) && in_array(AddonsPageTemplateUtil::TEMPLATE_ID, $query['slug__not_in']))
122 ) {
123 return $query_result;
124 }
125
126 $template = AddonsPageTemplateUtil::GetAddonsPageBlockTemplateObject();
127
128 $query_result[] = $template;
129
130 return $query_result;
131 }, 10, 3);
132
133 add_filter('get_block_file_template', function ($block_template, $id, $template_type) {
134 if ($template_type !== WizardItemTypes::WP_TEMPLATE) {
135 return $block_template;
136 }
137
138 if ($id === get_stylesheet() . "//" . AddonsPageTemplateUtil::TEMPLATE_ID || $id === AddonsPageTemplateUtil::PLUGIN_SLUG . '//' . AddonsPageTemplateUtil::TEMPLATE_ID) {
139 return AddonsPageTemplateUtil::GetAddonsPageBlockTemplateObject();
140 }
141
142 return $block_template;
143 }, 10, 3);
144
145 self::AddTemplatePartFallback('header', 'header');
146 self::AddTemplatePartFallback('footer', 'footer');
147 }
148
149 private static function InitializeWizardRecommenderSwitchAction()
150 {
151 // Add action to the switch theme hook
152 add_action('switch_theme', function () {
153 self::MaybeSetWizardRecommenderTransient();
154 });
155
156 // Check if WooCommerce is active
157 add_action('activated_plugin', function ($plugin) {
158 if ($plugin !== 'woocommerce/woocommerce.php') {
159 return;
160 }
161 self::MaybeSetWizardWooCommerceTransient();
162 });
163
164 add_action('deactivated_plugin', function ($plugin) {
165 if ($plugin !== 'woocommerce/woocommerce.php') {
166 return;
167 }
168 self::RemoveWizardWooCommerceTransient();
169 });
170 }
171
172 public static function MaybeSetWizardRecommenderTransient()
173 {
174 if (!GutenbergController::is_block_theme()) {
175 self::RemoveWizardRecommenderTransient();
176 self::RemoveWizardWooCommerceTransient();
177 return;
178 }
179 $current_theme = get_stylesheet();
180 $completed_themes = get_option('superbaddons_wizard_completed_themes', []);
181 if (in_array($current_theme, $completed_themes)) {
182 self::RemoveWizardRecommenderTransient();
183 return;
184 }
185 set_transient(self::RECOMMENDER_TRANSIENT, true, MONTH_IN_SECONDS);
186 self::MaybeSetWizardWooCommerceTransient();
187 }
188
189 public static function MaybeSetWizardWooCommerceTransient()
190 {
191 if (is_plugin_active('woocommerce/woocommerce.php')) {
192 set_transient(self::WOOCOMMERCE_TRANSIENT, true, MONTH_IN_SECONDS);
193 } else {
194 self::RemoveWizardWooCommerceTransient();
195 }
196 }
197
198 public static function RemoveWizardRecommenderTransient()
199 {
200 return delete_transient(self::RECOMMENDER_TRANSIENT);
201 }
202
203 public static function GetWizardRecommenderTransient()
204 {
205 return get_transient(self::RECOMMENDER_TRANSIENT);
206 }
207
208 public static function GetWizardWoocommerceTransient()
209 {
210 return get_transient(self::WOOCOMMERCE_TRANSIENT);
211 }
212
213 public static function RemoveWizardWooCommerceTransient()
214 {
215 return delete_transient(self::WOOCOMMERCE_TRANSIENT);
216 }
217
218 private static function SetWizardPartPreviewTransient($preview_data)
219 {
220 $user_id = get_current_user_id();
221 $transient = get_transient(WizardTemplatePreviewController::TEMPLATE_PART_PREVIEW_TRANSIENT);
222 if (!$transient || !is_array($transient)) {
223 $transient = [];
224 }
225 $transient[$user_id] = $preview_data;
226 return set_transient(WizardTemplatePreviewController::TEMPLATE_PART_PREVIEW_TRANSIENT, $transient, DAY_IN_SECONDS);
227 }
228
229 public static function GetPartPreviewTransient()
230 {
231 $user_id = get_current_user_id();
232 $transient = get_transient(WizardTemplatePreviewController::TEMPLATE_PART_PREVIEW_TRANSIENT);
233 return isset($transient[$user_id]) ? $transient[$user_id] : false;
234 }
235
236 public static function RemoveWizardPartPreviewTransient()
237 {
238 return delete_transient(self::RECOMMENDER_TRANSIENT);
239 }
240
241 public static function GetRecommendedBlockThemes()
242 {
243 if (!function_exists('themes_api')) {
244 require_once(ABSPATH . 'wp-admin/includes/theme.php');
245 }
246
247 return themes_api(
248 "query_themes",
249 array(
250 "author" => "superbaddons",
251 "per_page" => 24,
252 "browse" => "popular",
253 "fields" => array("name" => true, "slug" => true, "screenshot_url" => true)
254 )
255 );
256 }
257
258 public static function GetWizardURL($action)
259 {
260 return add_query_arg(
261 array(
262 'page' => DashboardController::PAGE_WIZARD,
263 self::ACTION_QUERY_PARAM => $action,
264 ),
265 admin_url("admin.php")
266 );
267 }
268
269 public static function GetWizardCompleteURL($wizardType)
270 {
271 return add_query_arg(
272 array(
273 'page' => DashboardController::PAGE_WIZARD,
274 self::ACTION_QUERY_PARAM => WizardActionParameter::COMPLETE,
275 self::COMPLETED_QUERY_PARAM => $wizardType
276 ),
277 admin_url("admin.php")
278 );
279 }
280
281 public static function GetCompletedWizardType()
282 {
283 // determine the type of wizard page that was completed.
284 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
285 return isset($_GET[self::COMPLETED_QUERY_PARAM]) ? sanitize_text_field(wp_unslash($_GET[self::COMPLETED_QUERY_PARAM])) : false;
286 }
287
288 private static function isAllowedAction($action)
289 {
290 if (!isset($action)) {
291 return false;
292 }
293 $allowed_actions = [WizardActionParameter::ADD_NEW_PAGES, WizardActionParameter::HEADER_FOOTER, WizardActionParameter::WOOCOMMERCE_HEADER, WizardActionParameter::THEME_DESIGNER, WizardActionParameter::RESTORE];
294 return in_array($action, $allowed_actions);
295 }
296
297 public static function IsWizardStages()
298 {
299 // determine if we are on a wizard stage page.
300 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
301 if (!isset($_GET[self::ACTION_QUERY_PARAM]) || !self::isAllowedAction(sanitize_text_field(wp_unslash($_GET[self::ACTION_QUERY_PARAM])))) {
302 return false;
303 }
304
305 return true;
306 }
307
308 public static function IsCompleteScreen()
309 {
310 // determine if we are on the wizard complete screen.
311 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
312 return isset($_GET[self::ACTION_QUERY_PARAM]) && sanitize_text_field(wp_unslash($_GET[self::ACTION_QUERY_PARAM])) === WizardActionParameter::COMPLETE;
313 }
314
315 public static function ThemeHasCompletedWizard()
316 {
317 $current_theme = get_stylesheet();
318 $completed_themes = get_option('superbaddons_wizard_completed_themes', []);
319 return in_array($current_theme, $completed_themes);
320 }
321
322 public static function CompleteWizard()
323 {
324 $current_theme = get_stylesheet();
325 $completed_themes = get_option('superbaddons_wizard_completed_themes', []);
326 if (!in_array($current_theme, $completed_themes)) {
327 $completed_themes[] = $current_theme;
328 update_option('superbaddons_wizard_completed_themes', $completed_themes, false);
329 }
330 self::RemoveWizardRecommenderTransient();
331 }
332
333 public static function CompleteWooCommerceWizard()
334 {
335 self::RemoveWizardWooCommerceTransient();
336 }
337
338 private static function InitializeTemplateWizardEndpoints()
339 {
340 RestController::AddRoute(self::WIZARD_ROUTE, array(
341 'methods' => WP_REST_Server::EDITABLE,
342 'permission_callback' => array(self::class, 'TemplateWizardCallbackPermissionCheck'),
343 'callback' => array(self::class, 'TemplateWizardCallback'),
344 ));
345 }
346
347 public static function TemplateWizardCallbackPermissionCheck()
348 {
349 // Restrict endpoint to only users who have the proper capability.
350 if (!current_user_can(Capabilities::ADMIN)) {
351 return new WP_Error('rest_forbidden', esc_html__('Unauthorized. Please check user permissions.', "superb-blocks"), array('status' => 401));
352 }
353
354 return true;
355 }
356
357 public static function TemplateWizardCallback($request)
358 {
359 if (!isset($request['action'])) {
360 return new \WP_Error('bad_request_plugin', 'Bad Plugin Request', array('status' => 400));
361 }
362 switch ($request['action']) {
363 case 'switchtheme':
364 return self::SwitchThemeCallback($request);
365 case 'create':
366 if (!GutenbergController::is_block_theme()) {
367 return new \WP_Error('bad_request_plugin', 'Bad Plugin Request', array('status' => 400));
368 }
369 return self::TemplateWizardCreateCallback($request);
370 case 'headerfooterpreview':
371 if (!GutenbergController::is_block_theme()) {
372 return new \WP_Error('bad_request_plugin', 'Bad Plugin Request', array('status' => 400));
373 }
374 return self::HeaderFooterPreviewCallback($request);
375 default:
376 return new \WP_Error('bad_request_plugin', 'Bad Plugin Request', array('status' => 400));
377 }
378 }
379
380 private static function SwitchThemeCallback($request)
381 {
382 try {
383 $theme_slug = sanitize_text_field($request['theme']);
384 if (empty($theme_slug)) {
385 return rest_ensure_response(['success' => false, 'text' => esc_html__("Couldn't find selected theme.", "superb-blocks")]);
386 }
387
388 $themes_api = self::GetRecommendedBlockThemes();
389
390 $accepted_theme = false;
391 foreach ($themes_api->themes as $theme) {
392 if ($theme->slug === $theme_slug) {
393 $accepted_theme = true;
394 break;
395 }
396 }
397
398 if (!$accepted_theme) {
399 throw new ThemeInstallerException(esc_html__("Selected theme is not recognized. Please contact support for assistance.", "superb-blocks"));
400 }
401
402 $installed = ThemeInstaller::Install($theme_slug);
403 if (!$installed) {
404 throw new ThemeInstallerException(esc_html__("The selected theme could not be installed. Please try again or contact support for assistance.", "superb-blocks"));
405 }
406
407 return rest_ensure_response(['success' => $installed]);
408 } catch (ThemeInstallerException $tex) {
409 return rest_ensure_response(['success' => false, 'text' => $tex->getMessage()]);
410 } catch (Exception $ex) {
411 LogController::HandleException($ex);
412 return new \WP_Error('internal_error_plugin', 'Internal Plugin Error', array('status' => 500));
413 }
414 }
415
416 private static function TemplateWizardCreateCallback($request)
417 {
418 try {
419 if (!isset($request['selection_data'])) {
420 return rest_ensure_response(['success' => false, 'text' => esc_html__("Something went wrong. The process could not start.", "superb-blocks")]);
421 }
422
423 $selection_data = json_decode($request['selection_data'], true);
424 if (!$selection_data) {
425 return rest_ensure_response(['success' => false, 'text' => esc_html__("Something went wrong. The process could not start.", "superb-blocks")]);
426 }
427
428 $stageUtil = new WizardStageUtil($request['wizardType']);
429 if (!self::ValidateSelectionData($selection_data, $stageUtil)) {
430 return rest_ensure_response(['success' => false, 'text' => esc_html__("Something went wrong. Please double-check that all steps have been correctly completed.", "superb-blocks")]);
431 }
432
433 WizardPartCreator::CreateTemplateParts($selection_data, $stageUtil);
434
435 if ($stageUtil->HasPageStages()) {
436 $menu_items = WizardPageCreator::CreateTemplatePages($selection_data, $stageUtil);
437
438 if (empty($menu_items)) {
439 // If the wizard is in restore mode, we can continue even if no menu items are created.
440 if (!$stageUtil->IsRestore()) {
441 // If the wizard is not in restore mode, we need to throw an error.
442 return rest_ensure_response(['success' => false, 'text' => esc_html__("Something went wrong. Templates and/or pages were not able to be properly processed.", "superb-blocks")]);
443 }
444 }
445
446 WizardMenuCreator::MaybeUpdateMenu($selection_data, $menu_items);
447 }
448
449 if ($stageUtil->GetType() === WizardActionParameter::THEME_DESIGNER) {
450 WizardController::CompleteWizard();
451 } elseif ($stageUtil->GetType() === WizardActionParameter::WOOCOMMERCE_HEADER) {
452 WizardController::CompleteWooCommerceWizard();
453 }
454
455 return rest_ensure_response(['success' => true]);
456 } catch (WizardException $wex) {
457 return rest_ensure_response(['success' => false, 'text' => $wex->getMessage()]);
458 } catch (Exception $ex) {
459 LogController::HandleException($ex);
460 return new WP_Error('internal_error_plugin', 'Internal Plugin Error', array('status' => 500));
461 }
462 }
463
464 private static function ValidateSelectionData($selection_data, $stageUtil)
465 {
466 if (!self::isAllowedAction($stageUtil->GetType())) {
467 return false;
468 }
469
470 foreach ($stageUtil->GetStages() as $stage_type) {
471 if (!isset($selection_data[$stage_type])) {
472 if ($stageUtil->GetType() === WizardActionParameter::RESTORE) {
473 // If stages are missing, but the action is restore, then we can continue as the restore action can have any number of stages.
474 continue;
475 }
476 if ($stage_type === WizardStageTypes::NAVIGATION_MENU_STAGE && !$stageUtil->GetMenuAvailability()['available']) {
477 // If the navigation stage is missing and navigation menu is not available, then we can continue.
478 continue;
479 }
480 return false;
481 }
482
483 $stage_selections = $selection_data[$stage_type];
484
485 if (!is_array($stage_selections)) {
486 return false;
487 }
488
489 if (empty($stage_selections)) {
490 continue;
491 }
492
493 if (isset($stage_selections[0]['isChanged']) && !boolval($stage_selections[0]['isChanged'])) {
494 continue;
495 }
496
497 foreach ($stage_selections as $selection) {
498 if (!isset($selection['slug']) || !isset($selection['title'])) {
499 return false;
500 }
501 }
502 }
503
504 return true;
505 }
506
507 public static function HeaderFooterPreviewCallback($request)
508 {
509 $preview_transient = [];
510
511 if (isset($request['header'])) {
512 $preview_transient['header'] = sanitize_text_field($request['header']);
513 }
514
515 if (isset($request['footer'])) {
516 $preview_transient['footer'] = sanitize_text_field($request['footer']);
517 }
518
519 self::SetWizardPartPreviewTransient($preview_transient);
520
521 return rest_ensure_response(['success' => true]);
522 }
523 }
524