PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / trunk
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar vtrunk
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 1.2.0 1.2.1 All 156 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 trunk, at includes/Abilities/BuilderInfo.php

326 lines 11.1 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 if ( ! $theme ) {
222 return array();
223 }
224 if ( ! self::$triggers_booted ) {
225 self::boot(); // Ensures nx_before_metabox_load fired so extensions registered nx_themes_trigger.
226 self::$triggers_booted = true;
227 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- consuming core NotificationX (nx_) hooks.
228 $triggers = apply_filters( 'nx_themes_trigger', array() );
229 // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
230 self::$triggers = is_array( $triggers ) ? $triggers : array();
231 }
232
233 $list = isset( self::$triggers[ $theme ] ) && is_array( self::$triggers[ $theme ] ) ? self::$triggers[ $theme ] : array();
234
235 $template = array();
236 $prefix = '@notification-template.';
237 $len = strlen( $prefix );
238 foreach ( $list as $entry ) {
239 if ( ! is_string( $entry ) || 0 !== strpos( $entry, $prefix ) ) {
240 continue;
241 }
242 // Entry shape: "@notification-template.<param>:<value>". Split on the
243 // FIRST colon only — a value may itself contain colons (e.g. custom
244 // text like "in last {{day:7}}").
245 $rest = substr( $entry, $len );
246 $pos = strpos( $rest, ':' );
247 if ( false === $pos ) {
248 continue;
249 }
250 $param = substr( $rest, 0, $pos );
251 $value = substr( $rest, $pos + 1 );
252 if ( '' !== $param ) {
253 $template[ $param ] = $value;
254 }
255 }
256 return $template;
257 }
258
259 /**
260 * Filter a raw theme map down to the ids belonging to a source. Prefers each
261 * entry's own source rule (["includes","source",[...]]); falls back to the
262 * "<source>_" id prefix.
263 *
264 * @param array $themes Raw theme map.
265 * @param string $source Source id.
266 * @return string[]
267 */
268 protected static function filter_ids( $themes, $source ) {
269 $out = array();
270 foreach ( $themes as $id => $theme ) {
271 $value = is_array( $theme ) && isset( $theme['value'] ) ? (string) $theme['value'] : (string) $id;
272 $match = false;
273
274 // The entry's `rules` can be a NotificationX\Core\Rule object, a plain
275 // array, or a nested mix (e.g. GDPR uses ["and", <Rule>, <Rule>] where
276 // the source rule is nested). JSON round-trip the WHOLE tree so every
277 // nested Rule object becomes an array and the recursive source lookup
278 // below can reach it.
279 $rules = null;
280 if ( is_array( $theme ) && isset( $theme['rules'] ) ) {
281 $decoded = json_decode( wp_json_encode( $theme['rules'] ), true );
282 $rules = is_array( $decoded ) ? $decoded : null;
283 }
284
285 if ( is_array( $rules ) && ! empty( $rules ) ) {
286 $sources = self::rule_sources( $rules );
287 if ( ! empty( $sources ) ) {
288 $match = in_array( $source, $sources, true );
289 } else {
290 $match = 0 === strpos( $value, $source . '_' );
291 }
292 } else {
293 $match = 0 === strpos( $value, $source . '_' );
294 }
295
296 if ( $match ) {
297 $out[] = $value;
298 }
299 }
300 return $out;
301 }
302
303 /**
304 * Extract source ids from a (possibly nested) rules array shaped like
305 * ["includes","source",[...]].
306 *
307 * @param array $rules Rules array.
308 * @return string[]
309 */
310 protected static function rule_sources( $rules ) {
311 $found = array();
312 if ( isset( $rules[0], $rules[1], $rules[2] ) && 'includes' === $rules[0] && 'source' === $rules[1] && is_array( $rules[2] ) ) {
313 foreach ( $rules[2] as $s ) {
314 $found[] = (string) $s;
315 }
316 return $found;
317 }
318 foreach ( $rules as $r ) {
319 if ( is_array( $r ) ) {
320 $found = array_merge( $found, self::rule_sources( $r ) );
321 }
322 }
323 return $found;
324 }
325 }
326