PluginProbe
YayMail – WooCommerce Email Customizer / 4.4.1
YayMail – WooCommerce Email Customizer v4.4.1
4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 trunk 1.9.6 2.1.4 2.1.5 3.2.2 3.2.6 3.2.7.1 3.2.8.1 3.2.9 3.3 3.3.1 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9 3.4 3.4.1 All 57 releases
yaymail / vendor-prefixed / src / AdminShell.php

AdminShell.php in YayMail – WooCommerce Email Customizer 4.4.1, at vendor-prefixed/src/AdminShell.php

287 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 namespace YayMailScoped\YayCommerce\AdminShell;
4
5 use YayMailScoped\YayCommerce\AdminShell\Contracts\AddonHostAdapter;
6 use YayMailScoped\YayCommerce\AdminShell\Contracts\PluginMenuAdapter;
7 use YayMailScoped\YayCommerce\AdminShell\License\Contracts\LicenseConfigAdapter;
8 use YayMailScoped\YayCommerce\AdminShell\License\LicenseHandler;
9 use YayMailScoped\YayCommerce\AdminShell\License\PluginInfoFactory;
10 use YayMailScoped\YayCommerce\AdminShell\Menu\ExternalPluginMenuAdapter;
11 use YayMailScoped\YayCommerce\AdminShell\Menu\PluginSubmenu;
12 use YayMailScoped\YayCommerce\AdminShell\Menu\MenuSuppressor;
13 use YayMailScoped\YayCommerce\AdminShell\Menu\PagesRouter;
14 use YayMailScoped\YayCommerce\AdminShell\Menu\TopLevelMenu;
15 use YayMailScoped\YayCommerce\AdminShell\Pages\RecommendedPluginsPage;
16 use YayMailScoped\YayCommerce\AdminShell\Registry\AddonBridge;
17 use YayMailScoped\YayCommerce\AdminShell\Registry\LegacyBridge;
18 use YayMailScoped\YayCommerce\AdminShell\Registry\LicenseRegistry;
19 use YayMailScoped\YayCommerce\AdminShell\Support\Constants;
20 \defined('ABSPATH') || exit;
21 /**
22 * Public facade — entry point for consuming plugins.
23 *
24 * Version election: when multiple scoped copies coexist, the highest
25 * VERSION wins and registers menus/pages. All copies' register_plugin()
26 * and enable_license() still run — they hook global WP actions.
27 */
28 class AdminShell
29 {
30 /** Package version — used for cross-scope version election. */
31 const VERSION = '2.6.1';
32 private static ?self $instance = null;
33 private static bool $booted = \false;
34 private static array $enabled_slugs = [];
35 /** The scoped prefix for THIS copy (derived from namespace). */
36 private static string $prefix = '';
37 private LicenseRegistry $registry;
38 private function __construct()
39 {
40 $this->registry = new LicenseRegistry();
41 }
42 /**
43 * Bootstrap the shared admin shell.
44 *
45 * Each scoped copy calls boot(). All register their version in a shared
46 * global. Actual menu/page registration is deferred to admin_menu where
47 * only the highest version runs.
48 */
49 public static function boot() : void
50 {
51 if (self::$booted) {
52 return;
53 }
54 self::$booted = \true;
55 require_once __DIR__ . '/Support/Constants.php';
56 $instance = self::get_instance();
57 // Derive our scoped prefix from the namespace (e.g. "YayMailScoped\YayCommerce\AdminShell" → "YayMailScoped")
58 $ns = __NAMESPACE__;
59 // YayCommerce\AdminShell or ScopedPrefix\YayCommerce\AdminShell
60 $parts = \explode('\\', $ns);
61 self::$prefix = \count($parts) > 2 ? $parts[0] : 'default';
62 // Register this version in the shared global for cross-scope election
63 if (!isset($GLOBALS['yaycommerce_admin_shell_versions'])) {
64 $GLOBALS['yaycommerce_admin_shell_versions'] = [];
65 }
66 $GLOBALS['yaycommerce_admin_shell_versions'][self::$prefix] = ['version' => self::VERSION, 'registry' => $instance->registry, 'boot_cb' => [static::class, 'do_shell_registration']];
67 // Register the version election — only once (first copy to call boot sets it up)
68 if (1 === \count($GLOBALS['yaycommerce_admin_shell_versions'])) {
69 \add_action('admin_menu', [static::class, 'elect_version'], 8);
70 }
71 // Legacy bridge — reads yaycommerce_licensing_plugins filter.
72 // Runs for ALL versions (uses global WP hooks, contributes to any winning registry).
73 $legacy_bridge = new LegacyBridge($instance->registry);
74 $legacy_bridge->init();
75 // On AJAX requests admin_menu doesn't fire, so register AJAX handlers directly.
76 // Global guard ensures only one scoped copy registers (action name is identical across copies).
77 // DOING_AJAX must be in scoper.inc.php exclude-constants to avoid namespace prefixing.
78 if (\defined('DOING_AJAX') && \DOING_AJAX) {
79 $registered_ver = $GLOBALS['yaycommerce_ajax_handlers_registered'] ?? '0.0.0';
80 if (\version_compare(self::VERSION, $registered_ver, '>')) {
81 $GLOBALS['yaycommerce_ajax_handlers_registered'] = self::VERSION;
82 RecommendedPluginsPage::get_instance();
83 }
84 }
85 \do_action('yaycommerce_admin_shell_booted', $instance);
86 }
87 /**
88 * Version election — picks the highest version and runs its shell registration.
89 * Called once at admin_menu priority 8 (before TopLevelMenu at 9).
90 */
91 public static function elect_version() : void
92 {
93 $versions = $GLOBALS['yaycommerce_admin_shell_versions'] ?? [];
94 if (empty($versions)) {
95 return;
96 }
97 // Find highest version
98 $winner_prefix = '';
99 $winner_ver = '0.0.0';
100 foreach ($versions as $prefix => $data) {
101 if (\version_compare($data['version'], $winner_ver, '>')) {
102 $winner_ver = $data['version'];
103 $winner_prefix = $prefix;
104 }
105 }
106 // Call the winner's registration — may be a different scoped class
107 $winner = $versions[$winner_prefix];
108 if (isset($winner['boot_cb']) && \is_callable($winner['boot_cb'])) {
109 \call_user_func($winner['boot_cb']);
110 }
111 }
112 /**
113 * Register menus/pages — only called by the winning version.
114 */
115 public static function do_shell_registration() : void
116 {
117 $instance = self::get_instance();
118 // Merge all registries into the winner's registry
119 foreach ($GLOBALS['yaycommerce_admin_shell_versions'] ?? [] as $prefix => $data) {
120 if ($prefix !== self::$prefix && isset($data['registry'])) {
121 $other_registry = $data['registry'];
122 foreach ($other_registry->all() as $info) {
123 if (!$instance->registry->get($info->slug)) {
124 $instance->registry->register($info);
125 }
126 }
127 }
128 }
129 $suppressor = new MenuSuppressor();
130 $suppressor->init();
131 $top_menu = new TopLevelMenu();
132 $top_menu->init();
133 $router = new PagesRouter($instance->registry);
134 $router->init();
135 }
136 /**
137 * Register a plugin with the admin shell.
138 * Auto-detects pro vs lite via instanceof.
139 * Runs for ALL versions (not version-gated).
140 */
141 public static function register_plugin(PluginMenuAdapter $adapter) : void
142 {
143 self::validate_adapter($adapter);
144 // Submenu registration — per-plugin, all versions
145 $submenu = new PluginSubmenu($adapter);
146 $submenu->init();
147 // Action links + row meta — per-plugin, all versions
148 if (\is_admin()) {
149 self::register_plugin_links($adapter);
150 }
151 // License subsystem — per-plugin, all versions
152 if ($adapter instanceof LicenseConfigAdapter) {
153 self::enable_license($adapter);
154 }
155 // Addon host — bridge plugin-specific addon filter into registry.
156 if ($adapter instanceof AddonHostAdapter) {
157 $filter_name = $adapter->get_addon_licensing_filter();
158 if (!empty($filter_name)) {
159 $bridge = new AddonBridge($filter_name, self::get_instance()->registry);
160 $bridge->init();
161 }
162 }
163 }
164 /**
165 * Register an "Other Plugins" submenu under an external plugin's own top-level menu.
166 * Use for plugins with their own menu hierarchy (e.g. whatsapp, filester) that should
167 * not appear under the shared YayCommerce menu.
168 *
169 * @param array{parent_menu: string, menu_title: string, menu_capability: string, menu_slug: string} $config
170 */
171 public static function register_external_plugin_menu(array $config) : void
172 {
173 $adapter = new ExternalPluginMenuAdapter($config);
174 $adapter->init();
175 }
176 /**
177 * Validate adapter values upfront.
178 */
179 private static function validate_adapter(PluginMenuAdapter $adapter) : void
180 {
181 $menu_title = $adapter->get_menu_title();
182 if (empty($menu_title)) {
183 \trigger_error('[YayCommerce AdminShell] get_menu_title() must return a non-empty string.', \E_USER_WARNING);
184 }
185 $basename = $adapter->get_plugin_basename();
186 if (empty($basename)) {
187 \trigger_error('[YayCommerce AdminShell] get_plugin_basename() must return a non-empty string.', \E_USER_WARNING);
188 }
189 $callback = $adapter->get_settings_page_callback();
190 if (null !== $callback && !\is_callable($callback)) {
191 \trigger_error('[YayCommerce AdminShell] get_settings_page_callback() returned a non-callable value.', \E_USER_WARNING);
192 }
193 if ($adapter instanceof LicenseConfigAdapter) {
194 $slug = $adapter->get_plugin_slug();
195 if (empty($slug)) {
196 \trigger_error('[YayCommerce AdminShell] get_plugin_slug() must return a non-empty string.', \E_USER_WARNING);
197 }
198 $item_id = $adapter->get_item_id();
199 if ($item_id <= 0) {
200 \trigger_error('[YayCommerce AdminShell] get_item_id() must return a positive integer.', \E_USER_WARNING);
201 }
202 $store_url = $adapter->get_store_url();
203 if (empty($store_url)) {
204 \trigger_error('[YayCommerce AdminShell] get_store_url() must return a non-empty URL.', \E_USER_WARNING);
205 }
206 $plugin_file = $adapter->get_plugin_file();
207 if (empty($plugin_file)) {
208 \trigger_error('[YayCommerce AdminShell] get_plugin_file() must return a non-empty path.', \E_USER_WARNING);
209 }
210 }
211 }
212 /**
213 * Enable the license subsystem for a plugin.
214 * Runs for ALL versions (not version-gated).
215 */
216 public static function enable_license(LicenseConfigAdapter $adapter) : void
217 {
218 $slug = $adapter->get_plugin_slug();
219 if (isset(self::$enabled_slugs[$slug])) {
220 return;
221 }
222 self::$enabled_slugs[$slug] = \true;
223 $instance = self::get_instance();
224 new LicenseHandler($adapter);
225 $info = PluginInfoFactory::from_adapter($adapter);
226 $instance->registry->register($info);
227 \do_action('yaycommerce_admin_shell_license_enabled', $adapter);
228 }
229 /**
230 * Register plugin action links + row meta.
231 */
232 private static function register_plugin_links(PluginMenuAdapter $adapter) : void
233 {
234 $basename = $adapter->get_plugin_basename();
235 \add_filter('plugin_action_links_' . $basename, function (array $links) use($adapter) {
236 $new = [];
237 $menu_slug = $adapter->get_menu_slug();
238 if (!empty($menu_slug)) {
239 $url = \admin_url('admin.php?page=' . $menu_slug);
240 $new['settings'] = '<a href="' . \esc_url($url) . '">' . \esc_html($adapter->get_settings_label()) . '</a>';
241 }
242 $pro_url = $adapter->get_pro_url();
243 if (!empty($pro_url)) {
244 $new['go-pro'] = '<a href="' . \esc_url($pro_url) . '" target="_blank" style="color:#00a32a;font-weight:700;">' . \esc_html__('Go Pro', 'yaycommerce') . '</a>';
245 }
246 return \array_merge($new, $links);
247 });
248 \add_filter('plugin_row_meta', function (array $meta, string $file) use($adapter, $basename) {
249 if ($file !== $basename) {
250 return $meta;
251 }
252 $docs_url = $adapter->get_docs_url();
253 if (!empty($docs_url)) {
254 $meta[] = '<a href="' . \esc_url($docs_url) . '" target="_blank">' . \esc_html__('Docs', 'yaycommerce') . '</a>';
255 }
256 $meta[] = '<a href="https://yaycommerce.com/support" target="_blank">' . \esc_html__('Support', 'yaycommerce') . '</a>';
257 return $meta;
258 }, 10, 2);
259 }
260 /**
261 * Return the shared registry.
262 */
263 public static function registry() : LicenseRegistry
264 {
265 return self::get_instance()->registry;
266 }
267 private static function get_instance() : self
268 {
269 if (null === self::$instance) {
270 self::$instance = new self();
271 }
272 return self::$instance;
273 }
274 /**
275 * Reset state — for unit tests only.
276 */
277 public static function reset() : void
278 {
279 self::$instance = null;
280 self::$booted = \false;
281 self::$enabled_slugs = [];
282 self::$prefix = '';
283 unset($GLOBALS['yaycommerce_admin_shell_versions']);
284 unset($GLOBALS['yaycommerce_ajax_handlers_registered']);
285 }
286 }
287