PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
← All changes | class/class-mainwp-api-manager.php +638 -495 5.0.3 → 6.2.2 View file →
@@ -11,576 +11,719 @@
11 11 */
12 12
13 13 namespace MainWP\Dashboard;
14 14
15 +// Exit if accessed directly.
16 +if ( ! defined( 'ABSPATH' ) ) {
17 + exit;
18 +}
19 +
15 20 /**
16 21 * Class MainWP_Api_Manager
17 22 *
18 23 * @package MainWP\Dashboard
19 24 */
20 -class MainWP_Api_Manager {
25 +class MainWP_Api_Manager { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
21 26
22 - /**
23 - * Extensions upgrade URL.
24 - *
25 - * @var string Default URL 'https://mainwp.com/'
26 - */
27 - private $upgrade_url = 'https://mainwp.com/';
27 + /**
28 + * Extensions upgrade URL.
29 + *
30 + * @var string Default URL 'https://mainwp.com/'
31 + */
32 + private $upgrade_url = 'https://mainwp.com/';
28 33
29 - /**
30 - * Extensions license renewal URL.
31 - *
32 - * @var string Default 'https://mainwp.com/my-account'
33 - */
34 - private $renew_license_url = 'https://mainwp.com/my-account';
34 + /**
35 + * Extensions license renewal URL.
36 + *
37 + * @var string Default 'https://mainwp.com/my-account'
38 + */
39 + private $renew_license_url = 'https://mainwp.com/my-account';
35 40
36 - /**
37 - * Protected static variable to hold the single instance of the class.
38 - *
39 - * @var mixed Default null
40 - */
41 - protected static $instance = null;
41 + /**
42 + * Protected static variable to hold the single instance of the class.
43 + *
44 + * @var mixed Default null
45 + */
46 + protected static $instance = null;
42 47
43 - /**
44 - * MainWP installation domain name.
45 - *
46 - * @var string $domain Empty by default.
47 - */
48 - public $domain = '';
48 + /**
49 + * MainWP installation domain name.
50 + *
51 + * @var string $domain Empty by default.
52 + */
53 + public $domain = '';
49 54
50 - /**
51 - * Return the single instance of the class.
52 - *
53 - * @return mixed $instance The single instance of the class.
54 - */
55 - public static function instance() {
55 + /**
56 + * Return the single instance of the class.
57 + *
58 + * @return mixed $instance The single instance of the class.
59 + */
60 + public static function instance() {
56 61
57 - if ( is_null( self::$instance ) ) {
58 - self::$instance = new self();
59 - }
62 + if ( is_null( static::$instance ) ) {
63 + static::$instance = new self();
64 + }
60 65
61 - return self::$instance;
62 - }
66 + return static::$instance;
67 + }
63 68
64 - /**
65 - * Cloning is forbidden.
66 - *
67 - * @since 1.2
68 - */
69 - public function __clone() {
70 - }
69 + /**
70 + * Cloning is forbidden.
71 + *
72 + * @since 1.2
73 + */
74 + public function __clone() {
75 + }
71 76
72 - /**
73 - * Un-serializing instances of this class is forbidden.
74 - *
75 - * @since 1.2
76 - */
77 - public function __wakeup() {
78 - }
77 + /**
78 + * Un-serializing instances of this class is forbidden.
79 + *
80 + * @since 1.2
81 + */
82 + public function __wakeup() {
83 + }
79 84
80 - /**
81 - * MainWP_Api_Manager constructor.
82 - *
83 - * Run each time the class is called.
84 - *
85 - * Replace HTTP protocol to HTTPS.
86 - */
87 - public function __construct() {
88 - $this->domain = str_ireplace( array( 'http://', 'https://' ), '', home_url() );
89 - }
85 + /**
86 + * MainWP_Api_Manager constructor.
87 + *
88 + * Run each time the class is called.
89 + *
90 + * Replace HTTP protocol to HTTPS.
91 + */
92 + public function __construct() {
93 + $this->domain = str_ireplace( array( 'http://', 'https://' ), '', home_url() );
94 + }
90 95
91 - /**
92 - * Get Upgrade URL.
93 - *
94 - * @return string Activation upgrade URL.
95 - */
96 - public function get_upgrade_url() {
97 - $url = apply_filters( 'mainwp_api_manager_upgrade_url', $this->upgrade_url );
98 - return $url;
99 - }
96 + /**
97 + * Get Upgrade URL.
98 + *
99 + * @return string Activation upgrade URL.
100 + */
101 + public function get_upgrade_url() {
102 + return apply_filters( 'mainwp_api_manager_upgrade_url', $this->upgrade_url );
103 + }
100 104
101 - /**
102 - * Get domain URL.
103 - *
104 - * @return string domain URL.
105 - */
106 - public function get_domain() {
107 - return $this->domain;
108 - }
105 + /**
106 + * Get domain URL.
107 + *
108 + * @return string domain URL.
109 + */
110 + public function get_domain() {
111 + return $this->domain;
112 + }
109 113
110 - /**
111 - * Get activation info.
112 - *
113 - * @param mixed $ext_key extension key.
114 - *
115 - * @return mixed get_option() get activation information.
116 - */
117 - public function get_activation_info( $ext_key ) {
118 - if ( empty( $ext_key ) ) {
119 - return array();
120 - }
114 + /**
115 + * Get activation info.
116 + *
117 + * @param mixed $ext_key extension key.
118 + *
119 + * @return mixed get_option() get activation information.
120 + */
121 + public function get_activation_info( $ext_key ) {
122 + if ( empty( $ext_key ) ) {
123 + return array();
124 + }
121 125
122 - return get_option( $ext_key . '_APIManAdder' );
123 - }
126 + $info = get_option( $ext_key . '_APIManAdder' );
124 127
125 - /**
126 - * Store activation info.
127 - *
128 - * @param mixed $ext_key Extension key.
129 - * @param mixed $info Activation information.
130 - *
131 - * @return mixed Set activation info.
132 - * @uses \MainWP\Dashboard\MainWP_Utility::update_option()
133 - */
134 - public function set_activation_info( $ext_key, $info ) {
128 + // MWP-1546: per-extension license keys were historically stored as
129 + // plaintext inside the option array's 'api_key' field. New writes
130 + // (set_activation_info below) replace that field with a
131 + // {encrypted_val, file_key} envelope produced by the
132 + // mainwp_encrypt_key_value filter (MainWP_Keys_Manager). On read,
133 + // the matching mainwp_decrypt_key_value filter reverses the
134 + // envelope.
135 + //
136 + // Encrypt-on-first-read migration: if the stored row still carries
137 + // a plaintext api_key string (legacy installs upgraded from before
138 + // this change), rewrite it as the encrypted envelope right now.
139 + // Existing dashboards migrate transparently as each extension's
140 + // option is touched, without waiting for an unrelated activation
141 + // event. We persist via update_option directly rather than calling
142 + // set_activation_info to avoid churning the activations_cached
143 + // option on every legacy read.
144 + if ( is_array( $info ) && ! empty( $info['api_key'] ) && is_string( $info['api_key'] ) ) {
145 + $rewritten = static::encrypt_activation_info( $ext_key, $info );
146 + if ( is_array( $rewritten )
147 + && isset( $rewritten['api_key'] )
148 + && is_array( $rewritten['api_key'] )
149 + && ! empty( $rewritten['api_key']['encrypted_val'] ) ) {
150 + MainWP_Utility::update_option( $ext_key . '_APIManAdder', $rewritten );
151 + }
152 + }
135 153
136 - if ( empty( $ext_key ) ) {
137 - return false;
138 - }
154 + return static::decrypt_activation_info( $info );
155 + }
139 156
140 - // Clear cached of all activations to reload for next loading.
141 - update_option( 'mainwp_extensions_all_activation_cached', '' );
157 + /**
158 + * Reverse the api_key encryption applied by set_activation_info().
159 + *
160 + * Returns the input unchanged when api_key is missing, empty, or a
161 + * plaintext string (legacy format). Falls back to the original value
162 + * if the mainwp_decrypt_key_value filter cannot recover a string, so
163 + * a missing keyfile cannot orphan a license activation.
164 + *
165 + * @param mixed $info Raw option value as returned by get_option().
166 + * @return mixed Same shape as $info, with 'api_key' decrypted to plaintext.
167 + */
168 + public static function decrypt_activation_info( $info ) {
169 + if ( ! is_array( $info ) || empty( $info['api_key'] ) ) {
170 + return $info;
171 + }
172 + if ( ! is_array( $info['api_key'] ) ) {
173 + return $info; // Legacy plaintext string, no envelope to reverse.
174 + }
175 + if ( empty( $info['api_key']['encrypted_val'] ) ) {
176 + // Malformed envelope (array but missing encrypted_val). Drop the
177 + // unreadable value rather than returning the raw array; callers
178 + // (api-handler.php readers, the hooks filter) do not is_string-
179 + // guard the result and would forward the array to the licensing
180 + // API as http_build_query garbage. Mirrors the decrypt-failure
181 + // branch below.
182 + $info['api_key'] = '';
183 + return $info;
184 + }
185 + $decrypted = apply_filters( 'mainwp_decrypt_key_value', false, $info['api_key'], '' );
186 + if ( is_string( $decrypted ) && '' !== $decrypted ) {
187 + $info['api_key'] = $decrypted;
188 + } else {
189 + // Decrypt failed (e.g. missing keyfile). Drop the unreadable
190 + // ciphertext so callers see an empty string rather than the
191 + // raw envelope array, which they would treat as truthy and
192 + // forward to the licensing API as garbage.
193 + $info['api_key'] = '';
194 + }
195 + return $info;
196 + }
142 197
143 - return MainWP_Utility::update_option( $ext_key . '_APIManAdder', $info );
144 - }
198 + /**
199 + * Store activation info.
200 + *
201 + * @param mixed $ext_key Extension key.
202 + * @param mixed $info Activation information.
203 + *
204 + * @return mixed Set activation info.
205 + * @uses \MainWP\Dashboard\MainWP_Utility::update_option()
206 + */
207 + public function set_activation_info( $ext_key, $info ) {
145 208
146 - /**
147 - * Remove activation info.
148 - *
149 - * @param mixed $ext_key Extension key.
150 - *
151 - * @return mixed Remove activation info.
152 - */
153 - public function remove_activation_info( $ext_key ) {
154 - if ( empty( $ext_key ) ) {
155 - return;
156 - }
209 + if ( empty( $ext_key ) ) {
210 + return false;
211 + }
157 212
158 - $options = $this->get_activation_info( $ext_key );
159 - if ( ! is_array( $options ) ) {
160 - $options = array();
161 - }
213 + // Clear cached of all activations to reload for next loading.
214 + update_option( 'mainwp_extensions_all_activation_cached', '' );
162 215
163 - if ( isset( $options['api_key'] ) ) {
164 - $options['api_key'] = '';
165 - }
166 - if ( isset( $options['activated_key'] ) ) {
167 - $options['activated_key'] = 'Deactivated';
168 - }
169 - $this->set_activation_info( $ext_key, $options );
170 - }
216 + // MWP-1546: encrypt the 'api_key' field at rest using the same
217 + // mainwp_encrypt_key_value filter that 3rd-party API keys use. Other
218 + // array members (activated_key, deactivate_checkbox, product_id,
219 + // instance_id, software_version, mainwp_version, product_item_id)
220 + // are not credentials and stay plaintext for backwards compatibility
221 + // with any reader that consumes them directly.
222 + $info = static::encrypt_activation_info( $ext_key, $info );
171 223
172 - /**
173 - * Check API Key & API Email again MainWP Servers.
174 - *
175 - * @param array $api_slug Extension activation info.
176 - * @param string $api_key API license key.
177 - *
178 - * @return array Activation info.
179 - *
180 - * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::activate()
181 - * @uses \MainWP\Dashboard\MainWP_Utility::update_option()
182 - */
183 - public function license_key_activation( $api_slug, $api_key ) {
224 + // Codex follow-up: fail closed when encryption did not produce an
225 + // envelope. encrypt_activation_info() returns the input unchanged
226 + // when the mainwp_encrypt_key_value filter fails (missing keyfile,
227 + // un-writable uploads dir, etc.), which would leave api_key as a
228 + // plaintext string and silently downgrade this write back to the
229 + // pre-MWP-1546 leak path. Refuse the write and let the caller
230 + // surface the error rather than persisting plaintext credentials.
231 + if ( is_array( $info )
232 + && isset( $info['api_key'] )
233 + && is_string( $info['api_key'] )
234 + && '' !== $info['api_key'] ) {
235 + return false;
236 + }
184 237
185 - $options = $this->get_activation_info( $api_slug );
238 + return MainWP_Utility::update_option( $ext_key . '_APIManAdder', $info );
239 + }
186 240
187 - if ( ! is_array( $options ) ) {
188 - $options = array();
189 - }
190 - $current_api_key = isset( $options['api_key'] ) ? $options['api_key'] : '';
191 - $activation_status = isset( $options['activated_key'] ) ? $options['activated_key'] : '';
241 + /**
242 + * Encrypt the api_key field of an activation-info array via the
243 + * mainwp_encrypt_key_value filter (MainWP_Keys_Manager). Replaces the
244 + * plaintext string with the {encrypted_val, file_key} envelope.
245 + *
246 + * @param string $ext_key Extension slug; included in the keyfile prefix.
247 + * @param mixed $info Activation info as supplied by callers.
248 + * @return mixed Same shape as $info, with 'api_key' replaced by the
249 + * encryption envelope (or unchanged if encryption fails).
250 + */
251 + public static function encrypt_activation_info( $ext_key, $info ) {
252 + if ( ! is_array( $info ) ) {
253 + return $info;
254 + }
255 + if ( empty( $info['api_key'] ) || ! is_string( $info['api_key'] ) ) {
256 + return $info;
257 + }
258 + $prefix = 'extension_' . sanitize_key( $ext_key ) . '_';
259 + $envelope = apply_filters( 'mainwp_encrypt_key_value', false, $info['api_key'], $prefix, false );
260 + if ( is_array( $envelope ) && ! empty( $envelope['encrypted_val'] ) ) {
261 + $info['api_key'] = $envelope;
262 + }
263 + return $info;
264 + }
192 265
193 - if ( 'Deactivated' === $activation_status || '' === $activation_status || '' === $api_key || $current_api_key !== $api_key ) {
194 - if ( $current_api_key !== $api_key ) {
195 - $reset = $this->replace_license_key(
196 - array(
197 - 'api_key' => $api_key,
198 - 'product_id' => $options['product_id'],
199 - 'instance' => $options['instance_id'],
200 - 'software_version' => $options['software_version'],
201 - 'software_slug' => $api_slug,
202 - )
203 - );
204 - if ( ! $reset ) {
205 - return array( 'error' => esc_html__( 'The license could not be deactivated.', 'mainwp' ) );
206 - }
207 - }
266 + /**
267 + * Remove activation info.
268 + *
269 + * @param mixed $ext_key Extension key.
270 + *
271 + * @return mixed Remove activation info.
272 + */
273 + public function remove_activation_info( $ext_key ) {
274 + if ( empty( $ext_key ) ) {
275 + return;
276 + }
208 277
209 - $return = array();
278 + $options = $this->get_activation_info( $ext_key );
279 + if ( ! is_array( $options ) ) {
280 + $options = array();
281 + }
210 282
211 - $args = array(
212 - 'api_key' => $api_key,
213 - 'product_id' => $options['product_id'],
214 - 'instance' => $options['instance_id'],
215 - 'software_version' => $options['software_version'],
216 - 'object' => $this->domain,
217 - 'software_slug' => $api_slug,
218 - );
283 + if ( isset( $options['api_key'] ) ) {
284 + $options['api_key'] = '';
285 + }
286 + if ( isset( $options['activated_key'] ) ) {
287 + $options['activated_key'] = 'Deactivated';
288 + }
289 + $this->set_activation_info( $ext_key, $options );
290 + }
219 291
220 - $activate_results = MainWP_Api_Manager_Key::instance()->activate( $args );
221 - if ( ! empty( $activate_results ) ) {
222 - $activate_results = json_decode( $activate_results, true );
223 - } else {
224 - $activate_results = array();
225 - }
292 + /**
293 + * Check API Key & API Email again MainWP Servers.
294 + *
295 + * @param array $api_slug Extension activation info.
296 + * @param string $api_key API license key.
297 + *
298 + * @return array Activation info.
299 + *
300 + * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::activate()
301 + * @uses \MainWP\Dashboard\MainWP_Utility::update_option()
302 + */
303 + public function license_key_activation( $api_slug, $api_key ) { // phpcs:ignore -- NOSONAR - complex.
226 304
227 - if ( true === $activate_results['activated'] ) {
228 - $return['result'] = 'SUCCESS';
229 - $mess = isset( $activate_results['message'] ) ? $activate_results['message'] : '';
230 - $return['message'] = esc_html__( 'The extension has been activated. ', 'mainwp' ) . $mess;
231 - $options['api_key'] = $api_key;
232 - $options['activated_key'] = 'Activated';
233 - $options['deactivate_checkbox'] = 'off';
234 - $options['mainwp_version'] = MainWP_System::instance()->get_dashboard_version();
235 - MainWP_Utility::instance()->get_set_deactivated_licenses_alerted( $api_slug, 0, 'set' ); // so next time deactived it will alerted.
236 - }
305 + $options = $this->get_activation_info( $api_slug );
237 306
238 - if ( false === $activate_results ) {
239 - $apisslverify = get_option( 'mainwp_api_sslVerifyCertificate' );
240 - if ( 1 === $apisslverify ) {
241 - MainWP_Utility::update_option( 'mainwp_api_sslVerifyCertificate', 0 );
242 - $return['retry_action'] = 1;
243 - } else {
244 - $return['error'] = esc_html__( 'Connection failed to the License Key API server. Try again later.', 'mainwp' );
245 - }
246 - $options['api_key'] = '';
247 - $options['activated_key'] = 'Deactivated';
248 - }
307 + if ( ! is_array( $options ) ) {
308 + $options = array();
309 + }
310 + $current_api_key = isset( $options['api_key'] ) ? $options['api_key'] : '';
311 + $activation_status = isset( $options['activated_key'] ) ? $options['activated_key'] : '';
249 312
250 - $error = $this->check_response_for_api_errors( $activate_results );
251 - if ( ! empty( $error ) ) {
252 - $return['error'] = $error;
253 - }
313 + if ( 'Deactivated' === $activation_status || '' === $activation_status || '' === $api_key || $current_api_key !== $api_key ) {
314 + if ( $current_api_key !== $api_key ) {
315 + $reset = $this->replace_license_key(
316 + array(
317 + 'api_key' => $api_key,
318 + 'product_id' => $options['product_id'],
319 + 'instance' => $options['instance_id'],
320 + 'software_version' => $options['software_version'],
321 + 'software_slug' => $api_slug,
322 + )
323 + );
324 + if ( ! $reset ) {
325 + return array( 'error' => esc_html__( 'The license could not be deactivated.', 'mainwp' ) );
326 + }
327 + }
254 328
255 - $this->set_activation_info( $api_slug, $options );
329 + $return = array();
256 330
257 - return $return;
258 - } else {
259 - return array( 'result' => 'SUCCESS' );
260 - }
261 - }
331 + $args = array(
332 + 'api_key' => $api_key,
333 + 'product_id' => $options['product_id'],
334 + 'instance' => $options['instance_id'],
335 + 'software_version' => $options['software_version'],
336 + 'object' => $this->domain,
337 + 'software_slug' => $api_slug,
338 + );
262 339
263 - /**
264 - * Deactivate the current license key before activating the new license key.
265 - *
266 - * @param array $args Request arguments.
267 - *
268 - * @return bool True on success, false on failure.
269 - *
270 - * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::deactivate()
271 - */
272 - private function replace_license_key( $args ) {
273 - $reset = MainWP_Api_Manager_Key::instance()->deactivate( $args ); // reset license key activation.
340 + $activate_results = MainWP_Api_Manager_Key::instance()->activate( $args );
341 + if ( ! empty( $activate_results ) ) {
342 + $activate_results = json_decode( $activate_results, true );
343 + } else {
344 + $activate_results = array();
345 + }
274 346
275 - if ( true === $reset ) {
276 - return true;
277 - }
278 - }
347 + if ( true === $activate_results['activated'] ) {
348 + $return['result'] = 'SUCCESS';
349 + $mess = isset( $activate_results['message'] ) ? $activate_results['message'] : '';
350 + $return['message'] = esc_html__( 'The extension has been activated. ', 'mainwp' ) . $mess;
351 + $options['api_key'] = $api_key;
352 + $options['activated_key'] = 'Activated';
353 + $options['deactivate_checkbox'] = 'off';
354 + $options['mainwp_version'] = MainWP_System::instance()->get_dashboard_version();
355 + MainWP_Utility::instance()->get_set_deactivated_licenses_alerted( $api_slug, 0, 'set' ); // so next time deactived it will alerted.
356 + }
279 357
280 - /**
281 - * Deactivate license Key.
282 - *
283 - * @param array $api_slug Extension activation info.
284 - * @param array $api_key Extension activation api key.
285 - *
286 - * @return array Deactivation info.
287 - *
288 - * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::deactivate()
289 - */
290 - public function license_key_deactivation( $api_slug, $api_key ) {
358 + if ( false === $activate_results ) {
359 + $apisslverify = get_option( 'mainwp_api_sslVerifyCertificate' );
360 + if ( 1 === $apisslverify ) {
361 + MainWP_Utility::update_option( 'mainwp_api_sslVerifyCertificate', 0 );
362 + $return['retry_action'] = 1;
363 + } else {
364 + $return['error'] = esc_html__( 'Connection failed to the License Key API server. Try again later.', 'mainwp' );
365 + }
366 + $options['api_key'] = '';
367 + $options['activated_key'] = 'Deactivated';
368 + }
291 369
292 - $options = $this->get_activation_info( $api_slug );
293 - if ( ! is_array( $options ) ) {
294 - $options = array();
295 - }
370 + $error = $this->check_response_for_api_errors( $activate_results );
371 + if ( ! empty( $error ) ) {
372 + $return['error'] = $error;
373 + }
296 374
297 - $activation_status = isset( $options['activated_key'] ) ? $options['activated_key'] : '';
375 + // MWP-1546 follow-up: surface the fail-closed write so the user is not
376 + // told the activation succeeded when only the upstream half landed.
377 + // The license slot was already consumed at mainwp.com; without the
378 + // local persist the dashboard will offer to re-activate and burn a
379 + // second slot. The encrypt filter only fails on broken installs
380 + // (missing keyfile, un-writable uploads/mainwp/pk/), but when it
381 + // does the operator needs to know.
382 + if ( false === $this->set_activation_info( $api_slug, $options ) ) {
383 + $return['error'] = esc_html__( 'License activated upstream but local state could not be saved. Check uploads/mainwp/pk/ permissions and try the activation again.', 'mainwp' );
384 + }
298 385
299 - if ( empty( $api_key ) ) {
300 - $options['api_key'] = '';
301 - $this->set_activation_info( $api_slug, $options );
302 - }
386 + return $return;
387 + } else {
388 + return array( 'result' => 'SUCCESS' );
389 + }
390 + }
303 391
304 - $return = array();
392 + /**
393 + * Deactivate the current license key before activating the new license key.
394 + *
395 + * @param array $args Request arguments.
396 + *
397 + * @return bool True on success, false on failure.
398 + *
399 + * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::deactivate()
400 + */
401 + private function replace_license_key( $args ) {
402 + $reset = MainWP_Api_Manager_Key::instance()->deactivate( $args ); // reset license key activation.
305 403
306 - if ( 'Activated' === $activation_status && '' !== $api_key ) {
307 - $activate_results = MainWP_Api_Manager_Key::instance()->deactivate(
308 - array(
309 - 'product_id' => $options['product_id'],
310 - 'instance' => $options['instance_id'],
311 - 'api_key' => $api_key,
312 - 'object' => $this->domain,
313 - 'software_version' => $options['software_version'],
314 - 'software_slug' => $api_slug,
315 - )
316 - ); // reset license key activation.
404 + if ( true === $reset ) {
405 + return true;
406 + }
407 + }
317 408
318 - $activate_results = json_decode( $activate_results, true );
319 - if ( isset( $activate_results['deactivated'] ) && true === $activate_results['deactivated'] ) {
320 - $options['api_key'] = '';
321 - $options['activated_key'] = 'Deactivated';
322 - $options['deactivate_checkbox'] = 'on';
323 - $return['result'] = 'SUCCESS';
324 - $return['activations_remaining'] = $activate_results['activations_remaining'];
325 - }
409 + /**
410 + * Deactivate license Key.
411 + *
412 + * @param array $api_slug Extension activation info.
413 + * @param array $api_key Extension activation api key.
414 + *
415 + * @return array Deactivation info.
416 + *
417 + * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::deactivate()
418 + */
419 + public function license_key_deactivation( $api_slug, $api_key ) {
326 420
327 - $error = $this->check_response_for_api_errors( $activate_results );
328 - if ( ! empty( $error ) ) {
329 - $return['error'] = $error;
330 - $options['api_key'] = '';
331 - $options['activated_key'] = 'Deactivated';
332 - }
421 + $options = $this->get_activation_info( $api_slug );
422 + if ( ! is_array( $options ) ) {
423 + $options = array();
424 + }
333 425
334 - $this->set_activation_info( $api_slug, $options );
426 + $activation_status = isset( $options['activated_key'] ) ? $options['activated_key'] : '';
335 427
336 - return $return;
337 - }
338 - // to fix: clear cached of all activations to reload for next loading.
339 - update_option( 'mainwp_extensions_all_activation_cached', '' );
340 - return array( 'result' => 'SUCCESS' );
341 - }
428 + if ( empty( $api_key ) ) {
429 + $options['api_key'] = '';
430 + $this->set_activation_info( $api_slug, $options );
431 + }
342 432
343 - /**
344 - * Test the users MainWP.com Login details against MainWP Server.
345 - *
346 - * @param string $api_key MainWP api key.
347 - *
348 - * @return mixed Login test result.
349 - *
350 - * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::verify_api_key()
351 - */
352 - public function verify_mainwp_api( $api_key ) {
353 - if ( empty( $api_key ) ) {
354 - return false;
355 - }
433 + $return = array();
356 434
357 - return MainWP_Api_Manager_Key::instance()->verify_api_key(
358 - array(
359 - 'api_key' => $api_key,
360 - )
361 - );
362 - }
435 + if ( 'Activated' === $activation_status && '' !== $api_key ) {
436 + $activate_results = MainWP_Api_Manager_Key::instance()->deactivate(
437 + array(
438 + 'product_id' => $options['product_id'],
439 + 'instance' => $options['instance_id'],
440 + 'api_key' => $api_key,
441 + 'object' => $this->domain,
442 + 'software_version' => $options['software_version'],
443 + 'software_slug' => $api_slug,
444 + )
445 + ); // reset license key activation.
363 446
447 + $activate_results = json_decode( $activate_results, true );
448 + if ( isset( $activate_results['deactivated'] ) && true === $activate_results['deactivated'] ) {
449 + $options['api_key'] = '';
450 + $options['activated_key'] = 'Deactivated';
451 + $options['deactivate_checkbox'] = 'on';
452 + $return['result'] = 'SUCCESS';
453 + $return['activations_remaining'] = $activate_results['activations_remaining'];
454 + }
364 455
365 - /**
366 - * Check if the user purchased the software.
367 - *
368 - * @param string $productId extension (product) ID.
369 - *
370 - * @return mixed purchase_software() purchase extensions.
371 - *
372 - * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::purchase_software()
373 - */
374 - public function purchase_software( $productId ) {
375 - return MainWP_Api_Manager_Key::instance()->purchase_software(
376 - array(
377 - 'product_id' => $productId,
378 - )
379 - );
380 - }
456 + $error = $this->check_response_for_api_errors( $activate_results );
457 + if ( ! empty( $error ) ) {
458 + $return['error'] = $error;
459 + $options['api_key'] = '';
460 + $options['activated_key'] = 'Deactivated';
461 + }
381 462
382 - /**
383 - * Get users purchased extensions.
384 - *
385 - * @param string $api_key api key.
386 - * @param string $productId extension (product) ID.
387 - * @param bool $no_register registration request.
388 - *
389 - * @return array Purchased extensions.
390 - *
391 - * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::get_purchased_software()
392 - */
393 - public function get_purchased_extension( $api_key, $productId = '', $no_register = false ) {
394 - if ( empty( $api_key ) ) {
395 - return false;
396 - }
463 + // MWP-1546 follow-up: surface the fail-closed write. The slot was
464 + // already released at mainwp.com; without the local persist the
465 + // dashboard would still display the extension as Activated.
466 + if ( false === $this->set_activation_info( $api_slug, $options ) ) {
467 + $return['error'] = esc_html__( 'License deactivated upstream but local state could not be cleared. Check uploads/mainwp/pk/ permissions.', 'mainwp' );
468 + }
397 469
398 - return MainWP_Api_Manager_Key::instance()->get_purchased_software(
399 - array(
400 - 'product_id' => $productId,
401 - 'api_key' => $api_key,
402 - 'noauth' => $no_register ? 1 : 0,
403 - )
404 - );
405 - }
470 + return $return;
471 + }
472 + // to fix: clear cached of all activations to reload for next loading.
473 + update_option( 'mainwp_extensions_all_activation_cached', '' );
474 + return array( 'result' => 'SUCCESS' );
475 + }
406 476
407 - /**
408 - * Grab users associate MainWP License key for the selected Extension.
409 - *
410 - * @param array $api_slug Extension activation info.
411 - * @param string $master_api_key MainWP master api key.
412 - *
413 - * @return mixed Activation info.
414 - *
415 - * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::grab_api_key()
416 - */
417 - public function grab_license_key( $api_slug, $master_api_key ) { // phpcs:ignore -- Current complexity is the only way to achieve desired results, pull request solutions appreciated.
477 + /**
478 + * Test the users MainWP.com Login details against MainWP Server.
479 + *
480 + * @param string $api_key MainWP api key.
481 + *
482 + * @return mixed Login test result.
483 + *
484 + * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::verify_api_key()
485 + */
486 + public function verify_mainwp_api( $api_key ) {
487 + if ( empty( $api_key ) ) {
488 + return false;
489 + }
418 490
419 - $options = $this->get_activation_info( $api_slug );
420 - if ( ! is_array( $options ) ) {
421 - $options = array();
422 - }
423 - $activation_status = isset( $options['activated_key'] ) ? $options['activated_key'] : '';
424 - $api_key = isset( $options['api_key'] ) ? $options['api_key'] : '';
491 + return MainWP_Api_Manager_Key::instance()->verify_api_key(
492 + array(
493 + 'api_key' => $api_key,
494 + )
495 + );
496 + }
425 497
426 - if ( ! isset( $options['product_item_id'] ) ) {
427 - $options['product_item_id'] = 0; // to compatible.
428 - }
429 498
430 - if ( 'Deactivated' === $activation_status || '' === $activation_status || '' === $api_key || empty( $options['product_item_id'] ) ) {
431 - $return = array();
432 - if ( ! empty( $master_api_key ) ) {
433 - $args = array(
434 - 'api_key' => $master_api_key,
435 - 'product_id' => isset( $options['product_id'] ) ? $options['product_id'] : '',
436 - 'instance' => isset( $options['instance_id'] ) ? $options['instance_id'] : '',
437 - 'software_version' => isset( $options['software_version'] ) ? $options['software_version'] : '',
438 - 'object' => $this->domain,
439 - 'software_slug' => $api_slug,
440 - );
499 + /**
500 + * Check if the user purchased the software.
501 + *
502 + * @param string $productId extension (product) ID.
503 + *
504 + * @return mixed purchase_software() purchase extensions.
505 + *
506 + * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::purchase_software()
507 + */
508 + public function purchase_software( $productId ) {
509 + return MainWP_Api_Manager_Key::instance()->purchase_software(
510 + array(
511 + 'product_id' => $productId,
512 + )
513 + );
514 + }
441 515
442 - $activate_results = MainWP_Api_Manager_Key::instance()->grab_api_key( $args );
443 - if ( ! empty( $activate_results ) ) {
444 - $activate_results = json_decode( $activate_results, true );
445 - } else {
446 - $activate_results = array();
447 - }
516 + /**
517 + * Get users purchased extensions.
518 + *
519 + * @param string $api_key api key.
520 + * @param string $productId extension (product) ID.
521 + * @param bool $no_register registration request.
522 + *
523 + * @return array Purchased extensions.
524 + *
525 + * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::get_purchased_software()
526 + */
527 + public function get_purchased_extension( $api_key, $productId = '', $no_register = false ) {
528 + if ( empty( $api_key ) ) {
529 + return false;
530 + }
448 531
449 - $options['api_key'] = '';
450 - $options['activated_key'] = 'Deactivated';
532 + return MainWP_Api_Manager_Key::instance()->get_purchased_software(
533 + array(
534 + 'product_id' => $productId,
535 + 'api_key' => $api_key,
536 + 'noauth' => $no_register ? 1 : 0,
537 + )
538 + );
539 + }
451 540
452 - if ( ! isset( $options['product_item_id'] ) ) {
453 - $options['product_item_id'] = 0; // to compatible.
454 - }
541 + /**
542 + * Grab users associate MainWP License key for the selected Extension.
543 + *
544 + * @param array $api_slug Extension activation info.
545 + * @param string $master_api_key MainWP master api key.
546 + *
547 + * @return mixed Activation info.
548 + *
549 + * @uses \MainWP\Dashboard\MainWP_Api_Manager_Key::grab_api_key()
550 + */
551 + public function grab_license_key( $api_slug, $master_api_key ) { // phpcs:ignore -- NOSONAR - Current complexity is the only way to achieve desired results, pull request solutions appreciated.
455 552
456 - if ( is_array( $activate_results ) && isset( $activate_results['activated'] ) && ( true === $activate_results['activated'] ) && ! empty( $activate_results['api_key'] ) ) {
457 - $return['result'] = 'SUCCESS';
458 - $mess = isset( $activate_results['message'] ) ? $activate_results['message'] : '';
459 - $return['message'] = esc_html__( 'Extension activated. ', 'mainwp' ) . $mess;
460 - $options['api_key'] = $activate_results['api_key'];
461 - $return['api_key'] = $activate_results['api_key'];
462 - $options['activated_key'] = 'Activated';
463 - $options['product_item_id'] = isset( $activate_results['product_item_id'] ) ? intval( $activate_results['product_item_id'] ) : 0;
464 - $options['deactivate_checkbox'] = 'off';
465 - } elseif ( false === $activate_results ) {
466 - $return['error'] = esc_html__( 'Connection with the API license server could not be established. Please, try again later.', 'mainwp' );
467 - } elseif ( isset( $activate_results['error'] ) ) {
468 - $return['error'] = $activate_results['error'];
469 - } elseif ( empty( $activate_results['api_key'] ) ) {
470 - $return['error'] = esc_html__( 'License key could not be found.', 'mainwp' );
471 - } else {
472 - $return['error'] = esc_html__( 'An undefined error occurred. Please try again later.', 'mainwp' );
473 - }
553 + $options = $this->get_activation_info( $api_slug );
554 + if ( ! is_array( $options ) ) {
555 + $options = array();
556 + }
557 + $activation_status = isset( $options['activated_key'] ) ? $options['activated_key'] : '';
558 + $api_key = isset( $options['api_key'] ) ? $options['api_key'] : '';
474 559
475 - $error = $this->check_response_for_api_errors( $activate_results );
476 - if ( ! empty( $error ) ) {
477 - $return['error'] = $error;
478 - }
560 + if ( ! isset( $options['product_item_id'] ) ) {
561 + $options['product_item_id'] = 0; // to compatible.
562 + }
479 563
480 - $this->set_activation_info( $api_slug, $options );
564 + if ( 'Deactivated' === $activation_status || '' === $activation_status || '' === $api_key || empty( $options['product_item_id'] ) ) {
565 + $return = array();
566 + if ( ! empty( $master_api_key ) ) {
567 + $args = array(
568 + 'api_key' => $master_api_key,
569 + 'product_id' => isset( $options['product_id'] ) ? $options['product_id'] : '',
570 + 'instance' => isset( $options['instance_id'] ) ? $options['instance_id'] : '',
571 + 'software_version' => isset( $options['software_version'] ) ? $options['software_version'] : '',
572 + 'object' => $this->domain,
573 + 'software_slug' => $api_slug,
574 + );
481 575
482 - return $return;
483 - } else {
484 - return array( 'error' => esc_html__( 'MainWP API key are required in order to grab extensions API keys.', 'mainwp' ) );
485 - }
486 - }
576 + $activate_results = MainWP_Api_Manager_Key::instance()->grab_api_key( $args );
577 + if ( ! empty( $activate_results ) ) {
578 + $activate_results = json_decode( $activate_results, true );
579 + } else {
580 + $activate_results = array();
581 + }
487 582
488 - return array( 'result' => 'SUCCESS' );
489 - }
583 + $options['api_key'] = '';
584 + $options['activated_key'] = 'Deactivated';
490 585
491 - /**
492 - * Check if $response contains any api errors.
493 - *
494 - * @param array $response Response array.
495 - *
496 - * @return string $error Error message.
497 - */
498 - public function check_response_for_api_errors( $response ) {
499 - if ( ! is_array( $response ) || ! isset( $response['code'] ) ) {
500 - return false;
501 - }
586 + if ( ! isset( $options['product_item_id'] ) ) {
587 + $options['product_item_id'] = 0; // to compatible.
588 + }
502 589
503 - if ( isset( $response['error'] ) ) {
504 - return $response['error'];
505 - }
590 + if ( is_array( $activate_results ) && isset( $activate_results['activated'] ) && ( true === $activate_results['activated'] ) && ! empty( $activate_results['api_key'] ) ) {
591 + $return['result'] = 'SUCCESS';
592 + $mess = isset( $activate_results['message'] ) ? $activate_results['message'] : '';
593 + $return['message'] = esc_html__( 'Extension activated. ', 'mainwp' ) . $mess;
594 + $options['api_key'] = $activate_results['api_key'];
595 + $return['api_key'] = $activate_results['api_key'];
596 + $options['activated_key'] = 'Activated';
597 + $options['product_item_id'] = isset( $activate_results['product_item_id'] ) ? intval( $activate_results['product_item_id'] ) : 0;
598 + $options['deactivate_checkbox'] = 'off';
599 + } elseif ( false === $activate_results ) {
600 + $return['error'] = esc_html__( 'Connection with the API license server could not be established. Please, try again later.', 'mainwp' );
601 + } elseif ( isset( $activate_results['error'] ) ) {
602 + $return['error'] = $activate_results['error'];
603 + } elseif ( empty( $activate_results['api_key'] ) ) {
604 + $return['error'] = esc_html__( 'License key could not be found.', 'mainwp' );
605 + } else {
606 + $return['error'] = esc_html__( 'An undefined error occurred. Please try again later.', 'mainwp' );
607 + }
506 608
507 - $error = '';
508 - switch ( $response['code'] ) {
509 - case '100':
510 - $error = esc_html__( 'Activation error! Please try to deactivate and re-activate the extension on the WP > Plugins page and try to activate API key again.', 'mainwp' );
511 - break;
512 - case '102':
513 - $error = esc_html__( 'Activation error! Download permission for this product could not be found.', 'mainwp' );
514 - break;
515 - case '101':
516 - $error = esc_html__( 'Activation error! Matching API key could not be found.', 'mainwp' );
517 - break;
518 - case '103':
519 - case '104':
520 - $error = esc_html__( 'Invalid Instance ID! Please try to deactivate and re-activate the extension on the WP > Plugins page and try to activate API key again.', 'mainwp' );
521 - break;
522 - case '105':
523 - case '106':
524 - $error = isset( $response['error'] ) ? $response['error'] : '';
525 - $info = isset( $response['additional info'] ) ? ' ' . $response['additional info'] : '';
526 - $error = $error . $info;
527 - break;
528 - case '900':
529 - $error = esc_html__( 'Your membership is on hold. Reactivate your membership to activate MainWP extensions', 'mainwp' );
530 - break;
531 - case '901':
532 - $error = esc_html__( 'Your membership has been canceled. Reactivate your membership to activate MainWP extensions', 'mainwp' );
533 - break;
534 - case '902':
535 - $error = esc_html__( 'Your membership has expired. Reactivate your membership to activate MainWP extensions', 'mainwp' );
536 - break;
537 - }
538 - return $error;
539 - }
609 + $error = $this->check_response_for_api_errors( $activate_results );
610 + if ( ! empty( $error ) ) {
611 + $return['error'] = $error;
612 + }
540 613
541 - /**
542 - * Check if $response contains any install errors.
543 - *
544 - * @param array $response Response array.
545 - * @param string $software_title Extension title.
546 - *
547 - * @return string $return Installation Error messages.
548 - */
549 - public function check_response_for_intall_errors( $response, $software_title = '' ) {
550 - if ( ! is_array( $response ) || ! isset( $response['error'] ) ) {
551 - return false;
552 - }
614 + // MWP-1546 follow-up: surface the fail-closed write. The grab
615 + // call may have produced a fresh license key upstream; if the
616 + // local persist fails the dashboard would lose track of it
617 + // entirely.
618 + if ( false === $this->set_activation_info( $api_slug, $options ) ) {
619 + $return['error'] = esc_html__( 'License retrieved upstream but local state could not be saved. Check uploads/mainwp/pk/ permissions and try again.', 'mainwp' );
620 + }
553 621
554 - $return = false;
555 - switch ( $response['error'] ) {
556 - case 'subscription_on_hold':
557 - $return = esc_html__( 'Your membership is on hold. Reactivate your membership to install MainWP extensions.', 'mainwp' );
558 - break;
559 - case 'subscription_cancelled':
560 - $return = esc_html__( 'Your membership has been canceled. Reactivate your membership to install MainWP extensions.', 'mainwp' );
561 - break;
562 - case 'subscription_expired':
563 - $return = esc_html__( 'Your membership has expired. Reactivate your membership to install MainWP extensions.', 'mainwp' );
564 - break;
565 - default: // download_revoked.
566 - $return = sprintf( esc_html__( 'Download permission for %1$s has been revoked possibly due to a license key or membership expiring. You can reactivate or purchase a license key from your account <a href="%2$s" target="_blank">dashboard</a>.', 'mainwp' ), $software_title, $this->renew_license_url );
567 - break;
568 - }
569 - return $return;
570 - }
622 + return $return;
623 + } else {
624 + return array( 'error' => esc_html__( 'MainWP API key are required in order to grab extensions API keys.', 'mainwp' ) );
625 + }
626 + }
571 627
572 - /**
573 - * Request plugin information
574 - *
575 - * @param array $args Request arguments.
576 - *
577 - * @return array Plugin info.
578 - *
579 - * @uses MainWP_Api_Manager_Plugin_Update::request()
580 - */
581 - public function request_extension_information( $args ) {
582 - return MainWP_Api_Manager_Plugin_Update::instance()->request( $args );
583 - }
628 + return array( 'result' => 'SUCCESS' );
629 + }
630 +
631 + /**
632 + * Check if $response contains any api errors.
633 + *
634 + * @param array $response Response array.
635 + *
636 + * @return string $error Error message.
637 + */
638 + public function check_response_for_api_errors( $response ) { //phpcs:ignore -- complex.
639 + if ( ! is_array( $response ) || ! isset( $response['code'] ) ) {
640 + return false;
641 + }
642 +
643 + if ( isset( $response['error'] ) ) {
644 + return $response['error'];
645 + }
646 +
647 + $error = '';
648 + switch ( $response['code'] ) {
649 + case '100':
650 + $error = esc_html__( 'Activation error! Please try to deactivate and re-activate the extension on the WP > Plugins page and try to activate API key again.', 'mainwp' );
651 + break;
652 + case '102':
653 + $error = esc_html__( 'Activation error! Download permission for this product could not be found.', 'mainwp' );
654 + break;
655 + case '101':
656 + $error = esc_html__( 'Activation error! Matching API key could not be found.', 'mainwp' );
657 + break;
658 + case '103':
659 + case '104':
660 + $error = esc_html__( 'Invalid Instance ID! Please try to deactivate and re-activate the extension on the WP > Plugins page and try to activate API key again.', 'mainwp' );
661 + break;
662 + case '105':
663 + case '106':
664 + $error = isset( $response['error'] ) ? $response['error'] : '';
665 + $info = isset( $response['additional info'] ) ? ' ' . $response['additional info'] : '';
666 + $error = $error . $info;
667 + break;
668 + case '900':
669 + $error = esc_html__( 'Your membership is on hold. Reactivate your membership to activate MainWP extensions', 'mainwp' );
670 + break;
671 + case '901':
672 + $error = esc_html__( 'Your membership has been canceled. Reactivate your membership to activate MainWP extensions', 'mainwp' );
673 + break;
674 + case '902':
675 + $error = esc_html__( 'Your membership has expired. Reactivate your membership to activate MainWP extensions', 'mainwp' );
676 + break;
677 + default:
678 + break;
679 + }
680 + return $error;
681 + }
682 +
683 + /**
684 + * Check if $response contains any install errors.
685 + *
686 + * @param array $response Response array.
687 + * @param string $software_title Extension title.
688 + *
689 + * @return string $return Installation Error messages.
690 + */
691 + public function check_response_for_intall_errors( $response, $software_title = '' ) {
692 + if ( ! is_array( $response ) || ! isset( $response['error'] ) ) {
693 + return false;
694 + }
695 +
696 + $return = false;
697 + switch ( $response['error'] ) {
698 + case 'subscription_on_hold':
699 + $return = esc_html__( 'Your membership is on hold. Reactivate your membership to install MainWP extensions.', 'mainwp' );
700 + break;
701 + case 'subscription_cancelled':
702 + $return = esc_html__( 'Your membership has been canceled. Reactivate your membership to install MainWP extensions.', 'mainwp' );
703 + break;
704 + case 'subscription_expired':
705 + $return = esc_html__( 'Your membership has expired. Reactivate your membership to install MainWP extensions.', 'mainwp' );
706 + break;
707 + default: // download_revoked.
708 + /* translators: 1: Extension/software title, 2: URL to account dashboard */
709 + $return = sprintf( esc_html__( 'Download permission for %1$s has been revoked possibly due to a license key or membership expiring. You can reactivate or purchase a license key from your account <a href="%2$s" target="_blank">dashboard</a>.', 'mainwp' ), $software_title, $this->renew_license_url );
710 + break;
711 + }
712 + return $return;
713 + }
714 +
715 + /**
716 + * Request plugin information
717 + *
718 + * @param array $args Request arguments.
719 + *
720 + * @return array Plugin info.
721 + *
722 + * @uses MainWP_Api_Manager_Plugin_Update::request()
723 + */
724 + public function request_extension_information( $args ) {
725 + return MainWP_Api_Manager_Plugin_Update::instance()->request( $args );
726 + }
584 727 }
585 728
586 729 // End of class.