array( label, type, required ). */ public static function standard_fields() { return array( 'first_name' => array( 'label' => __( 'First name', 'acquisitionsuite' ), 'type' => 'text', 'required' => 1 ), 'last_name' => array( 'label' => __( 'Last name', 'acquisitionsuite' ), 'type' => 'text', 'required' => 0 ), 'email' => array( 'label' => __( 'Email address', 'acquisitionsuite' ), 'type' => 'email', 'required' => 1 ), 'phone' => array( 'label' => __( 'Phone number', 'acquisitionsuite' ), 'type' => 'phone', 'required' => 0 ), 'company' => array( 'label' => __( 'Company', 'acquisitionsuite' ), 'type' => 'text', 'required' => 0 ), 'website' => array( 'label' => __( 'Website', 'acquisitionsuite' ), 'type' => 'url', 'required' => 0 ), ); } /** @return array id => template */ public static function all() { $std = array( 'first_name', 'last_name', 'email' ); $all = array( 'free_resource' => array( 'id' => 'free_resource', 'label' => __( 'Offer a free resource', 'acquisitionsuite' ), 'description' => __( 'Turn visitors into qualified leads.', 'acquisitionsuite' ), 'icon' => 'dashicons-download', 'purpose' => 'file_gate', 'needs_file' => true, 'defaults' => array( 'name' => __( 'Free guide', 'acquisitionsuite' ), 'headline' => __( 'Download our free guide', 'acquisitionsuite' ), 'description' => __( 'Enter your details and we will send it to you.', 'acquisitionsuite' ), 'fields' => $std, 'button_label' => __( 'Download now', 'acquisitionsuite' ), 'submit_label' => __( 'Get my download', 'acquisitionsuite' ), 'after_message' => __( 'Thanks! Your download is on its way.', 'acquisitionsuite' ), 'email_subject' => __( 'Your download is ready, {full_name}', 'acquisitionsuite' ), 'email_body' => __( "Hi {full_name},\n\nThanks for your interest. Your file {file_name} is ready.", 'acquisitionsuite' ), ), ), 'email_list' => array( 'id' => 'email_list', 'label' => __( 'Grow your email list', 'acquisitionsuite' ), 'description' => __( 'Build and nurture your subscribers.', 'acquisitionsuite' ), 'icon' => 'dashicons-email-alt', 'purpose' => 'registration', 'needs_file' => false, 'defaults' => array( 'name' => __( 'Newsletter signup', 'acquisitionsuite' ), 'headline' => __( 'Join our newsletter', 'acquisitionsuite' ), 'description' => __( 'Tips and updates, straight to your inbox.', 'acquisitionsuite' ), 'fields' => array( 'first_name', 'email' ), 'button_label' => __( 'Subscribe', 'acquisitionsuite' ), 'submit_label' => __( 'Subscribe', 'acquisitionsuite' ), 'after_message' => __( 'You are on the list. Thanks for subscribing!', 'acquisitionsuite' ), 'email_subject' => __( 'Welcome, {full_name}', 'acquisitionsuite' ), 'email_body' => __( "Hi {full_name},\n\nThanks for subscribing. We will be in touch soon.", 'acquisitionsuite' ), ), ), 'enquiry' => array( 'id' => 'enquiry', 'label' => __( 'Capture service enquiries', 'acquisitionsuite' ), 'description' => __( 'Collect qualified enquiries.', 'acquisitionsuite' ), 'icon' => 'dashicons-businessman', 'purpose' => 'custom', 'needs_file' => false, 'defaults' => array( 'name' => __( 'Service enquiries', 'acquisitionsuite' ), 'headline' => __( 'Tell us what you need', 'acquisitionsuite' ), 'description' => __( 'We usually reply within one working day.', 'acquisitionsuite' ), 'fields' => array( 'first_name', 'last_name', 'email', 'phone', 'company' ), 'button_label' => __( 'Make an enquiry', 'acquisitionsuite' ), 'submit_label' => __( 'Send enquiry', 'acquisitionsuite' ), 'after_message' => __( 'Thanks! We have received your enquiry and will reply shortly.', 'acquisitionsuite' ), 'email_subject' => __( 'We received your enquiry, {full_name}', 'acquisitionsuite' ), 'email_body' => __( "Hi {full_name},\n\nThanks for getting in touch. We will reply as soon as we can.", 'acquisitionsuite' ), ), ), 'waitlist' => array( 'id' => 'waitlist', 'label' => __( 'Join a waitlist', 'acquisitionsuite' ), 'description' => __( 'Build anticipation before launch.', 'acquisitionsuite' ), 'icon' => 'dashicons-megaphone', 'purpose' => 'registration', 'needs_file' => false, 'tags' => array( __CLASS__, 'tags_waitlist' ), 'pro_defaults' => array( 'tags' => array( 'waitlist' ) ), 'extra' => array( array( 'key' => 'waitlist_for', 'label' => __( 'What are people waiting for?', 'acquisitionsuite' ), 'type' => 'text', 'required' => false, 'help' => __( 'Used in the confirmation, for example "the spring cohort". Leave empty to use your site name.', 'acquisitionsuite' ) ), ), 'defaults' => array( 'name' => __( 'Launch waitlist', 'acquisitionsuite' ), 'headline' => __( 'Join the waitlist', 'acquisitionsuite' ), 'description' => __( 'Be the first to know when we open.', 'acquisitionsuite' ), 'fields' => array( 'first_name', 'email' ), 'button_label' => __( 'Join the waitlist', 'acquisitionsuite' ), 'submit_label' => __( 'Join the waitlist', 'acquisitionsuite' ), 'after_message' => __( 'You are on the list. We will email you as soon as {waitlist_for} is ready.', 'acquisitionsuite' ), 'email_subject' => __( 'You are on the waitlist, {full_name}', 'acquisitionsuite' ), 'email_body' => __( "Hi {full_name},\n\nYou are on the waitlist for {waitlist_for}. We will email you as soon as it is ready.", 'acquisitionsuite' ), 'email_enabled' => 0, ), ), 'webinar' => array( 'id' => 'webinar', 'label' => __( 'Register webinar attendees', 'acquisitionsuite' ), 'description' => __( 'Grow your webinar audience.', 'acquisitionsuite' ), 'icon' => 'dashicons-video-alt3', 'purpose' => 'registration', 'needs_file' => false, 'tags' => array( __CLASS__, 'tags_webinar' ), 'pro_defaults' => array( 'tags' => array( 'webinar' ) ), 'extra' => array( array( 'key' => 'event_title', 'label' => __( 'Webinar title', 'acquisitionsuite' ), 'type' => 'text', 'required' => true ), array( 'key' => 'event_start', 'label' => __( 'Starts', 'acquisitionsuite' ), 'type' => 'datetime', 'required' => true, 'help' => __( 'In your site\'s timezone. Registrants get a link to add it to their calendar.', 'acquisitionsuite' ) ), array( 'key' => 'event_length', 'label' => __( 'Length', 'acquisitionsuite' ), 'type' => 'select', 'required' => false, 'default' => '60', 'options' => array( '30' => __( '30 minutes', 'acquisitionsuite' ), '45' => __( '45 minutes', 'acquisitionsuite' ), '60' => __( '1 hour', 'acquisitionsuite' ), '90' => __( '90 minutes', 'acquisitionsuite' ), '120' => __( '2 hours', 'acquisitionsuite' ) ) ), array( 'key' => 'event_link', 'label' => __( 'Join link', 'acquisitionsuite' ), 'type' => 'url', 'required' => false, 'help' => __( 'Zoom, Google Meet or similar. You can add it later by editing the funnel.', 'acquisitionsuite' ) ), ), 'defaults' => array( 'name' => __( 'Webinar registration', 'acquisitionsuite' ), 'headline' => __( 'Save your seat', 'acquisitionsuite' ), 'description' => __( 'Register and we will send you the details.', 'acquisitionsuite' ), 'fields' => array( 'first_name', 'email' ), 'button_label' => __( 'Register now', 'acquisitionsuite' ), 'submit_label' => __( 'Register', 'acquisitionsuite' ), 'after_message' => __( 'You are registered for {webinar_title} on {webinar_date}.', 'acquisitionsuite' ), 'email_subject' => __( 'You are registered: {webinar_title}', 'acquisitionsuite' ), 'email_body' => __( "Hi {full_name},\n\nYou are registered for {webinar_title}.\n\nWhen: {webinar_date} at {webinar_time}\nJoin here: {webinar_link}\n\nAdd it to your calendar: {webinar_calendar_link}", 'acquisitionsuite' ), 'email_enabled' => 1, // the details are the point of the email ), ), 'book_call' => array( 'id' => 'book_call', 'label' => __( 'Book discovery calls', 'acquisitionsuite' ), 'description' => __( 'Capture appointment requests.', 'acquisitionsuite' ), 'icon' => 'dashicons-calendar-alt', 'purpose' => 'appointment', 'needs_file' => false, 'tags' => array( __CLASS__, 'tags_book_call' ), 'prefill_redirect' => 'booking_url', 'notify_default' => 1, 'pro_defaults' => array( 'tags' => array( 'call-request' ) ), 'extra' => array( array( 'key' => 'booking_url', 'label' => __( 'Scheduling link', 'acquisitionsuite' ), 'type' => 'url', 'required' => false, 'help' => __( 'Calendly, Cal.com or similar. If you add one, people are sent there after the form.', 'acquisitionsuite' ) ), ), 'defaults' => array( 'name' => __( 'Discovery call requests', 'acquisitionsuite' ), 'headline' => __( 'Book a discovery call', 'acquisitionsuite' ), 'description' => __( 'Tell us a little about you and when suits.', 'acquisitionsuite' ), 'fields' => array( 'first_name', 'last_name', 'email', 'phone' ), 'custom' => array( array( 'label' => __( 'Preferred date', 'acquisitionsuite' ), 'type' => 'date', 'required' => 0 ), array( 'label' => __( 'What would you like to discuss?', 'acquisitionsuite' ), 'type' => 'textarea', 'required' => 0 ), ), 'button_label' => __( 'Book a call', 'acquisitionsuite' ), 'submit_label' => __( 'Request a call', 'acquisitionsuite' ), 'after_message' => __( 'Thanks! We have your request and will confirm a time shortly.', 'acquisitionsuite' ), 'email_subject' => __( 'We received your request, {full_name}', 'acquisitionsuite' ), 'email_body' => __( "Hi {full_name},\n\nThanks for asking for a call. We will confirm a time shortly.\n\nPrefer to pick a time yourself? {booking_link}", 'acquisitionsuite' ), 'email_enabled' => 0, ), ), ); /** * The funnel templates. Pro and add-ons add theirs here. A template needs: id, label, description, icon (a * dashicon class), purpose (file_gate, registration, appointment or custom), needs_file and defaults. * * @param array $all id => template */ $filtered = apply_filters( 'acquis_funnel_templates', $all ); $out = array(); foreach ( (array) $filtered as $id => $t ) { if ( is_array( $t ) && ! empty( $t['id'] ) && ! empty( $t['label'] ) && isset( $t['defaults'] ) ) { $out[ sanitize_key( $id ) ] = $t; } } return $out; } public static function get( $id ) { $all = self::all(); return isset( $all[ $id ] ) ? $all[ $id ] : null; } /** * The answers a funnel is built from, cleaned. * * @param string $template_id * @param array $raw What the browser sent (already JSON-decoded). * @param bool $editing True when changing an existing funnel: the form source cannot change then. * @param int $funnel_id The funnel being changed (0 when creating), so an extension can tell "this funnel" from others. * @return array|WP_Error { name, headline, description, file_id, form_mode, form_id, fields[], custom[], display, * button_label, submit_label, after{type,message,url}, email{enabled,subject,body}, * notify{enabled,email}, pro{} } */ public static function sanitize( $template_id, array $raw, $editing = false, $funnel_id = 0 ) { $tpl = self::get( sanitize_key( $template_id ) ); if ( ! $tpl ) { return new WP_Error( 'template', __( 'That template is not available.', 'acquisitionsuite' ) ); } $d = $tpl['defaults']; $t = function ( $key, $max ) use ( $raw ) { $v = isset( $raw[ $key ] ) && is_scalar( $raw[ $key ] ) ? sanitize_text_field( (string) $raw[ $key ] ) : ''; return function_exists( 'mb_substr' ) ? mb_substr( $v, 0, $max ) : substr( $v, 0, $max ); }; $a = array( 'template' => $tpl['id'], 'name' => $t( 'name', 120 ), 'headline' => $t( 'headline', 150 ), 'description' => $t( 'description', 300 ), 'button_label' => $t( 'button_label', 60 ), 'submit_label' => $t( 'submit_label', 60 ), ); if ( '' === $a['name'] ) { return new WP_Error( 'name', __( 'Enter a name for this funnel.', 'acquisitionsuite' ), array( 'field' => 'name' ) ); } foreach ( array( 'headline', 'button_label', 'submit_label' ) as $k ) { if ( '' === $a[ $k ] ) { $a[ $k ] = (string) ( $d[ $k ] ?? '' ); } } // ── Resource ────────────────────────────────────────────────────── $a['file_id'] = 0; if ( ! empty( $tpl['needs_file'] ) ) { $a['file_id'] = absint( $raw['file_id'] ?? 0 ); if ( ! $a['file_id'] || ! ACQUIS_File_Manager::get_file_data( $a['file_id'] ) ) { return new WP_Error( 'file', __( 'Choose the file people should receive.', 'acquisitionsuite' ), array( 'field' => 'file_id' ) ); } } // ── Form ────────────────────────────────────────────────────────── $a['form_mode'] = ( isset( $raw['form_mode'] ) && 'existing' === $raw['form_mode'] ) ? 'existing' : 'new'; $a['form_id'] = 0; $a['fields'] = array(); $a['custom'] = array(); if ( 'existing' === $a['form_mode'] ) { $a['form_id'] = absint( $raw['form_id'] ?? 0 ); if ( ! $a['form_id'] || ! ACQUIS_P3_DB::get_form( $a['form_id'] ) ) { return new WP_Error( 'form', __( 'Choose one of your existing forms.', 'acquisitionsuite' ), array( 'field' => 'form_id' ) ); } } else { $std = self::standard_fields(); foreach ( (array) ( $raw['fields'] ?? array() ) as $k ) { $k = sanitize_key( (string) $k ); if ( isset( $std[ $k ] ) && ! in_array( $k, $a['fields'], true ) ) { $a['fields'][] = $k; } } if ( ! in_array( 'email', $a['fields'], true ) ) { $a['fields'][] = 'email'; // A lead needs an email address. } // Keep the order of the standard list, not the order they were ticked in. $a['fields'] = array_values( array_intersect( array_keys( $std ), $a['fields'] ) ); $types = array( 'text', 'textarea', 'number', 'phone', 'url', 'date' ); $new_fields = 0; $seen_keys = array(); foreach ( (array) ( $raw['custom'] ?? array() ) as $c ) { $label = is_array( $c ) && isset( $c['label'] ) && is_scalar( $c['label'] ) ? sanitize_text_field( (string) $c['label'] ) : ''; if ( '' === $label ) { continue; } // A field the form already has (shown when editing) keeps its key, so renaming it never makes a new one, // and keeps its type, so a dropdown stays a dropdown. It can be of any type and have any name. $key = ''; if ( $editing && isset( $c['key'] ) && is_string( $c['key'] ) && preg_match( '/^[a-z0-9_]{1,64}$/', $c['key'] ) && ! isset( $std[ $c['key'] ] ) && ! in_array( $c['key'], $seen_keys, true ) ) { $key = $c['key']; } if ( '' === $key && $new_fields >= 5 ) { continue; // at most five NEW custom fields at a time } if ( '' !== $key ) { $type = isset( $c['type'] ) && is_string( $c['type'] ) && preg_match( '/^[a-z0-9_-]{1,30}$/', $c['type'] ) ? $c['type'] : 'text'; } else { $type = isset( $c['type'] ) && in_array( $c['type'], $types, true ) ? $c['type'] : 'text'; $new_fields++; } $item = array( 'label' => substr( $label, 0, 80 ), 'type' => $type, 'required' => empty( $c['required'] ) ? 0 : 1 ); if ( '' !== $key ) { $item['key'] = $key; $seen_keys[] = $key; } $a['custom'][] = $item; } } if ( $editing ) { // The form a funnel was built on does not change afterwards: make a new funnel for a different form. unset( $a['form_mode'], $a['form_id'] ); } // Embedded and popup are always there. Floating and slide-in are added to the allowed list by Pro. $display = isset( $raw['display'] ) ? sanitize_key( (string) $raw['display'] ) : 'embedded'; $allowed = (array) apply_filters( 'acquis_form_display_styles', array( 'embedded', 'popup' ) ); $a['display'] = in_array( $display, array( 'embedded', 'popup', 'floating', 'slidein' ), true ) && in_array( $display, $allowed, true ) ? $display : 'embedded'; $a['slidein_delay'] = isset( $raw['slidein_delay'] ) && is_numeric( $raw['slidein_delay'] ) ? max( 0, min( 300, (int) $raw['slidein_delay'] ) ) : 8; // ── The template's own questions ────────────────────────────────── $a['extra'] = array(); foreach ( (array) ( $tpl['extra'] ?? array() ) as $def ) { $res = self::clean_extra( $def, isset( $raw['extra'][ $def['key'] ] ) ? $raw['extra'][ $def['key'] ] : '', $editing ); if ( is_wp_error( $res ) ) { return $res; } $a['extra'][ $def['key'] ] = $res; } $sec = wp_parse_args( (array) ( $tpl['sections'] ?? array() ), array( 'after' => 1, 'email' => 1, 'notify' => 1 ) ); // ── After submission ────────────────────────────────────────────── $after = $sec['after'] ? (array) ( $raw['after'] ?? array() ) : array(); $type = ( isset( $after['type'] ) && 'redirect' === $after['type'] ) ? 'redirect' : 'message'; $url = ''; if ( 'redirect' === $type ) { $url = esc_url_raw( trim( (string) ( $after['url'] ?? '' ) ), array( 'http', 'https' ) ); if ( '' === $url ) { return new WP_Error( 'after_url', __( 'Enter the address people should be sent to, starting with https://.', 'acquisitionsuite' ), array( 'field' => 'after_url' ) ); } } $msg = sanitize_text_field( (string) ( $after['message'] ?? '' ) ); $a['after'] = array( 'type' => $type, 'message' => '' !== $msg ? substr( $msg, 0, 300 ) : (string) ( $d['after_message'] ?? '' ), 'url' => $url, ); // ── Delivery email (for this funnel only) ───────────────────────── $email = $sec['email'] ? (array) ( $raw['email'] ?? array() ) : array(); $a['email'] = array( 'enabled' => ! empty( $email['enabled'] ) ? 1 : 0, 'subject' => substr( sanitize_text_field( (string) ( $email['subject'] ?? '' ) ), 0, 190 ), 'body' => substr( sanitize_textarea_field( (string) ( $email['body'] ?? '' ) ), 0, 3000 ), ); if ( $a['email']['enabled'] && ( '' === $a['email']['subject'] || '' === trim( $a['email']['body'] ) ) ) { return new WP_Error( 'email', __( 'Write a subject and a message for the delivery email, or switch the custom email off.', 'acquisitionsuite' ), array( 'field' => 'email' ) ); } // ── Notify me ───────────────────────────────────────────────────── $notify = $sec['notify'] ? (array) ( $raw['notify'] ?? array() ) : array(); $addr = sanitize_email( (string) ( $notify['email'] ?? '' ) ); $on = ! empty( $notify['enabled'] ); if ( $on && '' === $addr ) { $addr = (string) get_option( 'admin_email' ); } if ( $on && ! is_email( $addr ) ) { return new WP_Error( 'notify', __( 'Enter a valid email address for the notification.', 'acquisitionsuite' ), array( 'field' => 'notify_email' ) ); } $a['notify'] = array( 'enabled' => $on ? 1 : 0, 'email' => $on ? $addr : '' ); /** * Pro (and add-ons) clean their own part of the answers here and return it under a key of their choosing, or * return a WP_Error to stop the build. * * @param array $answers The cleaned answers so far. * @param array $raw What the browser sent. * @param array $tpl The template. * @param int $funnel_id The funnel being edited, 0 when creating. */ $a = apply_filters( 'acquis_funnel_answers', $a, $raw, $tpl, (int) $funnel_id ); return $a; } /** The shortcode to place a funnel's form on a page. */ public static function shortcode( $template_id, $form_id, $file_id = 0, array $answers = array() ) { $tpl = self::get( $template_id ); if ( $tpl && ! empty( $tpl['needs_file'] ) ) { $label = isset( $answers['button_label'] ) && '' !== $answers['button_label'] ? ' label="' . esc_attr( str_replace( '"', '', $answers['button_label'] ) ) . '"' : ''; return '[acquis_download id="' . absint( $file_id ) . '" form="' . absint( $form_id ) . '"' . $label . ']'; } $display = isset( $answers['display'] ) && in_array( $answers['display'], array( 'popup', 'floating', 'slidein' ), true ) ? $answers['display'] : 'embedded'; $delay = 'slidein' === $display && isset( $answers['slidein_delay'] ) ? ' delay="' . (int) $answers['slidein_delay'] . '"' : ''; return '[acquis_form id="' . absint( $form_id ) . '" display="' . $display . '"' . $delay . ']'; } // ── A template's own questions ───────────────────────────────────────── /** * One answer to one of a template's extra questions, cleaned by its type. * * @return string|WP_Error */ private static function clean_extra( array $def, $value, $editing ) { $value = is_scalar( $value ) ? trim( (string) $value ) : ''; $field = 'extra_' . $def['key']; $need = ! empty( $def['required'] ); $fail = function ( $text ) use ( $field ) { return new WP_Error( $field, $text, array( 'field' => $field ) ); }; if ( '' === $value && isset( $def['default'] ) ) { $value = (string) $def['default']; } if ( '' === $value ) { /* translators: %s: the name of a question in the wizard */ return $need ? $fail( sprintf( __( 'Fill in: %s', 'acquisitionsuite' ), $def['label'] ) ) : ''; } switch ( $def['type'] ) { case 'url': $u = esc_url_raw( $value, array( 'http', 'https' ) ); return '' === $u ? $fail( __( 'Enter an address starting with https://.', 'acquisitionsuite' ) ) : $u; case 'datetime': $d = DateTimeImmutable::createFromFormat( 'Y-m-d\TH:i', $value, wp_timezone() ); if ( ! $d || $d->format( 'Y-m-d\TH:i' ) !== $value ) { return $fail( __( 'Choose a date and time.', 'acquisitionsuite' ) ); } if ( ! $editing && $d->getTimestamp() < time() ) { return $fail( __( 'Choose a date and time in the future.', 'acquisitionsuite' ) ); } return $value; case 'number': if ( ! is_numeric( $value ) || (float) $value < 0 ) { return $fail( __( 'Enter a number.', 'acquisitionsuite' ) ); } return (string) round( (float) $value, 2 ); case 'select': return isset( $def['options'][ $value ] ) ? $value : $fail( __( 'Choose one of the options.', 'acquisitionsuite' ) ); case 'textarea': return substr( sanitize_textarea_field( $value ), 0, 1000 ); default: return substr( sanitize_text_field( $value ), 0, 200 ); } } /** * The values a funnel offers as {tags} in its after-submission message and its delivery email, on top of {full_name}, * {email}, {file_name} and {site_name}. They come from the funnel's own answers, so editing the funnel changes what * the next registrant is told. * * @return array tag (with braces) => value */ public static function tags( $template_id, array $answers ) { $tpl = self::get( $template_id ); if ( $tpl && ! empty( $tpl['tags'] ) && is_callable( $tpl['tags'] ) ) { return (array) call_user_func( $tpl['tags'], $answers ); } return array(); } public static function tags_waitlist( array $a ) { $for = trim( (string) ( $a['extra']['waitlist_for'] ?? '' ) ); return array( '{waitlist_for}' => '' !== $for ? $for : (string) get_bloginfo( 'name' ) ); } public static function tags_book_call( array $a ) { $url = (string) ( $a['extra']['booking_url'] ?? '' ); return array( '{booking_link}' => '' !== $url ? $url : (string) home_url( '/' ) ); } public static function tags_webinar( array $a ) { $e = (array) ( $a['extra'] ?? array() ); $title = (string) ( $e['event_title'] ?? $a['name'] ?? '' ); $link = (string) ( $e['event_link'] ?? '' ); $start = ! empty( $e['event_start'] ) ? DateTimeImmutable::createFromFormat( 'Y-m-d\TH:i', (string) $e['event_start'], wp_timezone() ) : false; $date = $time = $cal = ''; if ( $start ) { $mins = max( 5, (int) ( $e['event_length'] ?? 60 ) ); $end = $start->modify( '+' . $mins . ' minutes' ); $date = wp_date( get_option( 'date_format' ), $start->getTimestamp() ); $time = wp_date( get_option( 'time_format' ) . ' T', $start->getTimestamp() ); $utc = new DateTimeZone( 'UTC' ); // add_query_arg() does not encode its values, and a title has spaces: build the query properly. $cal = 'https://calendar.google.com/calendar/render?' . http_build_query( array( 'action' => 'TEMPLATE', 'text' => $title, 'dates' => $start->setTimezone( $utc )->format( 'Ymd\THis\Z' ) . '/' . $end->setTimezone( $utc )->format( 'Ymd\THis\Z' ), 'details' => '' !== $link ? $link : '', ), '', '&', PHP_QUERY_RFC3986 ); } return array( '{webinar_title}' => $title, '{webinar_date}' => $date, '{webinar_time}' => $time, '{webinar_link}' => '' !== $link ? $link : __( '(the link will be sent before the start)', 'acquisitionsuite' ), '{webinar_calendar_link}' => $cal, ); } }