PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.7
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.7
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Addons / ProAddonMigration.php

ProAddonMigration.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.7, at includes/Addons/ProAddonMigration.php

247 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Migration: convert monolithic Pro features into opt-in addons.
4 *
5 * For each wrapped addon we run a feature-specific "is the merchant
6 * actually using this?" check. If yes → flip the addon flag ON so the
7 * feature keeps working post-migration. If no → leave OFF; the admin
8 * enables it explicitly from the Addons grid before the feature loads.
9 *
10 * Detection rules (all signals VERIFIED by reading the actual Pro source
11 * — no guessing):
12 *
13 * recurring_invoices — meta `_easy_invoice_recurring_enabled` = '1' on any
14 * post. (RecurringInvoices.php declares the constant.)
15 * partial_payments — option `easy_invoice_pro_partial_payments_enable`
16 * stores '1' when the admin toggled the feature on.
17 * (PartialPayments.php line 66.)
18 * client_portal — option `easy_invoice_pro_client_portal_settings`
19 * is written the first time the portal admin page
20 * is saved. (ClientPortal.php line 105.)
21 * item_library — option `easy_invoice_pro_saved_items` exists and
22 * contains at least one item. (ItemLibraryService.php
23 * line 22 — ITEMS_OPTION_KEY constant.)
24 * custom_templates — option `easy_invoice_pro_templates` exists with
25 * at least one saved template. (TemplateBuilderService
26 * line 22 — TEMPLATES_OPTION_KEY constant.)
27 * email_enhancements — option `easy_invoice_pro_email_settings` exists
28 * and is non-empty. (EmailEnhancements.php line 34.)
29 *
30 * pdf_toolkit — no verified detectable signal. Stays disabled;
31 * admin enables manually if they want it.
32 * additional_tax — no verified detectable signal. Stays disabled.
33 * bulk_operations — pure UI buttons, no persistent state. Disabled.
34 * privacy_tools — pure WP-hook wiring, no persistent state. Disabled.
35 *
36 * Migration is single-shot via MIGRATION_VERSION-stamped option. Bump the
37 * version to re-run after adding new detection rules.
38 *
39 * @package EasyInvoice
40 */
41
42 namespace EasyInvoice\Addons;
43
44 if (!defined('ABSPATH')) {
45 exit;
46 }
47
48 class ProAddonMigration {
49
50 public const MIGRATION_FLAG_OPTION = 'easy_invoice_pro_addon_migration_v1';
51 // Bump this whenever a new addon is added that needs auto-enable on
52 // existing Pro installs. The migration re-runs once per VERSION bump
53 // and only touches addons whose flag hasn't been explicitly set by
54 // the admin.
55 //
56 // v1 — initial migration (10 wrapped Pro features)
57 // v2 — adds 'reports' addon (was implicit Pro-only feature)
58 // v3 — adds 'secure_links' addon; auto-enables for installs whose
59 // legacy `easy_invoice_pro_enable_secure_links` option was
60 // 'yes' so signed URLs already mailed out stay resolvable.
61 public const MIGRATION_VERSION = '3';
62
63 /**
64 * Register the migration to fire on admin_init. Frontend visitors
65 * never trigger it.
66 */
67 public static function register(): void {
68 add_action('admin_init', [__CLASS__, 'maybeMigrate'], 5);
69 }
70
71 /**
72 * Idempotent entry point — runs once per MIGRATION_VERSION bump.
73 */
74 public static function maybeMigrate(): void {
75 if (!function_exists('easy_invoice_has_pro') || !easy_invoice_has_pro()) {
76 return;
77 }
78 if (get_option(self::MIGRATION_FLAG_OPTION) === self::MIGRATION_VERSION) {
79 return;
80 }
81 self::run();
82 // Store version with autoload=no so this guard row doesn't
83 // bloat the wp_options autoload set.
84 update_option(self::MIGRATION_FLAG_OPTION, self::MIGRATION_VERSION, false);
85 }
86
87 /**
88 * The actual migration. For every wrapped addon, set the enable flag
89 * based on whether we detect the feature in use. Never overwrites a
90 * flag the admin has already set explicitly.
91 */
92 public static function run(): void {
93 $enabled = [];
94 foreach (AddonRegistry::migratedProAddonIds() as $addon_id) {
95 $flag_key = AddonManager::optionKey($addon_id);
96 if (get_option($flag_key, null) !== null) {
97 continue; // admin already chose
98 }
99 $in_use = self::isFeatureInUse($addon_id);
100 update_option($flag_key, $in_use ? 1 : 0, false);
101 if ($in_use) {
102 $enabled[] = $addon_id;
103 }
104 }
105
106 /**
107 * Fires after the Pro→addon migration completes.
108 *
109 * @param array<int,string> $enabled IDs the migration enabled
110 */
111 do_action('easy_invoice_pro_addon_migration_completed', $enabled);
112 }
113
114 /**
115 * Dispatch to per-addon detector.
116 *
117 * Two categories of addon:
118 *
119 * FEATURE-SPECIFIC DETECTION — the feature had its own user-facing
120 * enable/disable setting OR produced its own data signal. We can
121 * tell precisely whether this merchant was using it.
122 * • recurring_invoices — meta on actual recurring invoices
123 * • partial_payments — own enable option
124 * • client_portal — settings option present
125 * • custom_templates — saved templates option non-empty
126 *
127 * PRO-INSTALL DETECTION — the feature had NO user toggle in old Pro
128 * (it always loaded, every site got it). To preserve "old users
129 * keep working", we enable for ANY install that we can prove
130 * previously ran Pro. Fresh installs leave them off.
131 * • pdf_toolkit
132 * • bulk_operations
133 * • item_library
134 * • additional_tax
135 * • email_enhancements
136 * • privacy_tools
137 */
138 public static function isFeatureInUse(string $addon_id): bool {
139 switch ($addon_id) {
140 // Feature-specific signals.
141 case 'recurring_invoices': return self::detectRecurring();
142 case 'partial_payments': return self::detectPartialPayments();
143 case 'client_portal': return self::detectClientPortal();
144 case 'custom_templates': return self::detectCustomTemplates();
145 case 'secure_links': return self::detectSecureLinks();
146 // Always-on Pro features — enable for any existing Pro install.
147 case 'pdf_toolkit':
148 case 'bulk_operations':
149 case 'item_library':
150 case 'additional_tax':
151 case 'email_enhancements':
152 case 'privacy_tools':
153 // Reports was an always-on Pro feature gated only by
154 // easy_invoice_has_pro(); existing Pro sites were using it.
155 case 'reports':
156 return self::isExistingProInstall();
157 default:
158 return false;
159 }
160 }
161
162 /**
163 * "Did this site ever run Pro before the addon-ization migration?"
164 *
165 * Verified signals (every entry is an option I have read the Pro
166 * source for and confirmed gets written during normal Pro lifecycle):
167 *
168 * easy_invoice_pro_license_details — written by License::activate()
169 * in includes/Updater/License.php line 97 the first time the
170 * admin saves a license key.
171 * easy_invoice_pro_license_key — written by the same call,
172 * line 101.
173 *
174 * Both are absent on a fresh install that has never activated Pro.
175 * We deliberately stick to license-flow signals because they're the
176 * least ambiguous "Pro was here" indicator — other options can be
177 * written by tools that don't imply real Pro use.
178 */
179 public static function isExistingProInstall(): bool {
180 if (get_option('easy_invoice_pro_license_details', null) !== null) return true;
181 if (get_option('easy_invoice_pro_license_key', null) !== null) return true;
182
183 /**
184 * Override hook for staging clones / unusual deploys where the
185 * heuristic mis-classifies. Return true to force "existing",
186 * false to force "fresh", null (default) to use the heuristic.
187 *
188 * @param bool|null $resolved
189 */
190 $override = apply_filters('easy_invoice_pro_addon_migration_is_existing', null);
191 return is_bool($override) ? $override : false;
192 }
193
194 // ── Verified per-feature detectors ──────────────────────────────────
195
196 private static function detectRecurring(): bool {
197 global $wpdb;
198 $found = $wpdb->get_var($wpdb->prepare(
199 "SELECT 1 FROM {$wpdb->postmeta}
200 WHERE meta_key = %s AND meta_value = %s LIMIT 1",
201 '_easy_invoice_recurring_enabled',
202 '1'
203 ));
204 return !empty($found);
205 }
206
207 private static function detectPartialPayments(): bool {
208 // The PartialPayments extension stores '1' / '0' as string. We
209 // count any truthy value (including the literal '1' or a future
210 // 'true') as "in use".
211 $value = get_option('easy_invoice_pro_partial_payments_enable', null);
212 if ($value === null) return false;
213 return !empty($value) && $value !== '0';
214 }
215
216 private static function detectClientPortal(): bool {
217 return get_option('easy_invoice_pro_client_portal_settings', null) !== null;
218 }
219
220 private static function detectCustomTemplates(): bool {
221 $templates = get_option('easy_invoice_pro_templates', null);
222 return is_array($templates) && !empty($templates);
223 }
224
225 /**
226 * Secure Links: the legacy Pro setting was a Yes/No checkbox in the
227 * Pro Settings page (see templates/admin/settings.php line 237 —
228 * checkbox writing easy_invoice_pro_enable_secure_links). Treat any
229 * truthy value as "in use" so we don't 404 secure URLs already
230 * mailed out. Sites that never enabled it stay disabled.
231 */
232 private static function detectSecureLinks(): bool {
233 $value = get_option('easy_invoice_pro_enable_secure_links', null);
234 if ($value === null) return false;
235 return $value === 'yes' || $value === '1' || $value === 1 || $value === true;
236 }
237
238 // detectItemLibrary() and detectEmailEnhancements() used to live here
239 // but were never called from isFeatureInUse(). Those addons are
240 // treated as "always-on Pro" features and fall through to
241 // isExistingProInstall() — the safer migration default (any site
242 // that ran Pro gets the feature, even if they hadn't actively used
243 // it yet). The per-feature detectors were removed to eliminate the
244 // dead-code path; reintroduce them only if a future product
245 // decision wants the stricter "must have used it" rule.
246 }
247