([^<]*)$#'; /** * Valid templates, keyed by id (cached per request). * * @var array>|null */ private static ?array $all = null; /** * Template categories, in display order: key => label. * * @return array */ public static function categories(): array { $categories = array( 'basic' => __( 'Basic', 'flexsolutions-form-builder' ), 'marketing' => __( 'Marketing', 'flexsolutions-form-builder' ), 'events' => __( 'Events', 'flexsolutions-form-builder' ), 'feedback' => __( 'Feedback', 'flexsolutions-form-builder' ), 'recruiting' => __( 'Recruiting', 'flexsolutions-form-builder' ), 'support' => __( 'Support', 'flexsolutions-form-builder' ), ); /** * Filters the template categories shown in the template picker. * * @param array $categories Category key => label. */ $categories = apply_filters( 'tflexforms_template_categories', $categories ); return is_array( $categories ) ? array_map( 'strval', $categories ) : array(); } /** * Every valid template, sorted by order then title, keyed by id. Each has * id, title, description, category, order, schema (Form_Schema), mail and * messages. A template whose fields are not a valid schema is left out. * * @return array> */ public static function all(): array { if ( null !== self::$all ) { return self::$all; } $raw = self::files(); /** * Filters the form templates before they are checked: add, change or * remove templates. Each is an array in the format of the plugin's * templates/*.json files (title, description, category, order, fields, * layout, mail, messages), keyed by a unique id. * * @param array> $raw Templates by id. */ $raw = apply_filters( 'tflexforms_templates', $raw ); $categories = self::categories(); $all = array(); foreach ( is_array( $raw ) ? $raw : array() as $id => $data ) { $id = sanitize_key( (string) $id ); $template = '' !== $id && is_array( $data ) ? self::normalize( $id, self::translate( $data ), $categories ) : null; if ( null !== $template ) { $all[ $id ] = $template; } } uasort( $all, static fn( array $a, array $b ): int => array( $a['order'], $a['title'] ) <=> array( $b['order'], $b['title'] ) ); self::$all = $all; return $all; } /** * One template, or null when there is no valid template with that id. * * @param string $id Template id. * @return array|null */ public static function get( string $id ): ?array { return self::all()[ $id ] ?? null; } /** * Forgets the cached templates (after filters change, in tests). */ public static function reset(): void { self::$all = null; } /** * Gives a form a template's fields, mail settings and messages. * * @param int $form_id Form post ID. * @param string $id Template id. */ public static function apply( int $form_id, string $id ): bool { $template = self::get( $id ); if ( null === $template ) { return false; } $meta = Schema_Migration::store_meta( $template['schema'] ); $meta[ Form_Post_Type::META_MAIL ] = $template['mail']; if ( array() !== $template['messages'] ) { $meta[ Form_Post_Type::META_MESSAGES ] = $template['messages']; } Schema_Migration::write_meta( $form_id, $meta ); return true; } /** * The text of a template that is translated: title, description, the * fields' labels, help, placeholders, button and choice text, headings, * mail subjects and bodies, and messages. * * @param array $data Template file data. * @return string[] */ public static function strings( array $data ): array { $strings = array(); self::walk( $data, static function ( string $text ) use ( &$strings ): string { $strings[] = $text; return $text; } ); return array_values( array_unique( $strings ) ); } /** * Decoded template files by id. * * @return array> */ public static function files(): array { $out = array(); $files = glob( PLUGIN_DIR . '/' . self::DIR . '/*.json' ); foreach ( false !== $files ? $files : array() as $file ) { $json = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local plugin file. $data = false !== $json ? json_decode( $json, true ) : null; if ( is_array( $data ) ) { $out[ basename( $file, '.json' ) ] = $data; } } return $out; } /** * A template with its text translated. * * @param array $data Template data. * @return array */ private static function translate( array $data ): array { $map = Template_Strings::all(); return self::walk( $data, static fn( string $text ): string => $map[ $text ] ?? $text ); } /** * Applies a callback to each translated text of a template. * * @param array $data Template data. * @param callable(string): string $callback Callback. * @return array */ private static function walk( array $data, callable $callback ): array { $text = static function ( array $item, array $keys ) use ( $callback ): array { foreach ( $keys as $key ) { if ( isset( $item[ $key ] ) && is_string( $item[ $key ] ) && '' !== $item[ $key ] ) { $item[ $key ] = $callback( $item[ $key ] ); } } return $item; }; $data = $text( $data, array( 'title', 'description' ) ); if ( isset( $data['fields'] ) && is_array( $data['fields'] ) ) { foreach ( $data['fields'] as $i => $field ) { if ( ! is_array( $field ) ) { continue; } $field = $text( $field, array( 'label', 'help', 'placeholder', 'text', 'firstLabel' ) ); // A section heading's text (other HTML is not translated). if ( isset( $field['content'] ) && is_string( $field['content'] ) && 1 === preg_match( self::HEADING, $field['content'], $m ) ) { $field['content'] = '' . esc_html( $callback( html_entity_decode( $m[2], ENT_QUOTES ) ) ) . ''; } if ( isset( $field['choices'] ) && is_array( $field['choices'] ) ) { foreach ( $field['choices'] as $c => $choice ) { $field['choices'][ $c ] = is_array( $choice ) ? $text( $choice, array( 'label' ) ) : $choice; } } $data['fields'][ $i ] = $field; } } if ( isset( $data['mail'] ) && is_array( $data['mail'] ) ) { $data['mail'] = $text( $data['mail'], array( 'subject', 'body', 'autoreply_subject', 'autoreply_body' ) ); } if ( isset( $data['messages'] ) && is_array( $data['messages'] ) ) { $data['messages'] = $text( $data['messages'], array_keys( $data['messages'] ) ); } return $data; } /** * A checked template, or null when its fields are not a valid schema. * * @param string $id Template id. * @param array $data Translated template data. * @param array $categories Known categories. * @return array|null */ private static function normalize( string $id, array $data, array $categories ): ?array { $doc = array( 'schemaVersion' => Form_Schema::VERSION, 'fields' => isset( $data['fields'] ) && is_array( $data['fields'] ) ? $data['fields'] : array(), ); if ( isset( $data['layout'] ) && is_array( $data['layout'] ) ) { $doc['layout'] = $data['layout']; } $schema = array() !== $doc['fields'] ? Form_Schema::from_array( $doc ) : null; if ( null === $schema ) { return null; } $category = isset( $data['category'] ) ? sanitize_key( (string) $data['category'] ) : ''; return array( 'id' => $id, 'title' => sanitize_text_field( (string) ( $data['title'] ?? $id ) ), 'description' => sanitize_text_field( (string) ( $data['description'] ?? '' ) ), 'category' => isset( $categories[ $category ] ) ? $category : (string) array_key_first( $categories ), 'order' => (int) ( $data['order'] ?? 100 ), 'schema' => $schema, 'mail' => Form::sanitize_mail_settings( array_merge( Form::default_mail_settings(), isset( $data['mail'] ) && is_array( $data['mail'] ) ? $data['mail'] : array() ) ), 'messages' => isset( $data['messages'] ) && is_array( $data['messages'] ) ? Form::sanitize_messages( $data['messages'] ) : array(), ); } }