PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-whitelabel-preservation.php

class-metasync-whitelabel-preservation.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at includes/class-metasync-whitelabel-preservation.php

285 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * Centralised white-label preservation helper.
8 *
9 * Both the Advanced "Clear All Settings" reset (Metasync_Debug_Manager) and the
10 * separate "Reset Authentication" flow (Metasync_Connect_Manager) must clear
11 * operational plugin data — API keys, authentication tokens, functional
12 * settings, caches, logs, crawl data — without destroying the reseller
13 * white-label identity stored in the main plugin option.
14 *
15 * The same extraction/restoration surface is also used by the ZIP export
16 * (Metasync_Admin_Ajax::handle_export_whitelabel_settings) so that reset and
17 * export can never drift apart: whatever the reset preserves is exactly what
18 * the export serialises, and vice versa.
19 *
20 * @package Metasync
21 * @subpackage Metasync/includes
22 */
23 class Metasync_Whitelabel_Preservation
24 {
25 /**
26 * The general-option keys that carry white-label branding. These are the
27 * only keys inside $options['general'] that are part of the white-label
28 * identity and therefore the only ones the reset must spare.
29 *
30 * Kept in sync with the export handler and the plugin-file header map.
31 */
32 private static $general_whitelabel_keys = array(
33 'white_label_plugin_name',
34 'white_label_plugin_description',
35 'white_label_plugin_author',
36 'white_label_plugin_author_uri',
37 'white_label_plugin_uri',
38 'white_label_plugin_menu_slug',
39 'white_label_plugin_menu_icon',
40 'whitelabel_otto_name',
41 );
42
43 /**
44 * Return the canonical list of general-option keys that carry white-label
45 * branding. Exposed so tests and the export handler share one source.
46 *
47 * @return string[]
48 */
49 public static function general_whitelabel_keys()
50 {
51 return self::$general_whitelabel_keys;
52 }
53
54 /**
55 * Capture the complete white-label identity from the live option store.
56 *
57 * Captures:
58 * - the full $options['whitelabel'] blob (logos, domains, custom OTTO
59 * name, visibility controls, access control, quick links, colour
60 * settings, password, recovery settings, …);
61 * - every general.* white-label branding key; and
62 * - the dedicated metasync_options_whitelabel_user option.
63 *
64 * The returned array is the single argument restore_whitelabel_state()
65 * consumes; it is safe to call before a destructive reset and to keep
66 * around for the duration of the request.
67 *
68 * @return array{whitelabel: array, general: array, whitelabel_user: mixed}
69 */
70 public static function extract_whitelabel_state()
71 {
72 $options = get_option(Metasync::option_name, array());
73 $options = is_array($options) ? $options : array();
74
75 $whitelabel = isset($options['whitelabel']) && is_array($options['whitelabel'])
76 ? $options['whitelabel']
77 : array();
78
79 $general = isset($options['general']) && is_array($options['general'])
80 ? $options['general']
81 : array();
82
83 $general_branding = array();
84 foreach (self::$general_whitelabel_keys as $key) {
85 if (array_key_exists($key, $general)) {
86 $general_branding[$key] = $general[$key];
87 }
88 }
89
90 $whitelabel_user = get_option(Metasync::option_name . '_whitelabel_user', null);
91
92 return array(
93 'whitelabel' => $whitelabel,
94 'general' => $general_branding,
95 'whitelabel_user' => $whitelabel_user,
96 );
97 }
98
99 /**
100 * Restore a previously captured white-label identity into the live option
101 * store, using the recovery-lock / authorised-write pattern that the JSON
102 * import and the password-recovery flow share.
103 *
104 * Writes back:
105 * - the whitelabel blob (merged over whatever the reset left behind);
106 * - the general branding keys; and
107 * - the dedicated whitelabel_user option.
108 *
109 * @param mixed $state A state array produced by extract_whitelabel_state().
110 * Anything else fails the restore.
111 * @return bool True on success, false if the option write did not stick.
112 */
113 public static function restore_whitelabel_state($state)
114 {
115 if (!is_array($state)) {
116 return false;
117 }
118
119 $options = get_option(Metasync::option_name, array());
120 if (!is_array($options)) {
121 $options = array();
122 }
123 if (!isset($options['general']) || !is_array($options['general'])) {
124 $options['general'] = array();
125 }
126
127 // Restore the whitelabel blob. Merge over whatever the reset left so a
128 // partial reset cannot lose branding that was never targeted.
129 $whitelabel = isset($state['whitelabel']) && is_array($state['whitelabel'])
130 ? $state['whitelabel']
131 : array();
132 if (!empty($whitelabel)) {
133 if (!isset($options['whitelabel']) || !is_array($options['whitelabel'])) {
134 $options['whitelabel'] = array();
135 }
136 $options['whitelabel'] = array_merge($options['whitelabel'], $whitelabel);
137 $options['whitelabel']['restored_at'] = time();
138
139 // The settings password may arrive as plaintext (from a ZIP export
140 // payload) or as the encrypted ciphertext captured before a reset.
141 // encrypt_secret() short-circuits on an already-encrypted value, so
142 // running it here is a no-op for the reset path and a re-encrypt for
143 // the import path — the password is always stored encrypted at rest.
144 if (!empty($options['whitelabel']['settings_password']) && is_string($options['whitelabel']['settings_password'])) {
145 $options['whitelabel']['settings_password'] = Metasync::encrypt_secret($options['whitelabel']['settings_password']);
146 }
147 }
148
149 // Restore the general branding keys.
150 $general_branding = isset($state['general']) && is_array($state['general'])
151 ? $state['general']
152 : array();
153 foreach ($general_branding as $key => $value) {
154 $options['general'][$key] = $value;
155 }
156
157 // The whitelabel blob may carry a settings password. It is re-encrypted
158 // above (a no-op when it was already the captured ciphertext), but the
159 // recovery-protection filter still treats any unauthorised password
160 // write as a conflict and swaps it back to the stored value. Mirror the
161 // JSON import's persist path: take the lock and authorise the write for
162 // the duration of the save.
163 $carries_password = !empty($options['whitelabel']['settings_password']);
164 $lock_owner = '';
165 if ($carries_password) {
166 if (!class_exists('Metasync_Admin_Ajax')) {
167 require_once __DIR__ . '/class-metasync-admin-ajax.php';
168 }
169 if (!class_exists('Metasync_Settings_Registration')) {
170 require_once __DIR__ . '/class-metasync-settings-registration.php';
171 }
172 if (!Metasync_Admin_Ajax::instance()->acquire_recovery_lock($lock_owner)) {
173 // A recovery or settings save is mid-write; we cannot safely
174 // restore the password-protected blob right now.
175 return false;
176 }
177 }
178
179 try {
180 if ($carries_password) {
181 Metasync_Settings_Registration::authorize_recovery_password_write(true);
182 }
183 update_option(Metasync::option_name, $options);
184 } finally {
185 if ($carries_password) {
186 Metasync_Settings_Registration::authorize_recovery_password_write(false);
187 Metasync_Admin_Ajax::instance()->release_recovery_lock($lock_owner);
188 }
189 }
190
191 // Verify the write stuck. The recovery filter can still swap a password
192 // back if the authorisation raced; compare the stored value.
193 $stored = get_option(Metasync::option_name, array());
194 if (!is_array($stored)) {
195 return false;
196 }
197
198 // The whitelabel blob and every general branding key we restored must
199 // be present in the stored option. A failed update_option leaves the
200 // pre-restore value behind, so this catches a silent write failure.
201 // The settings_password is intentionally re-encrypted during restore,
202 // so it is verified separately below against the written value rather
203 // than against the (possibly-plaintext) source value.
204 if (!empty($whitelabel)) {
205 if (!isset($stored['whitelabel']) || !is_array($stored['whitelabel'])) {
206 return false;
207 }
208 foreach ($whitelabel as $key => $value) {
209 if ($key === 'settings_password') {
210 continue;
211 }
212 if (!array_key_exists($key, $stored['whitelabel']) || $stored['whitelabel'][$key] !== $value) {
213 return false;
214 }
215 }
216 }
217 foreach ($general_branding as $key => $value) {
218 if (!isset($stored['general']) || !array_key_exists($key, $stored['general']) || $stored['general'][$key] !== $value) {
219 return false;
220 }
221 }
222
223 if ($carries_password) {
224 $stored_pw = $stored['whitelabel']['settings_password'] ?? '';
225 $expected_pw = $options['whitelabel']['settings_password'] ?? '';
226 if (!hash_equals((string) $expected_pw, (string) $stored_pw)) {
227 return false;
228 }
229 }
230
231 // Restore the dedicated whitelabel_user option.
232 $whitelabel_user = array_key_exists('whitelabel_user', $state)
233 ? $state['whitelabel_user']
234 : null;
235 if ($whitelabel_user !== null) {
236 update_option(Metasync::option_name . '_whitelabel_user', $whitelabel_user);
237 }
238
239 return true;
240 }
241
242 /**
243 * Build the white-label export payload shared by the ZIP export.
244 *
245 * The returned array has the exact shape the JSON importer consumes:
246 * - 'whitelabel_settings': the whitelabel blob, with the settings
247 * password exported as plaintext (so it can be re-encrypted on the
248 * destination site's salts);
249 * - 'general_settings': the general branding keys only.
250 *
251 * No API credentials (apikey / searchatlas_api_key / otto_pixel_uuid / …)
252 * are ever serialised — only the keys in $general_whitelabel_keys and the
253 * whitelabel blob are captured, so credentials cannot leak into the export.
254 *
255 * @return array{whitelabel_settings: array, general_settings: array}
256 */
257 public static function build_whitelabel_export_data()
258 {
259 $whitelabel_settings = Metasync::get_whitelabel_settings();
260
261 // Export the settings password as plaintext (not the encrypted blob):
262 // the encryption key derives from this site's salts, so the blob would
263 // be undecryptable after import on a different site. The importer
264 // re-encrypts it at rest with the destination site's salts.
265 if (!empty($whitelabel_settings['settings_password'])) {
266 $whitelabel_settings['settings_password'] = Metasync::get_whitelabel_password();
267 }
268
269 $general_settings = Metasync::get_option('general');
270 $general_settings = is_array($general_settings) ? $general_settings : array();
271
272 $whitelabel_related_general = array();
273 foreach (self::$general_whitelabel_keys as $key) {
274 if (array_key_exists($key, $general_settings)) {
275 $whitelabel_related_general[$key] = $general_settings[$key];
276 }
277 }
278
279 return array(
280 'whitelabel_settings' => $whitelabel_settings,
281 'general_settings' => $whitelabel_related_general,
282 );
283 }
284 }
285