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