*/ public static function stage_post_types() { return array( 'media' => array( 'attachment' ), 'patterns' => array( 'wp_block' ), 'content' => array( 'page', 'post' ), 'navigation' => array( 'wp_navigation' ), 'templates' => array( 'wp_template', 'wp_template_part' ), 'styles' => array( 'wp_global_styles' ), ); } /** * Whether a stage is allowed to deliver an object of one post type. * * The stage a batch was asked for is the only thing the receiving side * decided; everything in the answer was written by the other end. A demo * that answers the templates stage with a page, or with a post type of its * own invention, is either broken or trying something, and either way the * object does not belong in the stage that asked for it. Checked rather * than assumed, so an import can only ever create the kinds of object the * plan below names. * * @param string $stage Stage name. * @param string $type Post type the item claims to be. * @param array $plugins Plugins this demo declared. * @return bool */ public static function stage_allows_type( $stage, $type, $plugins = array() ) { $type = (string) $type; if ( '' === $type ) { return false; } return in_array( $type, self::post_types_for( $stage, $plugins ), true ); } /** * Media types an import will store. * * Narrower than what the site accepts from its own administrators, because * these files arrive over the network from a document. Anything that is * not a picture, a font or a plain document has no place in a demo. * * @return string[] */ public static function importable_mime_types() { return array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', 'image/x-icon', 'image/vnd.microsoft.icon', 'video/mp4', 'video/webm', 'audio/mpeg', 'application/pdf', 'font/woff', 'font/woff2', 'font/ttf', 'application/font-woff', 'application/font-woff2', ); } /** * Post types in one stage. * * @param string $stage Stage name. * @param array $plugins Plugins this demo uses, for the plugin-content stage. * @return string[] */ public static function post_types_for( $stage, $plugins = array() ) { if ( 'plugin-content' === $stage ) { return self::plugin_post_types( $plugins ); } $map = self::stage_post_types(); return isset( $map[ $stage ] ) ? $map[ $stage ] : array(); } /** * Post types owned by the plugins a demo uses. * * A WPForms block or shortcode is useless without the form it points at, * so the form travels with the demo and the page is pointed at the copy * made here. * * @param array $plugins Plugin entries, as the manifest lists them. * @return string[] */ public static function plugin_post_types( $plugins = array() ) { $signatures = self::plugin_signatures(); $types = array(); foreach ( (array) $plugins as $plugin ) { $slug = isset( $plugin['slug'] ) ? $plugin['slug'] : ''; if ( ! $slug || empty( $signatures[ $slug ]['postTypes'] ) ) { continue; } foreach ( (array) $signatures[ $slug ]['postTypes'] as $type ) { if ( post_type_exists( $type ) ) { $types[] = $type; } } } return array_values( array_unique( $types ) ); } /** * A WPForms form with everything belonging to the demo site taken out. * * Where a form's notifications go is the site owner's business, not the * demo's. A demo's endpoints are public, so an address typed into a * notification would be handed to anyone who asked, and a demo that set * one could have a customer's enquiries sent somewhere else. Connections * to outside services - marketing lists, payment accounts, webhooks - are * the demo site's own accounts and do not travel at all. Run by the demo * when it exports and again by the site that imports. * * @param mixed $form Form data, decoded. * @return array */ public static function scrub_form( $form ) { $form = is_array( $form ) ? $form : array(); unset( $form['providers'], $form['payments'] ); if ( ! isset( $form['settings'] ) || ! is_array( $form['settings'] ) ) { return $form; } unset( $form['settings']['webhooks'], $form['settings']['webhooks_enable'] ); if ( isset( $form['settings']['notifications'] ) && is_array( $form['settings']['notifications'] ) ) { foreach ( $form['settings']['notifications'] as $key => $notification ) { $form['settings']['notifications'][ $key ] = self::scrub_form_notification( $notification ); } } return $form; } /** * One WPForms notification, addressed by the site that receives it. * * It goes to that site's administrator and is sent from that address, * both by WPForms' own {admin_email} smart tag, which WPForms fills in * from the site's settings when it sends. Reply-to may only name a field * of the form, so a reply goes to whoever filled it in. The sender's name * is the site's own unless it is made only of smart tags. A literal * address anywhere else in the notification is replaced too. * * @param mixed $notification Notification settings. * @return array */ public static function scrub_form_notification( $notification ) { $notification = is_array( $notification ) ? $notification : array(); unset( $notification['carboncopy'] ); $notification['email'] = '{admin_email}'; $notification['sender_address'] = '{admin_email}'; $replyto = isset( $notification['replyto'] ) && is_string( $notification['replyto'] ) ? trim( $notification['replyto'] ) : ''; $notification['replyto'] = preg_match( '/^\{field_id="\d+"\}$/', $replyto ) ? $replyto : ''; $sender = isset( $notification['sender_name'] ) && is_string( $notification['sender_name'] ) ? trim( $notification['sender_name'] ) : ''; $notification['sender_name'] = preg_match( '/^(\s*\{[a-z_]+\}\s*)+$/', $sender ) ? $sender : '{site_name}'; foreach ( array( 'notification_name', 'subject', 'message' ) as $key ) { if ( isset( $notification[ $key ] ) && is_string( $notification[ $key ] ) ) { $notification[ $key ] = preg_replace( '/[^\s<>"\'{}\[\](),;:]+@[^\s<>"\'{}\[\](),;:]+\.[a-z]{2,}/i', '{admin_email}', $notification[ $key ] ); } } return $notification; } /** * Site options that travel in the settings stage. * * Deliberately a fixed list. A demo must not be able to change a customer's * site in ways they did not ask for, so only options that are part of how a * demo looks are carried. * * @return string[] */ public static function transferred_options() { return array( 'blogname', 'blogdescription', 'show_on_front', 'page_on_front', 'page_for_posts', 'posts_per_page', 'permalink_structure', 'site_logo', 'site_icon', 'date_format', 'time_format', 'start_of_week', ); } /** * Options in the settings stage whose value is a post id. * * These must be rewritten through the id map before they are stored. * * @return string[] */ public static function post_id_options() { return array( 'page_on_front', 'page_for_posts', 'site_logo', 'site_icon', ); } /** * Block attribute names holding a single object id. * * @return string[] */ public static function id_attributes() { return array( 'id', 'mediaId', 'backgroundId', 'imageId', 'iconId', 'ref', 'postId', 'featuredImageId', ); } /** * Block attribute names holding an array of object ids. * * @return string[] */ public static function id_list_attributes() { return array( 'ids', 'includes', 'selectedTerms', ); } /** * Block attribute names holding a URL. * * @return string[] */ public static function url_attributes() { return array( 'url', 'src', 'href', 'mediaLink', 'poster', 'backgroundImage', 'iconUrl', ); } /** * Plugins a demo's content can depend on, and how to recognise them. * * A demo site's author should not have to remember to declare that the * contact page holds a WPForms form. The source site reads its * own content and works it out, so what the manifest declares is what the * demo actually uses. * * Only plugins the demos actually use are listed. Recognising a plugin * nobody here builds with is not free: it means offering to install * something on a customer's site off the back of a shortcode that was never * tested against, and it invites the list to grow into a catalogue of the * whole plugin directory. Add an entry when a demo needs one. * * @return array */ public static function plugin_signatures() { return array( 'wpforms-lite' => array( 'name' => 'WPForms Lite', 'source' => 'wordpress.org', 'shortcodes' => array( 'wpforms' ), 'blocks' => array( 'wpforms/form-selector' ), // The form itself, so the demo's fields and settings // travel. WPForms keeps a whole form as JSON in the post // body, so no protected meta is needed. 'postTypes' => array( 'wpforms' ), // WPForms Pro lives in its own directory and does the // same job; a site running it needs nothing installed. 'providedBy' => array( 'wpforms' ), // The block attribute that names the form to show. 'formAttribute' => 'formId', ), ); } /** * Themes an import may install and activate. * * A demo names the theme it is built for, but which theme code comes onto * a customer's site is decided here, not by the demo server. Add a theme * here when a demo is built on one. * * @return array Theme name, keyed by WordPress.org slug. */ public static function approved_themes() { return array( PAGECORE_THEME_SLUG => 'PageCore FSE', ); } /** * The theme a demo's manifest asks for, checked against approved_themes(). * * An approved theme is described from this list, not from the manifest. * Any other theme is kept only to tell the customer about it, marked * unsupported, and is never installed or activated. * * @param mixed $theme Theme block as the manifest wrote it. * @return array Empty when the manifest names no theme. */ public static function demo_theme( $theme ) { $theme = is_array( $theme ) ? $theme : array(); $slug = isset( $theme['slug'] ) && is_string( $theme['slug'] ) ? sanitize_key( $theme['slug'] ) : ''; if ( '' === $slug ) { return array(); } $approved = self::approved_themes(); if ( isset( $approved[ $slug ] ) ) { return array( 'slug' => $slug, 'name' => $approved[ $slug ], ); } $name = isset( $theme['name'] ) && is_scalar( $theme['name'] ) ? sanitize_text_field( (string) $theme['name'] ) : ''; return array( 'slug' => $slug, 'name' => '' !== $name ? $name : $slug, 'unsupported' => true, ); } /** * The plugins a demo's manifest asks for, sorted against plugin_signatures(). * * Only a plugin this file knows may be installed or activated, and its * name and source come from here, not from the manifest. Anything else is * kept only to tell the customer about it. * * @param mixed $plugins requiredPlugins as the manifest wrote it. * @return array{supported: array[], other: array[]} */ public static function demo_plugins( $plugins ) { $signatures = self::plugin_signatures(); $supported = array(); $other = array(); foreach ( array_slice( is_array( $plugins ) ? $plugins : array(), 0, 20 ) as $plugin ) { $slug = is_array( $plugin ) && isset( $plugin['slug'] ) && is_string( $plugin['slug'] ) ? sanitize_key( $plugin['slug'] ) : ''; if ( '' === $slug || isset( $supported[ $slug ] ) || isset( $other[ $slug ] ) ) { continue; } if ( isset( $signatures[ $slug ] ) ) { $supported[ $slug ] = array( 'slug' => $slug, 'name' => isset( $signatures[ $slug ]['name'] ) ? (string) $signatures[ $slug ]['name'] : $slug, 'source' => isset( $signatures[ $slug ]['source'] ) ? (string) $signatures[ $slug ]['source'] : 'wordpress.org', 'detected' => ! empty( $plugin['detected'] ), ); continue; } $name = isset( $plugin['name'] ) && is_scalar( $plugin['name'] ) ? sanitize_text_field( (string) $plugin['name'] ) : ''; $other[ $slug ] = array( 'slug' => $slug, 'name' => '' !== $name ? $name : $slug, ); } return array( 'supported' => array_values( $supported ), 'other' => array_values( $other ), ); } /** * What to tell the customer about a plugin a demo uses that PageCore does * not install. * * @param string $name Plugin name, or its slug. * @return string */ public static function other_plugin_message( $name ) { return sprintf( /* translators: %s: plugin name. */ __( 'This demo also uses %s. PageCore won\'t install it, so install it yourself if you need it.', 'pagecore' ), $name ); } /** * The blocks that embed a form, from PageCore's own list of plugins. * * @return array Name of the attribute holding the form, keyed by block name. */ public static function form_blocks() { $blocks = array(); foreach ( self::plugin_signatures() as $signature ) { if ( empty( $signature['formAttribute'] ) ) { continue; } foreach ( (array) ( isset( $signature['blocks'] ) ? $signature['blocks'] : array() ) as $block ) { $blocks[ (string) $block ] = (string) $signature['formAttribute']; } } return $blocks; } /** * The form plugins a piece of markup is waiting for. * * A form block whose plugin is not active is only a marker; the customer * has to activate the plugin before a form can be chosen in it. * * @param string $content Serialized blocks. * @return string[] Plugin names. */ public static function inactive_form_plugins( $content ) { $content = (string) $content; $names = array(); foreach ( self::plugin_signatures() as $slug => $signature ) { if ( empty( $signature['formAttribute'] ) ) { continue; } foreach ( (array) ( isset( $signature['blocks'] ) ? $signature['blocks'] : array() ) as $block ) { if ( false !== strpos( $content, '