PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / trunk
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages vtrunk
3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 2.3.1 2.3.2 2.3.3 All 194 releases
convertkit / includes / class-convertkit-resource-forms.php

class-convertkit-resource-forms.php in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages trunk, at includes/class-convertkit-resource-forms.php

726 lines 21.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ConvertKit Forms Resource class.
4 *
5 * @package ConvertKit
6 * @author ConvertKit
7 */
8
9 /**
10 * Reads ConvertKit Forms from the options table, and refreshes
11 * ConvertKit Forms data stored locally from the API.
12 *
13 * @since 1.9.6
14 */
15 class ConvertKit_Resource_Forms extends ConvertKit_Resource_V4 {
16
17 /**
18 * Holds the Settings Key that stores site wide ConvertKit settings
19 *
20 * @var string
21 */
22 public $settings_name = 'convertkit_forms';
23
24 /**
25 * The type of resource
26 *
27 * @var string
28 */
29 public $type = 'forms';
30
31 /**
32 * Constructor.
33 *
34 * @since 1.9.8.4
35 *
36 * @param bool|string $context Context.
37 */
38 public function __construct( $context = false ) {
39
40 // Initialize the API if the Access Token has been defined in the Plugin Settings.
41 $settings = new ConvertKit_Settings();
42 if ( $settings->has_access_and_refresh_token() ) {
43 $this->api = new ConvertKit_API_V4(
44 CONVERTKIT_OAUTH_CLIENT_ID,
45 CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
46 $settings->get_access_token(),
47 $settings->get_refresh_token(),
48 $settings->debug_enabled(),
49 $context
50 );
51 }
52
53 // Get last query time and existing resources.
54 $this->last_queried = get_option( $this->settings_name . '_last_queried' );
55 $this->resources = get_option( $this->settings_name );
56
57 }
58
59 /**
60 * Fetches resources (forms, landing pages or tags) from the API, storing them in the options table
61 * with a last queried timestamp.
62 *
63 * If the refresh results in a 401, removes the access and refresh tokens from the settings.
64 *
65 * @since 3.1.2
66 *
67 * @return WP_Error|array
68 */
69 public function refresh() {
70
71 // Call parent refresh method.
72 $result = parent::refresh();
73
74 // If an error occurred, maybe delete credentials from the Plugin's settings
75 // if the error is a 401 unauthorized.
76 if ( is_wp_error( $result ) ) {
77 convertkit_maybe_delete_credentials( $result, CONVERTKIT_OAUTH_CLIENT_ID );
78 }
79
80 return $result;
81
82 }
83
84 /**
85 * Returns all inline forms based on the sort order.
86 *
87 * @since 2.7.3
88 *
89 * @return bool|array
90 */
91 public function get_inline() {
92
93 // If the ConvertKit WordPress Libraries are < 1.3.6 (e.g. loaded by an outdated
94 // addon), or a WordPress site updates this Plugin before other ConvertKit Plugins,
95 // get_by() won't be available and will cause an E_ERROR, crashing the site.
96 // @see https://wordpress.org/support/topic/error-1795/.
97 if ( ! method_exists( $this, 'get_by' ) ) { // @phpstan-ignore-line Older WordPress Libraries won't have this function.
98 return false;
99 }
100
101 return $this->get_by( 'format', array( 'inline' ) );
102
103 }
104
105 /**
106 * Returns whether any inline forms exist in the options table.
107 *
108 * @since 2.7.3
109 *
110 * @return bool
111 */
112 public function inline_exist() {
113
114 return (bool) $this->get_inline();
115
116 }
117
118 /**
119 * Returns all non-inline forms based on the sort order.
120 *
121 * @since 2.2.4
122 *
123 * @return bool|array
124 */
125 public function get_non_inline() {
126
127 // If the ConvertKit WordPress Libraries are < 1.3.6 (e.g. loaded by an outdated
128 // addon), or a WordPress site updates this Plugin before other ConvertKit Plugins,
129 // get_by() won't be available and will cause an E_ERROR, crashing the site.
130 // @see https://wordpress.org/support/topic/error-1795/.
131 if ( ! method_exists( $this, 'get_by' ) ) { // @phpstan-ignore-line Older WordPress Libraries won't have this function.
132 return false;
133 }
134
135 return $this->get_by( 'format', array( 'modal', 'slide in', 'sticky bar' ) );
136
137 }
138
139 /**
140 * Returns whether any non-inline forms exist in the options table.
141 *
142 * @since 2.2.4
143 *
144 * @return bool
145 */
146 public function non_inline_exist() {
147
148 if ( ! $this->get_non_inline() ) {
149 return false;
150 }
151
152 return true;
153
154 }
155
156 /**
157 * Returns all non-legacy forms (any v4 format: inline, modal, slide in
158 * or sticky bar) based on the sort order. Legacy forms lack a `format`
159 * key and are therefore excluded.
160 *
161 * @since 3.3.7
162 *
163 * @return bool|array
164 */
165 public function get_non_legacy() {
166
167 // If the ConvertKit WordPress Libraries are < 1.3.6 (e.g. loaded by an outdated
168 // addon), or a WordPress site updates this Plugin before other ConvertKit Plugins,
169 // get_by() won't be available and will cause an E_ERROR, crashing the site.
170 // @see https://wordpress.org/support/topic/error-1795/.
171 if ( ! method_exists( $this, 'get_by' ) ) { // @phpstan-ignore-line Older WordPress Libraries won't have this function.
172 return false;
173 }
174
175 return $this->get_by( 'format', array( 'inline', 'modal', 'slide in', 'sticky bar' ) );
176
177 }
178
179 /**
180 * Returns whether any non-legacy forms exist in the options table.
181 *
182 * @since 3.3.7
183 *
184 * @return bool
185 */
186 public function non_legacy_exist() {
187
188 return (bool) $this->get_non_legacy();
189
190 }
191
192 /**
193 * Determines if the given Form ID is a legacy Form or Landing Page.
194 *
195 * @since 2.5.0
196 *
197 * @param int $id Form or Landing Page ID.
198 */
199 public function is_legacy( $id ) {
200
201 // Get Form.
202 $form = $this->get_by_id( (int) $id );
203
204 // Return false if no Form exists.
205 if ( ! $form ) {
206 return false;
207 }
208
209 // If the `format` key exists, this is not a legacy Form.
210 if ( array_key_exists( 'format', $form ) ) {
211 return false;
212 }
213
214 return true;
215
216 }
217
218 /**
219 * Returns a <select> field populated with all forms, based on the given parameters.
220 *
221 * @since 2.3.9
222 *
223 * @param string $name Name.
224 * @param string $id ID.
225 * @param bool|array $css_classes <select> CSS class(es).
226 * @param string $selected_option <option> value to mark as selected.
227 * @param bool|array $prepend_options <option> elements to prepend before resources.
228 * @param bool|array $attributes <select> attributes.
229 * @param bool|string|array $description Description.
230 * @return string HTML Select Field
231 */
232 public function get_select_field_all( $name, $id, $css_classes, $selected_option, $prepend_options = false, $attributes = false, $description = false ) {
233
234 return $this->get_select_field(
235 $this->get_forms_for_select_field( $selected_option ),
236 $name,
237 $id,
238 $css_classes,
239 $selected_option,
240 $prepend_options,
241 $attributes,
242 $description
243 );
244
245 }
246
247 /**
248 * Outputs a <select> field populated with all forms, based on the given parameters.
249 *
250 * @since 2.8.5
251 *
252 * @param string $name Name.
253 * @param string $id ID.
254 * @param bool|array $css_classes <select> CSS class(es).
255 * @param string $selected_option <option> value to mark as selected.
256 * @param bool|array $prepend_options <option> elements to prepend before resources.
257 * @param bool|array $attributes <select> attributes.
258 * @param bool|string|array $description Description.
259 */
260 public function output_select_field_all( $name, $id, $css_classes, $selected_option, $prepend_options = false, $attributes = false, $description = false ) {
261
262 $this->output_select_field(
263 $this->get_forms_for_select_field( $selected_option ),
264 $name,
265 $id,
266 $css_classes,
267 $selected_option,
268 $prepend_options,
269 $attributes,
270 $description
271 );
272
273 }
274
275 /**
276 * Returns the array of forms to display in an "all forms" dropdown: every
277 * non-legacy form, plus the currently-selected legacy form (if any) so
278 * existing saved legacy assignments continue to render as selected in the
279 * UI without exposing other legacy forms as new selection options.
280 *
281 * @since 3.3.7
282 *
283 * @param string|int $selected_option Currently-selected form ID.
284 * @return array
285 */
286 private function get_forms_for_select_field( $selected_option ) {
287
288 $forms = $this->get_non_legacy();
289 if ( ! is_array( $forms ) ) {
290 $forms = array();
291 }
292
293 // If the currently-selected form is a legacy form, append it so the
294 // dropdown shows the saved value as selected.
295 if ( $selected_option && $this->is_legacy( $selected_option ) ) {
296 $legacy_form = $this->get_by_id( (int) $selected_option );
297 if ( $legacy_form ) {
298 $forms[] = $legacy_form;
299 }
300 }
301
302 return $forms;
303
304 }
305
306 /**
307 * Returns a <select> field populated with all non-inline forms, based on the given parameters.
308 *
309 * @since 2.3.9
310 *
311 * @param string $name Name.
312 * @param string $id ID.
313 * @param bool|array $css_classes <select> CSS class(es).
314 * @param array $selected_options <option> values to mark as selected.
315 * @param bool|array $prepend_options <option> elements to prepend before resources.
316 * @param bool|array $attributes <select> attributes.
317 * @param bool|string|array $description Description.
318 * @return string HTML Select Field
319 */
320 public function get_select_field_non_inline( $name, $id, $css_classes, $selected_options, $prepend_options = false, $attributes = false, $description = false ) {
321
322 return $this->get_multi_select_field(
323 $this->get_non_inline(),
324 $name,
325 $id,
326 $css_classes,
327 $selected_options,
328 $prepend_options,
329 $attributes,
330 $description
331 );
332
333 }
334
335 /**
336 * Outputs a <select> field populated with all non-inline forms, based on the given parameters.
337 *
338 * @since 2.3.9
339 *
340 * @param string $name Name.
341 * @param string $id ID.
342 * @param bool|array $css_classes <select> CSS class(es).
343 * @param array $selected_options <option> values to mark as selected.
344 * @param bool|array $prepend_options <option> elements to prepend before resources.
345 * @param bool|array $attributes <select> attributes.
346 * @param bool|string|array $description Description.
347 */
348 public function output_select_field_non_inline( $name, $id, $css_classes, $selected_options, $prepend_options = false, $attributes = false, $description = false ) {
349
350 echo wp_kses(
351 $this->get_select_field_non_inline(
352 $name,
353 $id,
354 $css_classes,
355 $selected_options,
356 $prepend_options,
357 $attributes,
358 $description
359 ),
360 convertkit_kses_allowed_html()
361 );
362
363 }
364
365 /**
366 * Returns a <select> field populated with the resources, based on the given parameters.
367 *
368 * @since 2.3.9
369 *
370 * @param array $forms Forms.
371 * @param string $name Name.
372 * @param string $id ID.
373 * @param bool|array $css_classes <select> CSS class(es).
374 * @param string $selected_option <option> value to mark as selected.
375 * @param bool|array $prepend_options <option> elements to prepend before resources.
376 * @param bool|array $attributes <select> attributes.
377 * @param bool|string|array $description Description.
378 * @return string HTML Select Field
379 */
380 private function get_select_field( $forms, $name, $id, $css_classes, $selected_option, $prepend_options = false, $attributes = false, $description = false ) {
381
382 $html = sprintf(
383 '<select name="%s" id="%s" class="%s"',
384 esc_attr( $name ),
385 esc_attr( $id ),
386 esc_attr( ( is_array( $css_classes ) ? implode( ' ', $css_classes ) : '' ) )
387 );
388
389 // Append any attributes.
390 if ( $attributes ) {
391 foreach ( $attributes as $key => $value ) {
392 $html .= sprintf(
393 ' %s="%s"',
394 esc_attr( $key ),
395 esc_attr( $value )
396 );
397 }
398 }
399
400 // Close select tag.
401 $html .= '>';
402
403 // If any prepended options exist, add them now.
404 if ( $prepend_options ) {
405 foreach ( $prepend_options as $value => $label ) {
406 $html .= sprintf(
407 '<option value="%s" data-preserve-on-refresh="1"%s>%s</option>',
408 esc_attr( $value ),
409 selected( $selected_option, $value, false ),
410 esc_attr( $label )
411 );
412 }
413 }
414
415 // Iterate through resources, if they exist, building <option> elements.
416 if ( $forms ) {
417 foreach ( $forms as $form ) {
418 // Legacy forms don't include a `format` key, so define them as inline.
419 $html .= sprintf(
420 '<option value="%s"%s>%s [%s]</option>',
421 esc_attr( $form['id'] ),
422 selected( $selected_option, $form['id'], false ),
423 esc_attr( $form['name'] ),
424 ( ! empty( $form['format'] ) ? esc_attr( $form['format'] ) : 'inline' )
425 );
426 }
427 }
428
429 // Close select.
430 $html .= '</select>';
431
432 // If no description is provided, return the select field now.
433 if ( ! $description ) {
434 return $html;
435 }
436
437 // Append description before returning field.
438 if ( ! is_array( $description ) ) {
439 return $html . '<p class="description">' . $description . '</p>';
440 }
441
442 // Return description lines in a paragraph, using breaklines for each description entry in the array.
443 return $html . '<p class="description">' . implode( '<br />', $description ) . '</p>';
444
445 }
446
447 /**
448 * Outputs a <select> field populated with the resources, based on the given parameters.
449 *
450 * @since 2.8.5
451 *
452 * @param array $forms Forms.
453 * @param string $name Name.
454 * @param string $id ID.
455 * @param bool|array $css_classes <select> CSS class(es).
456 * @param string $selected_option <option> value to mark as selected.
457 * @param bool|array $prepend_options <option> elements to prepend before resources.
458 * @param bool|array $attributes <select> attributes.
459 * @param bool|string|array $description Description.
460 */
461 private function output_select_field( $forms, $name, $id, $css_classes, $selected_option, $prepend_options = false, $attributes = false, $description = false ) {
462
463 echo wp_kses(
464 $this->get_select_field(
465 $forms,
466 $name,
467 $id,
468 $css_classes,
469 $selected_option,
470 $prepend_options,
471 $attributes,
472 $description
473 ),
474 convertkit_kses_allowed_html()
475 );
476
477 }
478
479 /**
480 * Returns a <select> field populated with the resources, based on the given parameters,
481 * that supports multiple selection.
482 *
483 * @since 2.6.9
484 *
485 * @param array $forms Forms.
486 * @param string $name Name.
487 * @param string $id ID.
488 * @param bool|array $css_classes <select> CSS class(es).
489 * @param array $selected_options <option> values to mark as selected.
490 * @param bool|array $prepend_options <option> elements to prepend before resources.
491 * @param bool|array $attributes <select> attributes.
492 * @param bool|string|array $description Description.
493 * @return string HTML Select Field
494 */
495 private function get_multi_select_field( $forms, $name, $id, $css_classes, $selected_options = array(), $prepend_options = false, $attributes = false, $description = false ) {
496
497 $html = sprintf(
498 '<select name="%s[]" id="%s" class="%s" multiple',
499 esc_attr( $name ),
500 esc_attr( $id ),
501 esc_attr( ( is_array( $css_classes ) ? implode( ' ', $css_classes ) : '' ) )
502 );
503
504 // Append any attributes.
505 if ( $attributes ) {
506 foreach ( $attributes as $key => $value ) {
507 $html .= sprintf(
508 ' %s="%s"',
509 esc_attr( $key ),
510 esc_attr( $value )
511 );
512 }
513 }
514
515 // Close select tag.
516 $html .= '>';
517
518 // If any prepended options exist, add them now.
519 if ( $prepend_options ) {
520 foreach ( $prepend_options as $value => $label ) {
521 $html .= sprintf(
522 '<option value="%s" data-preserve-on-refresh="1"%s>%s</option>',
523 esc_attr( $value ),
524 ( in_array( $value, $selected_options, true ) ? ' selected' : '' ),
525 esc_attr( $label )
526 );
527 }
528 }
529
530 // Iterate through resources, if they exist, building <option> elements.
531 if ( $forms ) {
532 foreach ( $forms as $form ) {
533 // Legacy forms don't include a `format` key, so define them as inline.
534 $html .= sprintf(
535 '<option value="%s"%s>%s [%s]</option>',
536 esc_attr( $form['id'] ),
537 ( in_array( $form['id'], $selected_options, true ) ? ' selected' : '' ),
538 esc_attr( $form['name'] ),
539 ( ! empty( $form['format'] ) ? esc_attr( $form['format'] ) : 'inline' )
540 );
541 }
542 }
543
544 // Close select.
545 $html .= '</select>';
546
547 // If no description is provided, return the select field now.
548 if ( ! $description ) {
549 return $html;
550 }
551
552 // Append description before returning field.
553 if ( ! is_array( $description ) ) {
554 return $html . '<p class="description">' . $description . '</p>';
555 }
556
557 // Return description lines in a paragraph, using breaklines for each description entry in the array.
558 return $html . '<p class="description">' . implode( '<br />', $description ) . '</p>';
559
560 }
561
562 /**
563 * Returns the HTML/JS markup for the given Form ID.
564 *
565 * Legacy Forms will return HTML.
566 * Current Forms will return a <script> embed string.
567 *
568 * @since 1.9.6
569 *
570 * @param int $id Form ID.
571 * @param int $post_id Post ID that requested the Form.
572 * @return WP_Error|string
573 */
574 public function get_html( $id, $post_id = 0 ) {
575
576 // Cast ID to integer.
577 $id = absint( $id );
578
579 // Bail if the resources are a WP_Error.
580 if ( is_wp_error( $this->resources ) ) {
581 return $this->resources;
582 }
583
584 // Bail if the resource doesn't exist.
585 if ( ! isset( $this->resources[ $id ] ) ) {
586 return new WP_Error(
587 'convertkit_resource_forms_get_html',
588 sprintf(
589 /* translators: ConvertKit Form ID */
590 __( 'Kit Form ID %s does not exist on Kit.', 'convertkit' ),
591 $id
592 ),
593 404
594 );
595 }
596
597 // Initialize Settings.
598 $settings = new ConvertKit_Settings();
599
600 // If no uid is present in the Form API data, this is a legacy form that's served by directly fetching the HTML
601 // from forms.kit.com.
602 if ( ! isset( $this->resources[ $id ]['uid'] ) ) {
603 // Bail if no Access Token is specified in the Plugin Settings.
604 if ( ! $settings->has_access_token() ) {
605 return new WP_Error(
606 'convertkit_resource_forms_get_html',
607 __( 'Kit Legacy Form could not be fetched as no Access Token specified in Plugin Settings', 'convertkit' )
608 );
609 }
610
611 // Initialize the API.
612 $api = new ConvertKit_API_V4(
613 CONVERTKIT_OAUTH_CLIENT_ID,
614 CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
615 $settings->get_access_token(),
616 $settings->get_refresh_token(),
617 $settings->debug_enabled(),
618 'output_form'
619 );
620
621 // Return Legacy Form HTML.
622 // We now call get_html() with the `embed_url` property, instead of get_form_html() with the `id` property,
623 // because `embed_url` includes the API Key.
624 return $api->get_html( $this->resources[ $id ]['embed_url'] );
625 }
626
627 // If the form's format is not an inline form, add the inline script before the closing </body> tag.
628 // This prevents a modal form's overlay being constrained by the WordPress Theme's styles,
629 // and accidentally embedding the same non-inline form twice, which would result in e.g. the same modal form
630 // displaying twice.
631 if ( $this->resources[ $id ]['format'] !== 'inline' ) {
632 add_filter(
633 'convertkit_output_scripts_footer',
634 function ( $scripts ) use ( $id, $post_id, $settings ) {
635
636 // Build script.
637 $script = array(
638 'async' => true,
639 'data-uid' => $this->resources[ $id ]['uid'],
640 'src' => $this->resources[ $id ]['embed_js'],
641 'data-kit-limit-per-session' => $settings->non_inline_form_limit_per_session() ? '1' : '0',
642 );
643
644 // If debugging is enabled, add the post ID to the script.
645 if ( $settings->debug_enabled() ) {
646 $script['data-kit-source-post-id'] = $post_id;
647 }
648
649 // Add the script to the scripts array.
650 $scripts[] = $script;
651
652 return $scripts;
653
654 }
655 );
656
657 // Sanity check we're not in the WordPress Admin interface.
658 // Some third party REST API Plugins seem to load frontend Posts, which would result in a wp_die() as
659 // the output Plugin class (rightly) isn't initialized in the backend.
660 if ( is_admin() ) {
661 return '';
662 }
663
664 // Don't output the global non-inline form, if defined, because
665 // a non-inline form was specified at either Post/Page default level, Post/Page level
666 // or Post Category level.
667 // This prevents multiple non-inline forms loading.
668 remove_action( 'wp_footer', array( WP_ConvertKit()->get_class( 'output' ), 'output_global_non_inline_form' ), 1 );
669
670 // Don't return a script for output, as it'll be output in the site's footer.
671 return '';
672 }
673
674 // If here, return Form <script> embed now, as we want the inline form to display at this specific point of the content.
675
676 // Define script key-value pairs.
677 $script = array(
678 'async' => true,
679 'data-uid' => $this->resources[ $id ]['uid'],
680 'src' => $this->resources[ $id ]['embed_js'],
681 );
682
683 // If debugging is enabled, add the post ID to the script.
684 if ( $settings->debug_enabled() ) {
685 $script['data-kit-source-post-id'] = $post_id;
686 }
687
688 /**
689 * Filter the form <script> key/value pairs immediately before the script is output.
690 *
691 * @since 2.4.5
692 *
693 * @param array $script Form script key/value pairs to output as <script> tag.
694 */
695 $script = apply_filters( 'convertkit_resource_forms_output_script', $script );
696
697 // Build script output.
698 $output = '<script';
699 foreach ( $script as $attribute => $value ) {
700 // If the value is true, just output the attribute.
701 if ( $value === true ) {
702 $output .= ' ' . esc_attr( $attribute );
703 continue;
704 }
705
706 // Sanitize attribute and value.
707 $attribute = esc_attr( $attribute );
708 $value = ( $attribute === 'src' ? esc_url( $value ) : esc_attr( $value ) );
709
710 // Output the attribute and value.
711 $output .= ' ' . $attribute;
712
713 // Output the value, if it's not a blank string.
714 if ( strlen( $value ) > 0 ) {
715 $output .= '="' . $value . '"';
716 }
717 }
718 $output .= '></script>';
719
720 // Return script output.
721 return $output;
722
723 }
724
725 }
726