PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.3.3
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.3.3
3.3.3 3.3.2 3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 All 158 releases
notificationx / includes / Abilities / BuilderInfo.php

BuilderInfo.php in NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar 3.3.3, at includes/Abilities/BuilderInfo.php

355 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shared builder metadata for MCP abilities: the authoritative theme ids and
4 * source→type map the admin wizard uses. Centralised so describe-type,
5 * create-notification and update-notification all agree on what is valid.
6 *
7 * @package NotificationX\Abilities
8 */
9
10 namespace NotificationX\Abilities;
11
12 use NotificationX\Types\TypeFactory;
13 use NotificationX\Extensions\ExtensionFactory;
14 use NotificationX\Core\Modules;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * Read-only lookups over the builder's theme/source registry.
22 */
23 class BuilderInfo {
24
25 /**
26 * @var bool Whether the builder theme filters have been primed this request.
27 */
28 protected static $booted = false;
29
30 /**
31 * @var array Raw nx_themes map (id => entry).
32 */
33 protected static $themes = array();
34
35 /**
36 * @var array Raw nx_res_themes map (id => entry).
37 */
38 protected static $res_themes = array();
39
40 /**
41 * @var array Raw nx_themes_trigger map (theme id => trigger list).
42 */
43 protected static $triggers = array();
44
45 /**
46 * @var bool Whether the theme-trigger map has been primed this request.
47 */
48 protected static $triggers_booted = false;
49
50 /**
51 * Prime the extension builder filters once (same trigger the admin metabox
52 * uses), then cache the theme maps for the rest of the request.
53 *
54 * @return void
55 */
56 public static function boot() {
57 if ( self::$booted ) {
58 return;
59 }
60 self::$booted = true;
61 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- consuming core NotificationX (nx_) hooks.
62 do_action( 'nx_before_metabox_load' );
63 $themes = apply_filters( 'nx_themes', array() );
64 $res = apply_filters( 'nx_res_themes', array() );
65 // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
66 self::$themes = is_array( $themes ) ? $themes : array();
67 self::$res_themes = is_array( $res ) ? $res : array();
68 }
69
70 /**
71 * The raw nx_themes map.
72 *
73 * @return array
74 */
75 public static function all_themes() {
76 self::boot();
77 return self::$themes;
78 }
79
80 /**
81 * The raw nx_res_themes map.
82 *
83 * @return array
84 */
85 public static function all_res_themes() {
86 self::boot();
87 return self::$res_themes;
88 }
89
90 /**
91 * Registered source (extension) objects keyed by id.
92 *
93 * @return array
94 */
95 public static function sources() {
96 $exts = ExtensionFactory::get_instance()->get_all();
97 return is_array( $exts ) ? $exts : array();
98 }
99
100 /**
101 * Whether a source id is registered.
102 *
103 * @param string $source Source id.
104 * @return bool
105 */
106 public static function source_exists( $source ) {
107 $exts = self::sources();
108 return $source && isset( $exts[ $source ] );
109 }
110
111 /**
112 * The notification type a source belongs to (e.g. press_bar → notification_bar).
113 *
114 * @param string $source Source id.
115 * @return string
116 */
117 public static function type_for_source( $source ) {
118 $exts = self::sources();
119 return isset( $exts[ $source ]->types ) ? (string) $exts[ $source ]->types : '';
120 }
121
122 /**
123 * Whether a source's module is enabled (sources with no module are always on).
124 *
125 * @param string $source Source id.
126 * @return bool
127 */
128 public static function source_enabled( $source ) {
129 $exts = self::sources();
130 if ( ! isset( $exts[ $source ] ) ) {
131 return false;
132 }
133 $module = isset( $exts[ $source ]->module ) ? $exts[ $source ]->module : '';
134 return $module ? (bool) Modules::get_instance()->is_enabled( $module ) : true;
135 }
136
137 /**
138 * Whether a source is Pro-only.
139 *
140 * @param string $source Source id.
141 * @return bool
142 */
143 public static function source_is_pro( $source ) {
144 $exts = self::sources();
145 return isset( $exts[ $source ] ) && ! empty( $exts[ $source ]->is_pro );
146 }
147
148 /**
149 * Valid theme ids for a source (desktop themes).
150 *
151 * @param string $source Source id.
152 * @return string[]
153 */
154 public static function theme_ids_for_source( $source ) {
155 return self::filter_ids( self::all_themes(), $source );
156 }
157
158 /**
159 * Valid responsive theme ids for a source.
160 *
161 * @param string $source Source id.
162 * @return string[]
163 */
164 public static function res_theme_ids_for_source( $source ) {
165 return self::filter_ids( self::all_res_themes(), $source );
166 }
167
168 /**
169 * Whether a theme id is valid for a source.
170 *
171 * @param string $source Source id.
172 * @param string $theme Theme id.
173 * @return bool
174 */
175 public static function is_valid_theme( $source, $theme ) {
176 return $theme && in_array( $theme, self::theme_ids_for_source( $source ), true );
177 }
178
179 /**
180 * A theme id that is guaranteed to exist for the source: the type's declared
181 * default if valid, otherwise the first available theme.
182 *
183 * @param string $source Source id.
184 * @return string
185 */
186 public static function effective_default_theme( $source ) {
187 $ids = self::theme_ids_for_source( $source );
188 $exts = self::sources();
189 $type_id = self::type_for_source( $source );
190 $types = TypeFactory::get_instance()->get_all();
191 $declared = '';
192 if ( isset( $exts[ $source ]->default_theme ) && $exts[ $source ]->default_theme ) {
193 $declared = $exts[ $source ]->default_theme;
194 } elseif ( $type_id && isset( $types[ $type_id ]->default_theme ) ) {
195 $declared = $types[ $type_id ]->default_theme;
196 }
197 if ( $declared && in_array( $declared, $ids, true ) ) {
198 return $declared;
199 }
200 return isset( $ids[0] ) ? $ids[0] : '';
201 }
202
203 /**
204 * The default notification-template (content-slot → data-tag map) the admin
205 * builder applies when a theme is selected.
206 *
207 * Data-driven types (comments, download stats, reviews, sales, forms) render
208 * their text entirely through this template; static-content types (bar,
209 * cookie notice, announcement, exit intent) carry their own literal content
210 * and have no template here. The builder writes it from the selected theme's
211 * `template` array via the nx_themes_trigger system (@notification-template.
212 * <param>:<tag> entries). A headless create/update (MCP, REST, WP-CLI) never
213 * fires those UI triggers, so without backfilling this the record saves and
214 * lists fine but renders blank for data-driven types. Reconstructing it from
215 * the exact same trigger data guarantees it can never diverge from the admin.
216 *
217 * @param string $theme Theme id (stored value, e.g. download_stats_today-download).
218 * @return array Param => tag/value map (first_param, custom_first_param, ...); empty when the theme has no template.
219 */
220 public static function default_template_for_theme( $theme ) {
221 $list = self::theme_triggers( $theme );
222
223 $template = array();
224 $prefix = '@notification-template.';
225 $len = strlen( $prefix );
226 foreach ( $list as $entry ) {
227 if ( ! is_string( $entry ) || 0 !== strpos( $entry, $prefix ) ) {
228 continue;
229 }
230 // Entry shape: "@notification-template.<param>:<value>". Split on the
231 // FIRST colon only — a value may itself contain colons (e.g. custom
232 // text like "in last {{day:7}}").
233 $rest = substr( $entry, $len );
234 $pos = strpos( $rest, ':' );
235 if ( false === $pos ) {
236 continue;
237 }
238 $param = substr( $rest, 0, $pos );
239 $value = substr( $rest, $pos + 1 );
240 if ( '' !== $param ) {
241 $template[ $param ] = $value;
242 }
243 }
244 return $template;
245 }
246
247 /**
248 * The default inline display locations (hook names) the admin builder
249 * applies when an inline theme is selected.
250 *
251 * Inline themes declare `inline_location` in their theme config; the
252 * builder copies it into the notification through the nx_themes_trigger
253 * system. A headless create never fires that trigger, so without this the
254 * record stores no location and the notification renders nowhere.
255 *
256 * @param string $theme Theme id (stored value, e.g. woo_inline_conv-theme-seven).
257 * @return array Hook names; empty when the theme declares none.
258 */
259 public static function default_inline_location_for_theme( $theme ) {
260 $list = self::theme_triggers( $theme );
261 if ( empty( $list['inline_location'] ) ) {
262 return array();
263 }
264 return array_values( array_filter( (array) $list['inline_location'], 'is_string' ) );
265 }
266
267 /**
268 * The nx_themes_trigger entries for one theme, booting the trigger map once.
269 *
270 * @param string $theme Theme id.
271 * @return array
272 */
273 protected static function theme_triggers( $theme ) {
274 if ( ! $theme || ! is_string( $theme ) ) {
275 return array();
276 }
277 if ( ! self::$triggers_booted ) {
278 self::boot(); // Ensures nx_before_metabox_load fired so extensions registered nx_themes_trigger.
279 self::$triggers_booted = true;
280 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- consuming core NotificationX (nx_) hooks.
281 $triggers = apply_filters( 'nx_themes_trigger', array() );
282 // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
283 self::$triggers = is_array( $triggers ) ? $triggers : array();
284 }
285 return isset( self::$triggers[ $theme ] ) && is_array( self::$triggers[ $theme ] ) ? self::$triggers[ $theme ] : array();
286 }
287
288 /**
289 * Filter a raw theme map down to the ids belonging to a source. Prefers each
290 * entry's own source rule (["includes","source",[...]]); falls back to the
291 * "<source>_" id prefix.
292 *
293 * @param array $themes Raw theme map.
294 * @param string $source Source id.
295 * @return string[]
296 */
297 protected static function filter_ids( $themes, $source ) {
298 $out = array();
299 foreach ( $themes as $id => $theme ) {
300 $value = is_array( $theme ) && isset( $theme['value'] ) ? (string) $theme['value'] : (string) $id;
301 $match = false;
302
303 // The entry's `rules` can be a NotificationX\Core\Rule object, a plain
304 // array, or a nested mix (e.g. GDPR uses ["and", <Rule>, <Rule>] where
305 // the source rule is nested). JSON round-trip the WHOLE tree so every
306 // nested Rule object becomes an array and the recursive source lookup
307 // below can reach it.
308 $rules = null;
309 if ( is_array( $theme ) && isset( $theme['rules'] ) ) {
310 $decoded = json_decode( wp_json_encode( $theme['rules'] ), true );
311 $rules = is_array( $decoded ) ? $decoded : null;
312 }
313
314 if ( is_array( $rules ) && ! empty( $rules ) ) {
315 $sources = self::rule_sources( $rules );
316 if ( ! empty( $sources ) ) {
317 $match = in_array( $source, $sources, true );
318 } else {
319 $match = 0 === strpos( $value, $source . '_' );
320 }
321 } else {
322 $match = 0 === strpos( $value, $source . '_' );
323 }
324
325 if ( $match ) {
326 $out[] = $value;
327 }
328 }
329 return $out;
330 }
331
332 /**
333 * Extract source ids from a (possibly nested) rules array shaped like
334 * ["includes","source",[...]].
335 *
336 * @param array $rules Rules array.
337 * @return string[]
338 */
339 protected static function rule_sources( $rules ) {
340 $found = array();
341 if ( isset( $rules[0], $rules[1], $rules[2] ) && 'includes' === $rules[0] && 'source' === $rules[1] && is_array( $rules[2] ) ) {
342 foreach ( $rules[2] as $s ) {
343 $found[] = (string) $s;
344 }
345 return $found;
346 }
347 foreach ( $rules as $r ) {
348 if ( is_array( $r ) ) {
349 $found = array_merge( $found, self::rule_sources( $r ) );
350 }
351 }
352 return $found;
353 }
354 }
355