PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-setup-wizard / step-publish-integration / class-wpbc-setup-wizard-publish-integration-module.php

class-wpbc-setup-wizard-publish-integration-module.php in Booking Calendar 11.9, at includes/page-setup-wizard/step-publish-integration/class-wpbc-setup-wizard-publish-integration-module.php

425 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Setup Wizard module adapter for progressive publishing choices.
4 *
5 * @package Booking Calendar
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Expose publishing configuration through the common Setup Wizard module contract.
14 */
15 final class WPBC_Setup_Wizard_Publish_Integration_Module implements WPBC_Setup_Wizard_Step_Module, WPBC_Setup_Wizard_Step_Values_Validator, WPBC_Setup_Wizard_Contextual_Values_Validator, WPBC_Setup_Wizard_Conditional_Step_Module {
16
17 /** @var WPBC_Setup_Wizard_Publish_Integration */
18 private $publish_integration;
19
20 /**
21 * Build the module around the publishing service.
22 *
23 * @param WPBC_Setup_Wizard_Publish_Integration|null $publish_integration Optional service for testing.
24 */
25 public function __construct( $publish_integration = null ) {
26 $this->publish_integration = $publish_integration instanceof WPBC_Setup_Wizard_Publish_Integration
27 ? $publish_integration
28 : new WPBC_Setup_Wizard_Publish_Integration();
29 }
30
31 /**
32 * Return the stable module identifier.
33 *
34 * @return string Stable step ID.
35 */
36 public function get_step_id() {
37 return 'publish_integration';
38 }
39
40 /**
41 * Return rail and template metadata.
42 *
43 * @return array{id:string,label:string,template:string,footer_note:string} Step definition.
44 */
45 public function get_step_definition() {
46 return array(
47 'id' => $this->get_step_id(),
48 'label' => __( 'Publish & integrate', 'booking' ),
49 'template' => 'step-publish-integration',
50 'footer_note' => __( 'Your publishing choice is saved when you continue. Back only navigates and does not undo a completed page update.', 'booking' ),
51 );
52 }
53
54 /**
55 * Return the server-owned template path.
56 *
57 * @return string Absolute template path.
58 */
59 public function get_template_path() {
60 return __DIR__ . '/template.php';
61 }
62
63 /**
64 * Return fields accepted by this step.
65 *
66 * @return string[] Ordered field allow-list.
67 */
68 public function get_field_names() {
69 return array(
70 'publish_destination',
71 'publish_page_title',
72 'publish_existing_page_id',
73 'publish_page_content',
74 );
75 }
76
77 /**
78 * Return fields required for a complete forward navigation request.
79 *
80 * Conditional meaning is enforced after all scalar fields are normalized.
81 * The existing-page ID uses zero as the safe non-selected placeholder.
82 *
83 * @return string[] Required field IDs.
84 */
85 public function get_required_fields() {
86 return array( 'publish_destination' );
87 }
88
89 /**
90 * Return safe initial suggestions.
91 *
92 * @return array<string,mixed> Initial draft values.
93 */
94 public function get_initial_values() {
95 return array(
96 'publish_destination' => $this->publish_integration->get_initial_destination(),
97 'publish_page_title' => '',
98 'publish_existing_page_id' => 0,
99 'publish_page_content' => WPBC_Setup_Wizard_Publish_Integration::CONTENT_RECOMMENDED,
100 );
101 }
102
103 /**
104 * Return earlier step values needed for journey-aware shortcode generation.
105 *
106 * @return string[] Dependency step IDs.
107 */
108 public function get_context_dependencies() {
109 return array( 'customer_journey', 'booking_form_template' );
110 }
111
112 /**
113 * Determine whether this step belongs to the current site route.
114 *
115 * @param array<string,mixed> $consumer_context Unused route context.
116 *
117 * @return bool False for live-demo websites.
118 */
119 public function is_available( array $consumer_context = array() ) {
120 unset( $consumer_context );
121
122 return $this->publish_integration->is_available();
123 }
124
125 /**
126 * Build the authorized presentation context for the page.
127 *
128 * @param array<string,mixed> $field_values Validated module values.
129 * @param array<string,mixed> $consumer_context Read-only prior-step values.
130 *
131 * @return array<string,mixed> Presentation context.
132 */
133 public function get_context( array $field_values, array $consumer_context = array() ) {
134 $customer_journey = $this->get_dependency_value( $consumer_context, 'customer_journey', 'customer_journey' );
135 $form_slug = $this->get_dependency_value( $consumer_context, 'booking_form_template', 'booking_form_template' );
136 $pages = $this->publish_integration->get_publishable_pages();
137 $destinations = $this->publish_integration->get_destination_options( $pages );
138 $content_options = $this->publish_integration->get_content_options( $customer_journey, $form_slug );
139 $insertion_help = $this->publish_integration->get_shortcode_insertion_help();
140 $content_ids = wp_list_pluck( $content_options, 'id' );
141 $recommended_id = $this->publish_integration->get_recommended_content_id( $customer_journey );
142 $destination_ids = array();
143
144 foreach ( $destinations as $destination ) {
145 if ( ! empty( $destination['is_enabled'] ) ) {
146 $destination_ids[] = (string) $destination['id'];
147 }
148 }
149
150 $selected_destination = isset( $field_values['publish_destination'] ) ? (string) $field_values['publish_destination'] : '';
151 if ( ! in_array( $selected_destination, $destination_ids, true ) ) {
152 $selected_destination = $this->publish_integration->get_initial_destination();
153 }
154
155 $selected_content = isset( $field_values['publish_page_content'] ) ? (string) $field_values['publish_page_content'] : '';
156 if ( WPBC_Setup_Wizard_Publish_Integration::CONTENT_RECOMMENDED === $selected_content || ! in_array( $selected_content, $content_ids, true ) ) {
157 $selected_content = $recommended_id;
158 }
159
160 $page_title = isset( $field_values['publish_page_title'] ) ? (string) $field_values['publish_page_title'] : '';
161 $has_unsaved_legacy_title = ! $this->has_saved_publish_result( $consumer_context )
162 && 'guided_appointment_flow' !== $customer_journey
163 && __( 'Book an appointment', 'booking' ) === $page_title;
164
165 if ( '' === trim( $page_title ) || $has_unsaved_legacy_title ) {
166 $page_title = $this->publish_integration->get_initial_page_title( $customer_journey );
167 }
168
169 $existing_page_id = isset( $field_values['publish_existing_page_id'] ) ? absint( $field_values['publish_existing_page_id'] ) : 0;
170 $selected_page = $this->find_page( $pages, $existing_page_id );
171 $selected_content_record = $this->find_content( $content_options, $selected_content );
172 $page_url = '';
173
174 if ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_CREATE_PAGE === $selected_destination ) {
175 $page_url = $this->publish_integration->get_new_page_url_preview( $page_title );
176 } elseif ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_EXISTING_PAGE === $selected_destination && ! empty( $selected_page['url'] ) ) {
177 $page_url = (string) $selected_page['url'];
178 }
179
180 return array(
181 'values' => array(
182 'publish_destination' => $selected_destination,
183 'publish_page_title' => $page_title,
184 'publish_existing_page_id' => $existing_page_id,
185 'publish_page_content' => $selected_content,
186 ),
187 'destinations' => $destinations,
188 'pages' => $pages,
189 'content_options' => $content_options,
190 'selected_content' => $selected_content_record,
191 'insertion_help' => $insertion_help,
192 'page_url' => $page_url,
193 'home_path' => $this->publish_integration->get_home_path(),
194 'customer_journey' => $customer_journey,
195 'has_created_page' => $this->has_created_page_result( $consumer_context ),
196 );
197 }
198
199 /**
200 * Validate one allow-listed field.
201 *
202 * @param string $field_id Stable field ID.
203 * @param mixed $raw_value Untrusted field value.
204 * @param bool $is_required Whether an empty value is invalid.
205 *
206 * @return mixed|WP_Error Normalized value or validation error.
207 */
208 public function validate_field( $field_id, $raw_value, $is_required ) {
209 switch ( $field_id ) {
210 case 'publish_destination':
211 return $this->publish_integration->validate_destination( $raw_value, $is_required );
212
213 case 'publish_page_title':
214 return $this->publish_integration->validate_page_title( $raw_value, $is_required );
215
216 case 'publish_existing_page_id':
217 return $this->publish_integration->validate_page_id( $raw_value );
218
219 case 'publish_page_content':
220 return $this->publish_integration->validate_content( $raw_value, $is_required );
221 }
222
223 return new WP_Error( 'wpbc_setup_wizard_publish_field_unknown', __( 'The publishing step received an unsupported field.', 'booking' ) );
224 }
225
226 /**
227 * Preserve normalized values before contextual validation.
228 *
229 * @param array<string,mixed> $validated_values Individually normalized fields.
230 *
231 * @return array<string,mixed> Unchanged normalized fields.
232 */
233 public function validate_values( array $validated_values ) {
234 return $validated_values;
235 }
236
237 /**
238 * Validate relationships to the selected journey and current WordPress pages.
239 *
240 * @param array<string,mixed> $validated_values Normalized current-step fields.
241 * @param array<string,mixed> $consumer_context Normalized dependency values.
242 *
243 * @return array<string,mixed>|WP_Error Validated values or field errors.
244 */
245 public function validate_values_with_context( array $validated_values, array $consumer_context ) {
246 $field_errors = array();
247 $destination = isset( $validated_values['publish_destination'] ) ? (string) $validated_values['publish_destination'] : '';
248 $customer_journey = $this->get_dependency_value( $consumer_context, 'customer_journey', 'customer_journey' );
249 $form_slug = $this->get_dependency_value( $consumer_context, 'booking_form_template', 'booking_form_template' );
250 $content_options = $this->publish_integration->get_content_options( $customer_journey, $form_slug );
251 $content_ids = wp_list_pluck( $content_options, 'id' );
252 $content_id = isset( $validated_values['publish_page_content'] ) ? (string) $validated_values['publish_page_content'] : '';
253
254 if ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_LATER !== $destination && '' === $content_id ) {
255 $field_errors['publish_page_content'] = __( 'Choose the booking experience to display.', 'booking' );
256 } elseif ( WPBC_Setup_Wizard_Publish_Integration::CONTENT_RECOMMENDED === $content_id ) {
257 $validated_values['publish_page_content'] = $this->publish_integration->get_recommended_content_id( $customer_journey );
258 } elseif ( '' !== $content_id && ! in_array( $content_id, $content_ids, true ) ) {
259 $field_errors['publish_page_content'] = __( 'Choose booking page content compatible with the selected customer journey.', 'booking' );
260 }
261
262 if ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_CREATE_PAGE === $destination ) {
263 $page_title = isset( $validated_values['publish_page_title'] ) ? trim( (string) $validated_values['publish_page_title'] ) : '';
264 if ( '' === $page_title ) {
265 $field_errors['publish_page_title'] = __( 'Enter a page title.', 'booking' );
266 }
267 }
268
269 if ( WPBC_Setup_Wizard_Publish_Integration::DESTINATION_EXISTING_PAGE === $destination ) {
270 $page_id = isset( $validated_values['publish_existing_page_id'] ) ? absint( $validated_values['publish_existing_page_id'] ) : 0;
271 if ( ! $page_id || ! $this->publish_integration->is_authorized_page( $page_id, $this->publish_integration->get_publishable_pages() ) ) {
272 $field_errors['publish_existing_page_id'] = __( 'Choose a WordPress page you are allowed to edit.', 'booking' );
273 }
274 }
275
276 if ( ! empty( $field_errors ) ) {
277 return new WP_Error(
278 'wpbc_setup_wizard_publish_validation_failed',
279 __( 'Review the publishing choices before continuing.', 'booking' ),
280 array( 'field_errors' => $field_errors )
281 );
282 }
283
284 return $validated_values;
285 }
286
287 /**
288 * Enqueue compiled assets for the publishing page.
289 *
290 * @param string $module_url Setup Wizard module URL.
291 * @param string|false $asset_version Plugin version.
292 * @param string $shared_style_handle Shared wizard stylesheet handle.
293 * @param string $shared_script_handle Shared wizard script handle.
294 *
295 * @return void
296 */
297 public function enqueue_assets( $module_url, $asset_version, $shared_style_handle, $shared_script_handle ) {
298 $module_url = trailingslashit( $module_url );
299
300 wp_enqueue_style( 'wpbc-setup-wizard-publish-integration', $module_url . 'step-publish-integration/_out/step-publish-integration.css', array( $shared_style_handle ), $asset_version );
301 wp_enqueue_script( 'wpbc-setup-wizard-publish-integration', $module_url . 'step-publish-integration/_out/step-publish-integration.js', array( $shared_script_handle, 'wp-url' ), $asset_version, true );
302 wp_localize_script(
303 'wpbc-setup-wizard-publish-integration',
304 'wpbc_setup_wizard_publish_integration',
305 array(
306 'i18n' => array(
307 'copied' => __( 'Copied to the clipboard.', 'booking' ),
308 'copy_failed' => __( 'Copy failed. Select the text and copy it manually.', 'booking' ),
309 'action_create' => __( 'Create page & continue', 'booking' ),
310 'action_update' => __( 'Update page & continue', 'booking' ),
311 'action_save' => __( 'Save & continue', 'booking' ),
312 'title_required' => __( 'Enter a page title.', 'booking' ),
313 'page_required' => __( 'Choose a WordPress page.', 'booking' ),
314 'content_required' => __( 'Choose the booking experience to display.', 'booking' ),
315 'notice_create' => __( 'Save creates this WordPress page now. Repeating the save updates the same Setup Wizard page instead of creating a duplicate.', 'booking' ),
316 'notice_existing' => __( 'Save adds or updates one managed Booking Calendar block without replacing the page\'s other content.', 'booking' ),
317 'notice_manual' => __( 'Save records this shortcode for the setup summary. No WordPress page is created or changed.', 'booking' ),
318 'notice_later' => __( 'No page will be created or changed. You can integrate the booking experience later.', 'booking' ),
319 ),
320 )
321 );
322 }
323
324 /**
325 * Determine whether this wizard checkpoint retains a created page mapping.
326 *
327 * The browser uses this read-only hint only to choose between the translated
328 * Create and Update action labels. The save handler independently revalidates
329 * the mapped page and remains authoritative for the mutation.
330 *
331 * @param array<string,mixed> $consumer_context Read-only prior-step values and save results.
332 *
333 * @return bool True when the publishing result contains a created-page identifier.
334 */
335 private function has_created_page_result( array $consumer_context ) {
336 $step_result = isset( $consumer_context['_step_results']['publish_integration'] ) && is_array( $consumer_context['_step_results']['publish_integration'] )
337 ? $consumer_context['_step_results']['publish_integration']
338 : array();
339 $identifiers = array_merge(
340 isset( $step_result['created_ids'] ) ? (array) $step_result['created_ids'] : array(),
341 isset( $step_result['updated_ids'] ) ? (array) $step_result['updated_ids'] : array()
342 );
343
344 foreach ( $identifiers as $identifier ) {
345 if ( is_scalar( $identifier ) && preg_match( '/^wp_page_created:[0-9]+$/', (string) $identifier ) ) {
346 return true;
347 }
348 }
349
350 return false;
351 }
352
353 /**
354 * Determine whether publishing values already crossed the canonical save boundary.
355 *
356 * Unfinished pre-update drafts may still contain the former universal
357 * appointment title. Only an unsaved draft may receive the improved
358 * journey-aware suggestion; saved user choices remain untouched.
359 *
360 * @param array<string,mixed> $consumer_context Read-only prior-step values and save results.
361 *
362 * @return bool True after a verified Publish & integrate save.
363 */
364 private function has_saved_publish_result( array $consumer_context ) {
365 $step_result = isset( $consumer_context['_step_results']['publish_integration'] ) && is_array( $consumer_context['_step_results']['publish_integration'] )
366 ? $consumer_context['_step_results']['publish_integration']
367 : array();
368
369 return 'saved' === ( isset( $step_result['status'] ) ? $step_result['status'] : '' );
370 }
371
372 /**
373 * Read one scalar value from an allow-listed dependency step.
374 *
375 * @param array<string,mixed> $consumer_context Dependency values keyed by step.
376 * @param string $step_id Dependency step ID.
377 * @param string $field_id Dependency field ID.
378 *
379 * @return string Sanitized scalar value or an empty string.
380 */
381 private function get_dependency_value( array $consumer_context, $step_id, $field_id ) {
382 if ( ! isset( $consumer_context[ $step_id ][ $field_id ] ) || ! is_scalar( $consumer_context[ $step_id ][ $field_id ] ) ) {
383 return '';
384 }
385
386 return sanitize_text_field( (string) $consumer_context[ $step_id ][ $field_id ] );
387 }
388
389 /**
390 * Find one page presentation record by ID.
391 *
392 * @param array<int,array<string,mixed>> $pages Authorized page records.
393 * @param int $page_id Selected page ID.
394 *
395 * @return array<string,mixed> Matching record or an empty array.
396 */
397 private function find_page( array $pages, $page_id ) {
398 foreach ( $pages as $page ) {
399 if ( isset( $page['id'] ) && absint( $page['id'] ) === absint( $page_id ) ) {
400 return $page;
401 }
402 }
403
404 return array();
405 }
406
407 /**
408 * Find one content presentation record by stable ID.
409 *
410 * @param array<int,array<string,string>> $content_options Content records.
411 * @param string $content_id Selected content ID.
412 *
413 * @return array<string,string> Matching record or an empty array.
414 */
415 private function find_content( array $content_options, $content_id ) {
416 foreach ( $content_options as $content_option ) {
417 if ( isset( $content_option['id'] ) && $content_id === $content_option['id'] ) {
418 return $content_option;
419 }
420 }
421
422 return array();
423 }
424 }
425