reason = $reason; $this->status = $status; } } function picsart_runtime_require($condition, $reason, $message, $status = 400) { if (!$condition) throw new Picsart_Runtime_Error(esc_html($reason), esc_html($message), absint($status)); } function picsart_runtime_key($purpose) { // The stored site URL has a fixed scheme; site_url() can switch it for HTTP cron requests. return hash_hmac('sha256', 'picsart-runtime-v2|' . untrailingslashit(get_option('siteurl')) . '|' . wp_get_environment_type() . '|' . get_current_blog_id() . '|' . get_current_user_id() . '|' . $purpose, wp_salt('auth'), true); } function picsart_runtime_seal($data, $purpose) { picsart_runtime_require(function_exists('openssl_encrypt'), 'ENCRYPTION', __('PHP OpenSSL is required to protect your Picsart connection.', 'picsart-commerce'), 503); $iv = random_bytes(12); $tag = ''; $cipher = openssl_encrypt(wp_json_encode($data), 'aes-256-gcm', picsart_runtime_key($purpose), OPENSSL_RAW_DATA, $iv, $tag, $purpose); picsart_runtime_require($cipher !== false, 'ENCRYPTION', __('Could not protect the Picsart connection.', 'picsart-commerce'), 503); return base64_encode($iv . $tag . $cipher); } function picsart_runtime_unseal($value, $purpose) { if (!$value) return []; $bytes = base64_decode($value, true); picsart_runtime_require($bytes !== false && strlen($bytes) > 28, 'STORAGE', __('Stored Picsart data is unavailable. Reconnect your account.', 'picsart-commerce'), 503); $plain = openssl_decrypt(substr($bytes, 28), 'aes-256-gcm', picsart_runtime_key($purpose), OPENSSL_RAW_DATA, substr($bytes, 0, 12), substr($bytes, 12, 16), $purpose); $data = $plain === false ? null : json_decode($plain, true); picsart_runtime_require(is_array($data), 'STORAGE', __('Stored Picsart data could not be verified. Reconnect your account.', 'picsart-commerce'), 503); return $data; } /** Atomic per-user lease; compare-and-delete prevents an expired owner releasing a newer lease. */ final class Picsart_Runtime_Store { public $data; private $key; private $lease; private $lock; private $encoded; public function __construct($reset_unreadable = false) { picsart_runtime_require(get_current_user_id() > 0, 'AUTH', __('Sign in to WordPress first.', 'picsart-commerce'), 401); $this->key = '_picsart_runtime_' . get_current_user_id(); $this->lock = $this->key . '_lock'; $this->lease = (time() + 180) . ':' . wp_generate_uuid4(); $old = get_option($this->lock); if ($old && (int) $old < time()) $this->delete_lease($old); $acquired = picsart_option_claim($this->lock,$this->lease); picsart_runtime_require($acquired, 'BUSY', __('Another Picsart action is finishing. Please try again shortly.', 'picsart-commerce'), 409); try { wp_cache_delete($this->key, 'options'); $this->encoded = get_option($this->key, ''); try { $data = picsart_runtime_unseal($this->encoded, $this->key); } catch (Picsart_Runtime_Error $error) { if (!$reset_unreadable || $error->reason !== 'STORAGE') throw $error; // Preserve the old ciphertext for recovery with the previous site salt. // Reset never replays or resumes any unreadable approval. $quarantine = $this->key . '_unreadable_' . hash('sha256', $this->encoded); picsart_runtime_require(add_option($quarantine, $this->encoded, '', false) || get_option($quarantine) === $this->encoded, 'STORAGE', __('Could not preserve the previous workspace. Reset was cancelled.', 'picsart-commerce'), 503); $data = []; $this->data = ['jobs'=>[], 'quotes'=>[], 'sources'=>[], 'savedTemplates'=>[]]; $this->save(); } $this->data = $data + ['jobs'=>[], 'quotes'=>[], 'sources'=>[], 'savedTemplates'=>[]]; $interrupted = false; foreach ($this->data['jobs'] as &$job) { if (!in_array($job['status'], ['SUBMITTING','IMPORTING'], true)) continue; $job['status'] = $job['status'] === 'SUBMITTING' ? 'UNKNOWN_SUBMISSION' : 'IMPORT_UNKNOWN'; $job['events'][] = ['at'=>time()*1000,'message'=>__('The previous request was interrupted. Inspect its existing result before trying again.', 'picsart-commerce')]; $interrupted = true; } unset($job); if ($interrupted) $this->save(); } catch (Throwable $error) { $this->release(); throw $error; } } private function delete_lease($value) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic lease ownership comparison; option cache invalidated immediately. $wpdb->delete($wpdb->options, ['option_name'=>$this->lock, 'option_value'=>$value]); wp_cache_delete($this->lock, 'options'); wp_cache_delete('notoptions', 'options'); } public function save() { global $wpdb; $sealed = picsart_runtime_seal($this->data, $this->key); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Fenced compare-and-swap joins the durable lease to the write; cache cleared below. WP option APIs cannot perform this atomic operation. if ($this->encoded === '') { $changed = $wpdb->query($wpdb->prepare("INSERT IGNORE INTO %i (option_name,option_value,autoload) SELECT %s,%s,'off' FROM %i WHERE option_name=%s AND BINARY option_value=%s AND CAST(option_value AS UNSIGNED)>=%d", $wpdb->options, $this->key, $sealed, $wpdb->options, $this->lock, $this->lease, time())); } else { $changed = $wpdb->query($wpdb->prepare("UPDATE %i AS state_row JOIN %i AS lease_row ON lease_row.option_name=%s SET state_row.option_value=%s WHERE state_row.option_name=%s AND BINARY state_row.option_value=%s AND BINARY lease_row.option_value=%s AND CAST(lease_row.option_value AS UNSIGNED)>=%d", $wpdb->options, $wpdb->options, $this->lock, $sealed, $this->key, $this->encoded, $this->lease, time())); } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching wp_cache_delete($this->key, 'options'); wp_cache_delete('notoptions', 'options'); picsart_runtime_require($changed === 1, 'LEASE', __('WordPress could not save this action or its lease expired. Check its result before retrying.', 'picsart-commerce'), 503); $this->encoded = $sealed; } public function release() { $this->delete_lease($this->lease); } } function picsart_runtime_instance() { $id = get_option('_picsart_runtime_instance'); if (!$id) { picsart_option_claim('_picsart_runtime_instance', wp_generate_uuid4()); $id = get_option('_picsart_runtime_instance'); } return $id; }