PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.6.0
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.6.0
3.6.0 3.5.3 3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / addons / StyleCustomizer / V2 / Templates.php
everest-forms / addons / StyleCustomizer / V2 Last commit date
assets 1 week ago BuilderPanel.php 1 week ago Compiler.php 1 week ago Engine.php 1 week ago FrontendEnqueue.php 1 week ago Migrator.php 1 week ago Palettes.php 1 week ago PreviewDraft.php 1 week ago RestController.php 1 week ago Sanitizer.php 1 week ago Schema.php 1 week ago Templates.php 1 week ago
Templates.php
394 lines
1 <?php
2 /**
3 * Style Customizer v2 — built-in style templates.
4 *
5 * Two bundled sets: the CURRENT default gallery (`assets/wp-json/default-templates-v2.json`,
6 * hand-authored directly in v2 token shape — no migration needed) is what {@see all()} shows in
7 * the picker. The original v1 template set (`assets/wp-json/default-templates.json`, converted
8 * via {@see Migrator::migrate_record()}) is kept loaded but flagged `legacy => true` and filtered
9 * out of the browsable grid (see the JS Templates pane) — existing forms that already have one of
10 * those applied keep matching it correctly (the "applied"/"Modified" badge logic and
11 * {@see resolve_legacy_slug()} both read the full, unfiltered list), they're just no longer
12 * offered to new selections.
13 *
14 * @package EverestForms\Addons\StyleCustomizer\V2
15 * @since x.x.x
16 */
17
18 namespace EverestForms\Addons\StyleCustomizer\V2;
19
20 defined( 'ABSPATH' ) || exit;
21
22 /**
23 * Template provider — the current default gallery, plus the legacy v1 set for continuity.
24 */
25 final class Templates {
26
27 /**
28 * Relative path (from the everest-forms plugin root) to the legacy v1 template set.
29 */
30 const JSON_PATH = 'addons/StyleCustomizer/assets/wp-json/default-templates.json';
31
32 /**
33 * Relative path to the current default gallery — already in v2 token shape.
34 */
35 const V2_JSON_PATH = 'addons/StyleCustomizer/assets/wp-json/default-templates-v2.json';
36
37 /**
38 * Option holding user-created v2 style templates.
39 */
40 const USER_OPTION = 'everest_forms_style_v2_user_templates';
41
42 /**
43 * The built-in templates that are FREE (usable without Pro). Matched by template name.
44 * The first four are the legacy v1 set (kept for existing forms); "Clearline" and "Ledger"
45 * are the current gallery's free entries.
46 */
47 const FREE_TEMPLATES = array( 'Default Template', 'Classic Template', 'In-Line Flair', 'Classic Flow', 'Clearline', 'Ledger' );
48
49 /**
50 * Memoized template list.
51 *
52 * @var array|null
53 */
54 protected static $cache = null;
55
56 /**
57 * All templates as v2 records: [ { id, name, image, palette, tokens, is_pro, legacy } ].
58 * Current gallery first, then the legacy v1 set (flagged, hidden from the picker's grid).
59 *
60 * @return array
61 */
62 public static function all() {
63 if ( null !== self::$cache ) {
64 return self::$cache;
65 }
66
67 $out = array_merge( self::load_current(), self::load_legacy() );
68
69 /**
70 * Filter the v2 style templates.
71 *
72 * @param array $out Templates.
73 */
74 self::$cache = apply_filters( 'evf_style_v2_templates', $out );
75 return self::$cache;
76 }
77
78 /**
79 * The current default gallery — already authored in v2 token shape, no migration needed.
80 *
81 * @return array
82 */
83 protected static function load_current() {
84 $out = array();
85 foreach ( self::load_json( self::V2_JSON_PATH ) as $tpl ) {
86 if ( empty( $tpl['name'] ) || empty( $tpl['tokens'] ) || ! is_array( $tpl['tokens'] ) ) {
87 continue;
88 }
89 $out[] = array(
90 'id' => isset( $tpl['id'] ) ? (string) $tpl['id'] : sanitize_key( $tpl['name'] ),
91 'name' => (string) $tpl['name'],
92 'image' => self::image_url_explicit( $tpl ),
93 'palette' => isset( $tpl['palette'] ) ? (string) $tpl['palette'] : '',
94 'is_pro' => ! in_array( (string) $tpl['name'], self::FREE_TEMPLATES, true ),
95 'legacy' => false,
96 'tokens' => $tpl['tokens'],
97 );
98 }
99 return $out;
100 }
101
102 /**
103 * The legacy v1 template set, migrated to v2 token shape. Kept loaded (never shown in the
104 * picker grid) so a form that already has one applied still matches it correctly.
105 *
106 * @return array
107 */
108 protected static function load_legacy() {
109 $out = array();
110 foreach ( self::load() as $tpl ) {
111 if ( empty( $tpl['name'] ) || empty( $tpl['data'] ) || ! is_array( $tpl['data'] ) ) {
112 continue;
113 }
114 $record = Migrator::migrate_record( $tpl['data'] );
115 $out[] = array(
116 'id' => sanitize_key( $tpl['name'] ),
117 'name' => (string) $tpl['name'],
118 'image' => self::image_url( $tpl ),
119 'palette' => '',
120 'is_pro' => ! in_array( (string) $tpl['name'], self::FREE_TEMPLATES, true ),
121 'legacy' => true,
122 'tokens' => isset( $record['tokens'] ) ? $record['tokens'] : array(),
123 );
124 }
125 return $out;
126 }
127
128 /**
129 * Whether a template id refers to a FREE built-in template (usable without Pro).
130 *
131 * @param string $id Template id.
132 * @return bool
133 */
134 public static function is_free_template_id( $id ) {
135 $id = (string) $id;
136 if ( '' === $id ) {
137 return true; // "no template" is always allowed.
138 }
139 foreach ( self::all() as $tpl ) {
140 if ( $tpl['id'] === $id ) {
141 return empty( $tpl['is_pro'] );
142 }
143 }
144 return false; // Unknown / user template → treat as Pro.
145 }
146
147 /**
148 * Resolve a legacy template slug to its v2 template id, so {@see Migrator::migrate_record()}
149 * can carry the v1 "selected template" across migration.
150 *
151 * @param string $slug Legacy template slug.
152 * @return string v2 template id, or '' if the slug is empty/unset.
153 */
154 public static function resolve_legacy_slug( $slug ) {
155 $slug = (string) $slug;
156 if ( '' === $slug ) {
157 return '';
158 }
159 foreach ( self::load() as $key => $tpl ) {
160 if ( (string) $key === $slug ) {
161 return ! empty( $tpl['name'] ) ? sanitize_key( (string) $tpl['name'] ) : '';
162 }
163 }
164 return 'legacy-' . sanitize_key( $slug );
165 }
166
167 /**
168 * Raw v1 template `data` for a legacy slug — the same "unsaved setting falls back to the
169 * active template's control default" values v1's WP_Customize_Setting resolution feeds
170 * {@see EVF_Style_Customizer_API::compile_scss()}, so a template's own colours/background
171 * can be live on a form's frontend without ever being persisted to its own
172 * `everest_forms_styles` record. {@see Migrator::migrate_record()} merges this in before
173 * mapping so the panel sees the same resolved values the frontend already renders.
174 *
175 * Checks the live `evf_style_templates` option first (built-ins plus any admin-saved custom
176 * template), falling back to the bundled JSON if that option was never seeded.
177 *
178 * @param string $slug Legacy template slug.
179 * @return array Template `data`, or an empty array if the slug is empty/unknown.
180 */
181 public static function legacy_template_data( $slug ) {
182 $slug = (string) $slug;
183 if ( '' === $slug ) {
184 return array();
185 }
186 $raw = get_option( 'evf_style_templates', '' );
187 $stored = is_string( $raw ) ? json_decode( $raw, true ) : $raw;
188 if ( is_array( $stored ) && isset( $stored[ $slug ]['data'] ) && is_array( $stored[ $slug ]['data'] ) ) {
189 return $stored[ $slug ]['data'];
190 }
191 $bundled = self::load();
192 return isset( $bundled[ $slug ]['data'] ) && is_array( $bundled[ $slug ]['data'] ) ? $bundled[ $slug ]['data'] : array();
193 }
194
195 /**
196 * User-created templates ("save current styles as a template"), newest first, plus any
197 * legacy custom template carried over (see {@see legacy_custom_templates()}).
198 *
199 * @return array [ { id, name, custom:true, image:'', palette, tokens } ]
200 */
201 public static function user_templates() {
202 $stored = get_option( self::USER_OPTION, array() );
203 $out = array();
204 if ( is_array( $stored ) ) {
205 foreach ( $stored as $tpl ) {
206 if ( empty( $tpl['id'] ) || ! isset( $tpl['tokens'] ) || ! is_array( $tpl['tokens'] ) ) {
207 continue;
208 }
209 $out[] = array(
210 'id' => (string) $tpl['id'],
211 'name' => isset( $tpl['name'] ) ? (string) $tpl['name'] : __( 'Untitled', 'everest-forms' ),
212 'custom' => true,
213 'image' => '',
214 'palette' => isset( $tpl['palette'] ) ? (string) $tpl['palette'] : '',
215 'tokens' => $tpl['tokens'],
216 );
217 }
218 }
219 return array_merge( $out, self::legacy_custom_templates() );
220 }
221
222 /**
223 * Legacy (v1) custom templates, migrated to v2 token shape on read. `evf_style_templates`
224 * holds both the built-in templates and any custom one saved via the old "Create Style
225 * Template" UI; built-ins are skipped here (matched by name against {@see all()}).
226 *
227 * @return array [ { id, name, custom:true, image:'', palette:'', tokens } ]
228 */
229 protected static function legacy_custom_templates() {
230 // Stored as a JSON string, not a native array — get_option() won't auto-decode it.
231 $raw = get_option( 'evf_style_templates', '' );
232 $stored = is_string( $raw ) ? json_decode( $raw, true ) : $raw;
233 if ( empty( $stored ) || ! is_array( $stored ) ) {
234 return array();
235 }
236 $builtin_names = wp_list_pluck( self::all(), 'name' );
237 $out = array();
238 foreach ( $stored as $slug => $tpl ) {
239 if ( empty( $tpl['name'] ) || empty( $tpl['data'] ) || ! is_array( $tpl['data'] ) ) {
240 continue;
241 }
242 if ( in_array( (string) $tpl['name'], $builtin_names, true ) ) {
243 continue;
244 }
245 $record = Migrator::migrate_record( $tpl['data'] );
246 $out[] = array(
247 'id' => 'legacy-' . sanitize_key( $slug ),
248 'name' => (string) $tpl['name'],
249 'custom' => true,
250 'image' => '',
251 'palette' => '',
252 'tokens' => isset( $record['tokens'] ) ? $record['tokens'] : array(),
253 );
254 }
255 return $out;
256 }
257
258 /**
259 * Delete a user template by id. A `legacy-…` id routes to {@see delete_legacy_custom_template()}.
260 *
261 * @param string $id Template id.
262 * @return bool Whether anything was removed.
263 */
264 public static function delete_user_template( $id ) {
265 $id = (string) $id;
266 if ( 0 === strpos( $id, 'legacy-' ) ) {
267 return self::delete_legacy_custom_template( substr( $id, 7 ) );
268 }
269 $stored = get_option( self::USER_OPTION, array() );
270 if ( ! is_array( $stored ) ) {
271 return false;
272 }
273 $next = array_values(
274 array_filter(
275 $stored,
276 static function ( $tpl ) use ( $id ) {
277 return ! isset( $tpl['id'] ) || (string) $tpl['id'] !== $id;
278 }
279 )
280 );
281 if ( count( $next ) === count( $stored ) ) {
282 return false;
283 }
284 update_option( self::USER_OPTION, $next, false );
285 return true;
286 }
287
288 /**
289 * Remove one entry from the legacy `evf_style_templates` option.
290 *
291 * @param string $slug The original `evf_style_templates` array key (id minus the `legacy-` prefix).
292 * @return bool Whether anything was removed.
293 */
294 protected static function delete_legacy_custom_template( $slug ) {
295 $raw = get_option( 'evf_style_templates', '' );
296 $stored = is_string( $raw ) ? json_decode( $raw, true ) : $raw;
297 if ( ! is_array( $stored ) || ! isset( $stored[ $slug ] ) ) {
298 return false;
299 }
300 unset( $stored[ $slug ] );
301 update_option( 'evf_style_templates', wp_json_encode( $stored ) );
302 return true;
303 }
304
305 /**
306 * Resolve a template's thumbnail to a local plugin URL when it ships with the addon;
307 * otherwise falls back to the JSON's remote URL.
308 *
309 * @param array $tpl Raw template ( name + image ).
310 * @return string
311 */
312 protected static function image_url( $tpl ) {
313 $dir = dirname( __DIR__ ) . '/assets/images/templates/';
314 $base = evf()->plugin_url() . '/addons/StyleCustomizer/assets/images/templates/';
315
316 $candidates = array();
317 if ( ! empty( $tpl['image'] ) ) {
318 $candidates[] = basename( wp_parse_url( (string) $tpl['image'], PHP_URL_PATH ) );
319 }
320 if ( ! empty( $tpl['name'] ) ) {
321 $candidates[] = sanitize_title( $tpl['name'] ) . '.png';
322 }
323
324 foreach ( $candidates as $file ) {
325 if ( $file && is_readable( $dir . $file ) ) {
326 return $base . $file;
327 }
328 }
329
330 return ! empty( $tpl['image'] ) ? esc_url_raw( (string) $tpl['image'] ) : '';
331 }
332
333 /**
334 * Same local/remote resolution as {@see image_url()}, but WITHOUT the guess-by-name fallback —
335 * for the current gallery only, so a new entry never accidentally adopts an unrelated bundled
336 * asset just because `sanitize_title( name ) . '.png'` happens to already exist on disk (that
337 * guess exists for the legacy v1 set's inconsistently-named remote images, not this one). No
338 * `image` field set → '' → the JS picker's live token-driven thumbnail renders instead.
339 *
340 * @param array $tpl Raw template ( name + image ).
341 * @return string
342 */
343 protected static function image_url_explicit( $tpl ) {
344 if ( empty( $tpl['image'] ) ) {
345 return '';
346 }
347 $dir = dirname( __DIR__ ) . '/assets/images/templates/';
348 $base = evf()->plugin_url() . '/addons/StyleCustomizer/assets/images/templates/';
349 $file = basename( wp_parse_url( (string) $tpl['image'], PHP_URL_PATH ) );
350 if ( $file && is_readable( $dir . $file ) ) {
351 return $base . $file;
352 }
353 return esc_url_raw( (string) $tpl['image'] );
354 }
355
356 /**
357 * Load + decode the bundled template JSON (trusted asset). Returns an array of templates.
358 *
359 * @return array
360 */
361 protected static function load() {
362 return self::load_json( self::JSON_PATH );
363 }
364
365 /**
366 * Load + decode a bundled template JSON (trusted asset) at the given plugin-relative path.
367 *
368 * @param string $rel_path Plugin-relative path (e.g. {@see JSON_PATH}).
369 * @return array
370 */
371 protected static function load_json( $rel_path ) {
372 $json = '';
373
374 if ( function_exists( 'evf_file_get_contents' ) ) {
375 $json = evf_file_get_contents( $rel_path );
376 }
377
378 if ( '' === $json || false === $json ) {
379 // __DIR__ is …/addons/StyleCustomizer/V2; the JSON sits in the sibling assets dir.
380 $path = dirname( __DIR__ ) . '/assets/wp-json/' . basename( $rel_path );
381 if ( is_readable( $path ) ) {
382 $json = (string) file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
383 }
384 }
385
386 if ( '' === $json ) {
387 return array();
388 }
389
390 $decoded = json_decode( $json, true );
391 return is_array( $decoded ) ? $decoded : array();
392 }
393 }
394