false, 'mode' => Embed::MODE_POPUP, 'where' => self::WHERE_ALL, 'pages' => array(), 'posts' => false, 'trigger' => Embed::TRIGGER_DELAY, 'delay' => Embed::DEFAULT_DELAY_SECONDS, 'scroll' => Embed::DEFAULT_SCROLL_PERCENT, 'frequency' => Embed::DEFAULT_FREQUENCY_DAYS, 'label' => '', 'position' => 'bottom-right', 'launcher' => false, 'hide_paths' => '', 'hide_shop' => true, ); } /** * Every form's placement, keyed by the form's public id. The old * "Show a form on every page" setting is moved in here the first time. */ public static function all() { $saved = get_option( self::OPTION, null ); if ( ! is_array( $saved ) ) { $saved = self::from_site_display(); update_option( self::OPTION, $saved, true ); } $all = array(); foreach ( $saved as $form_id => $placement ) { if ( Embed::is_valid_form_id( (string) $form_id ) && is_array( $placement ) ) { $all[ (string) $form_id ] = array_merge( self::defaults(), $placement ); } } return $all; } /** One form's placement (the defaults when it has none). */ public static function for_form( $form_id ) { $all = self::all(); return isset( $all[ $form_id ] ) ? $all[ $form_id ] : self::defaults(); } /** The old single "every page" setting, as a placement - or nothing. */ private static function from_site_display() { $old = Site_Display::get(); if ( empty( $old['enabled'] ) || ! Embed::is_valid_form_id( (string) $old['form'] ) ) { return array(); } $placement = array_intersect_key( $old, self::defaults() ); $placement['enabled'] = true; $placement['where'] = self::WHERE_ALL; return array( (string) $old['form'] => array_merge( self::defaults(), $placement ) ); } /** * Saves a form's placement from its page's fields, cleaned the same way * as a block or shortcode. * * @param string $form_id The form's public id. * @param array $raw Unslashed POST fields: show, where, pages[], posts, trigger, delay, scroll, frequency, label, position, launcher, hide_shop. */ public static function save_for_form( $form_id, array $raw ) { if ( ! Embed::is_valid_form_id( $form_id ) ) { return; } $show = isset( $raw['show'] ) ? (string) $raw['show'] : self::SHOW_NONE; $options = Embed::normalize_options( array( 'form' => $form_id, 'mode' => self::SHOW_NONE === $show ? Embed::MODE_POPUP : $show, 'trigger' => isset( $raw['trigger'] ) ? $raw['trigger'] : Embed::TRIGGER_DELAY, 'delay' => isset( $raw['delay'] ) ? $raw['delay'] : null, 'scroll' => isset( $raw['scroll'] ) ? $raw['scroll'] : null, 'frequency' => isset( $raw['frequency'] ) ? $raw['frequency'] : null, 'label' => isset( $raw['label'] ) ? $raw['label'] : '', 'position' => isset( $raw['position'] ) ? $raw['position'] : 'bottom-right', 'launcher' => ! empty( $raw['launcher'] ), ) ); $previous = self::for_form( $form_id ); $placement = array_merge( $previous, array_intersect_key( (array) $options, self::defaults() ), array( 'enabled' => in_array( $show, array( Embed::MODE_INLINE, Embed::MODE_POPUP, Embed::MODE_FLOATING ), true ), 'where' => isset( $raw['where'] ) && self::WHERE_CHOSEN === $raw['where'] ? self::WHERE_CHOSEN : self::WHERE_ALL, 'pages' => self::clean_page_ids( isset( $raw['pages'] ) ? (array) $raw['pages'] : array() ), 'posts' => ! empty( $raw['posts'] ), 'hide_shop' => ! empty( $raw['hide_shop'] ), ) ); $all = self::all(); $all[ $form_id ] = $placement; update_option( self::OPTION, $all, true ); } /** Whole, positive page ids, each once, at most MAX_PAGES. */ public static function clean_page_ids( array $ids ) { $clean = array(); foreach ( $ids as $id ) { if ( is_scalar( $id ) && 1 === preg_match( '/^[1-9][0-9]{0,18}$/', (string) $id ) ) { $clean[] = (int) $id; } } return array_slice( array_values( array_unique( $clean ) ), 0, self::MAX_PAGES ); } /** * Whether a placement shows on the page being viewed. * * @param array $placement One placement (output of for_form()). * @param array $context Keys: path (e.g. "/about/"), page_id (the viewed post/page id, 0 when it isn't one), is_post (a blog post), is_shop (a WooCommerce cart/checkout/account page). */ public static function matches( array $placement, array $context ) { if ( empty( $placement['enabled'] ) ) { return false; } if ( ! empty( $context['is_shop'] ) && ! empty( $placement['hide_shop'] ) ) { return false; } if ( self::WHERE_CHOSEN === $placement['where'] ) { $page_id = isset( $context['page_id'] ) ? (int) $context['page_id'] : 0; return ( $page_id > 0 && in_array( $page_id, array_map( 'intval', (array) $placement['pages'] ), true ) ) || ( ! empty( $placement['posts'] ) && ! empty( $context['is_post'] ) ); } // Every page, minus any listed in the old "Not on these pages" (kept from the earlier setting). $setting = array( 'enabled' => true, 'form' => 'placement', 'hide_paths' => (string) $placement['hide_paths'], 'hide_shop' => false, ); return Site_Display::should_show( $setting, isset( $context['path'] ) ? (string) $context['path'] : '/', false ); } /** The one pop-up or corner button for this page - the first that matches - or null. */ public static function overlay_for( array $all, array $context ) { foreach ( $all as $form_id => $placement ) { if ( Embed::MODE_INLINE !== $placement['mode'] && self::matches( $placement, $context ) ) { return array_merge( $placement, array( 'form' => (string) $form_id ) ); } } return null; } /** Every in-page form for this page, in order. */ public static function inline_for( array $all, array $context ) { $found = array(); foreach ( $all as $form_id => $placement ) { if ( Embed::MODE_INLINE === $placement['mode'] && self::matches( $placement, $context ) ) { $found[] = array_merge( $placement, array( 'form' => (string) $form_id ) ); } } return $found; } /** wp_footer: the page's pop-up or corner button. */ public static function print_footer_script() { if ( is_admin() || is_feed() || is_embed() || ! Settings::is_connected() ) { return; } $overlay = self::overlay_for( self::all(), self::current_context() ); if ( null !== $overlay ) { echo Embed::render( $overlay ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built by wp_get_script_tag(), which escapes every attribute. } } /** * the_content: in-page forms at the end of the viewed page's own content - * only the main page (not excerpts, widgets or other posts in a list). * * @param string $content The page's content. */ public static function append_to_content( $content ) { if ( is_admin() || is_feed() || ! is_singular() || ! in_the_loop() || ! is_main_query() || ! Settings::is_connected() ) { return $content; } $post_id = (int) get_the_ID(); if ( $post_id !== (int) get_queried_object_id() || isset( self::$appended[ $post_id ] ) ) { return $content; } $forms = self::inline_for( self::all(), self::current_context() ); if ( empty( $forms ) ) { return $content; } self::$appended[ $post_id ] = true; foreach ( $forms as $form ) { $content .= "\n" . '
' . Embed::render( $form ) . '
'; } return $content; } /** Forgets a form's placement (e.g. when it's archived). */ public static function remove_form( $form_id ) { $all = self::all(); unset( $all[ $form_id ] ); update_option( self::OPTION, $all, true ); } private static function current_context() { $path = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ), PHP_URL_PATH ) : '/'; $home = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ); if ( '' !== $home && '/' !== $home && 0 === strpos( $path, rtrim( $home, '/' ) ) ) { // A site installed in a subfolder: compare paths within the site. $path = substr( $path, strlen( rtrim( $home, '/' ) ) ); } return array( 'path' => $path, // The blog's own page (Settings → Reading → Posts page) counts as that page too. 'page_id' => is_singular() ? (int) get_queried_object_id() : ( is_home() ? (int) get_option( 'page_for_posts' ) : 0 ), 'is_post' => is_singular( 'post' ), 'is_shop' => ( function_exists( 'is_cart' ) && is_cart() ) || ( function_exists( 'is_checkout' ) && is_checkout() ) || ( function_exists( 'is_account_page' ) && is_account_page() ), ); } }