30, 'redirection'=>0, 'limit_response_size'=>8 * MB_IN_BYTES]); picsart_runtime_require(!is_wp_error($response), 'NETWORK', __('Picsart did not respond. Check the result of your last action before retrying.', 'picsart-commerce'), 502); return ['status'=>wp_remote_retrieve_response_code($response), 'body'=>json_decode(wp_remote_retrieve_body($response), true)]; } function picsart_runtime_discovery() { $meta = get_transient('picsart_runtime_oauth_discovery'); if (is_array($meta)) return $meta; $r = picsart_runtime_http(picsart_runtime_issuer() . '/.well-known/oauth-authorization-server'); $meta = $r['body']; picsart_runtime_require($r['status'] === 200 && is_array($meta) && ($meta['issuer'] ?? '') === picsart_runtime_issuer(), 'LOGIN', __('Picsart sign-in discovery is unavailable.', 'picsart-commerce'), 502); foreach (['device_authorization_endpoint', 'token_endpoint', 'jwks_uri'] as $field) { picsart_runtime_require(is_string($meta[$field] ?? null) && str_starts_with($meta[$field], picsart_runtime_issuer() . '/') && !str_contains($meta[$field], '..'), 'LOGIN', __('Picsart returned an unexpected authentication endpoint.', 'picsart-commerce'), 502); } set_transient('picsart_runtime_oauth_discovery', $meta, HOUR_IN_SECONDS); return $meta; } function picsart_runtime_form($url, $fields) { return picsart_runtime_http($url, ['method'=>'POST', 'headers'=>['Content-Type'=>'application/x-www-form-urlencoded'], 'body'=>http_build_query($fields, '', '&', PHP_QUERY_RFC3986)]); } function picsart_runtime_b64($text) { return base64_decode(strtr($text, '-_', '+/'), true); } function picsart_runtime_der($tag, $bytes) { $length = strlen($bytes); if ($length < 128) return chr($tag) . chr($length) . $bytes; $encoded = ''; for ($n = $length; $n; $n >>= 8) $encoded = chr($n & 255) . $encoded; return chr($tag) . chr(128 | strlen($encoded)) . $encoded . $bytes; } function picsart_runtime_jwk_pem($key) { $n = picsart_runtime_b64($key['n'] ?? ''); $e = picsart_runtime_b64($key['e'] ?? ''); picsart_runtime_require($n && $e && strlen($n) >= 256, 'TOKEN', __('Picsart identity key is invalid.', 'picsart-commerce'), 502); $integer = static function ($bytes) { $bytes = ltrim($bytes, "\0"); if (ord($bytes[0]) & 128) $bytes = "\0" . $bytes; return picsart_runtime_der(2, $bytes); }; $rsa = picsart_runtime_der(48, $integer($n) . $integer($e)); $algorithm = hex2bin('300d06092a864886f70d0101010500'); return "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode(picsart_runtime_der(48, $algorithm . picsart_runtime_der(3, "\0" . $rsa))), 64, "\n") . "-----END PUBLIC KEY-----\n"; } function picsart_runtime_verify_token($token) { $parts = explode('.', $token); picsart_runtime_require(count($parts) === 3, 'TOKEN', __('Picsart identity could not be verified.', 'picsart-commerce'), 401); $header = json_decode(picsart_runtime_b64($parts[0]), true); $claims = json_decode(picsart_runtime_b64($parts[1]), true); picsart_runtime_require(($header['alg'] ?? '') === 'RS256' && is_string($header['kid'] ?? null) && is_array($claims), 'TOKEN', __('Picsart identity could not be verified.', 'picsart-commerce'), 401); $keys = get_transient('picsart_runtime_jwks'); if (!is_array($keys) || !array_filter($keys, static fn($k)=>($k['kid'] ?? '') === $header['kid'])) { $r = picsart_runtime_http(picsart_runtime_discovery()['jwks_uri']); picsart_runtime_require($r['status'] === 200 && is_array($r['body']['keys'] ?? null), 'TOKEN', __('Picsart identity verification is unavailable.', 'picsart-commerce'), 502); $keys = $r['body']['keys']; set_transient('picsart_runtime_jwks', $keys, HOUR_IN_SECONDS); } $verified = false; foreach ($keys as $key) { if (($key['kid'] ?? '') !== $header['kid'] || ($key['kty'] ?? '') !== 'RSA' || ($key['use'] ?? 'sig') !== 'sig' || ($key['alg'] ?? 'RS256') !== 'RS256') continue; $signature = picsart_runtime_b64($parts[2]); $verified = $signature !== false && openssl_verify($parts[0] . '.' . $parts[1], $signature, picsart_runtime_jwk_pem($key), OPENSSL_ALGO_SHA256) === 1; } picsart_runtime_require($verified && ($claims['iss'] ?? '') === picsart_runtime_issuer() && ($claims['client_id'] ?? '') === picsart_runtime_client() && is_string($claims['sub'] ?? null) && $claims['sub'] !== '' && is_numeric($claims['exp'] ?? null) && $claims['exp'] > time() - 30 && ($claims['nbf'] ?? 0) <= time() + 30, 'TOKEN', __('Picsart identity verification failed. Connect again.', 'picsart-commerce'), 401); return $claims; } function picsart_runtime_credentials($body, $previous = []) { picsart_runtime_require(is_array($body) && is_string($body['access_token'] ?? null) && is_numeric($body['expires_in'] ?? null) && $body['expires_in'] > 0, 'TOKEN', __('Picsart returned an invalid sign-in response.', 'picsart-commerce'), 502); $claims = picsart_runtime_verify_token($body['access_token']); picsart_runtime_require(empty($previous['subject']) || $previous['subject'] === $claims['sub'], 'ACCOUNT_CHANGED', __('The Picsart account changed. Connect again.', 'picsart-commerce'), 401); return ['accessToken'=>$body['access_token'], 'refreshToken'=>$body['refresh_token'] ?? $previous['refreshToken'] ?? null, 'expiresAt'=>time() + (int)$body['expires_in'], 'subject'=>$claims['sub'], 'name'=>$claims['name'] ?? $previous['name'] ?? 'Picsart account', 'scope'=>$body['scope'] ?? $claims['scope'] ?? $previous['scope'] ?? '']; } function picsart_runtime_pending($attempt) { return ['status'=>'pending', 'attemptId'=>$attempt['id'], 'userCode'=>$attempt['userCode'], 'verificationUri'=>$attempt['verificationUri'], 'verificationUriComplete'=>$attempt['verificationUriComplete'] ?? null, 'expiresAt'=>$attempt['expiresAt'] * 1000, 'expiresIn'=>max(0, $attempt['expiresAt'] - time()) * 1000, 'interval'=>$attempt['interval']]; } function picsart_runtime_login($store, $action, $body = []) { if ($action === 'disconnect') { unset($store->data['credentials'], $store->data['device'], $store->data['pendingRefresh']); $store->save(); return ['ok'=>true]; } if ($action === 'cancel') { if (empty($body['attemptId']) || ($store->data['device']['id'] ?? '') === $body['attemptId']) unset($store->data['device']); $store->save(); return ['ok'=>true]; } if ($action === 'start') { if (!empty($store->data['device']['redeemed'])) return picsart_runtime_finish_login($store); $meta = picsart_runtime_discovery(); $r = picsart_runtime_form($meta['device_authorization_endpoint'], ['client_id'=>picsart_runtime_client(), 'scope'=>implode(' ', picsart_runtime_scopes())]); $v = $r['body']; picsart_runtime_require($r['status'] === 200 && is_array($v) && !empty($v['device_code']) && !empty($v['user_code']) && !empty($v['verification_uri']) && is_numeric($v['expires_in'] ?? null) && $v['expires_in'] > 0, 'LOGIN', __('Picsart could not start sign-in. Try again shortly.', 'picsart-commerce'), 502); foreach (['verification_uri','verification_uri_complete'] as $field) { if (empty($v[$field])) continue; $host = wp_parse_url($v[$field], PHP_URL_HOST); picsart_runtime_require(wp_parse_url($v[$field], PHP_URL_SCHEME) === 'https' && ($host === 'picsart.com' || str_ends_with((string)$host, '.picsart.com')), 'LOGIN', __('Picsart returned an unexpected sign-in link.', 'picsart-commerce'), 502); } $interval = max(5, (int)($v['interval'] ?? 5)); $store->data['device'] = ['id'=>wp_generate_uuid4(), 'deviceCode'=>$v['device_code'], 'userCode'=>$v['user_code'], 'verificationUri'=>$v['verification_uri'], 'verificationUriComplete'=>$v['verification_uri_complete'] ?? null, 'expiresAt'=>time() + (int)$v['expires_in'], 'interval'=>$interval, 'nextPollAt'=>time() + $interval]; $store->save(); return picsart_runtime_pending($store->data['device']); } $attempt = $store->data['device'] ?? null; if (!empty($attempt['redeemed'])) return picsart_runtime_finish_login($store); if (!$attempt || $attempt['expiresAt'] <= time()) { unset($store->data['device']); $store->save(); return ['status'=>'expired', 'message'=>__('Get a new code to connect Picsart.', 'picsart-commerce')]; } if (time() < $attempt['nextPollAt']) return picsart_runtime_pending($attempt); $store->data['device']['nextPollAt'] = time() + $attempt['interval']; $store->save(); $r = picsart_runtime_form(picsart_runtime_discovery()['token_endpoint'], ['grant_type'=>'urn:ietf:params:oauth:grant-type:device_code', 'device_code'=>$attempt['deviceCode'], 'client_id'=>picsart_runtime_client()]); if ($r['status'] === 200) { // Save the consumed grant encrypted before JWKS lookup or other fallible validation. $store->data['device']['redeemed'] = $r['body']; $store->data['device']['redeemedAt'] = time(); $store->save(); return picsart_runtime_finish_login($store); } $error = $r['body']['error'] ?? ''; if ($error === 'slow_down') { $store->data['device']['interval'] += 5; $store->data['device']['nextPollAt'] = time() + $store->data['device']['interval']; $store->save(); } if (in_array($error, ['authorization_pending', 'slow_down'], true) || $r['status'] >= 500) return picsart_runtime_pending($store->data['device']); unset($store->data['device']); $store->save(); return ['status'=>$error === 'access_denied' ? 'denied' : 'expired', 'message'=>__('Sign-in did not complete. Get a new code to try again.', 'picsart-commerce')]; } function picsart_runtime_finish_login($store) { $attempt = $store->data['device']; $credentials = picsart_runtime_credentials($attempt['redeemed']); $credentials['expiresAt'] = $attempt['redeemedAt'] + (int)$attempt['redeemed']['expires_in']; picsart_runtime_require(empty($store->data['credentials']) || $store->data['credentials']['subject'] === $credentials['subject'], 'ACCOUNT_CHANGED', __('Disconnect the current Picsart account before connecting a different account.', 'picsart-commerce'), 409); $store->data['credentials'] = $credentials; unset($store->data['device']); $store->save(); return ['status'=>'connected', 'attemptId'=>$attempt['id']]; } function picsart_runtime_token($store) { $c = $store->data['credentials'] ?? null; picsart_runtime_require($c, 'AUTH', __('Connect your Picsart account first.', 'picsart-commerce'), 401); if ($c['expiresAt'] <= time() + 60) { picsart_runtime_require(!empty($c['refreshToken']), 'EXPIRED', __('Reconnect your Picsart account.', 'picsart-commerce'), 401); if (empty($store->data['pendingRefresh'])) { $r = picsart_runtime_form(picsart_runtime_discovery()['token_endpoint'], ['grant_type'=>'refresh_token', 'refresh_token'=>$c['refreshToken'], 'client_id'=>picsart_runtime_client()]); if ($r['status'] === 400 && ($r['body']['error'] ?? '') === 'invalid_grant') { unset($store->data['credentials']); $store->save(); throw new Picsart_Runtime_Error('EXPIRED', esc_html__('Reconnect your Picsart account.', 'picsart-commerce'), 401); } picsart_runtime_require($r['status'] === 200, 'AUTH_UNAVAILABLE', __('Picsart could not refresh the connection. Try again shortly.', 'picsart-commerce'), 502); $store->data['pendingRefresh'] = ['body'=>$r['body'],'at'=>time()]; $store->save(); } $pending = $store->data['pendingRefresh']; $c = picsart_runtime_credentials($pending['body'], $c); $c['expiresAt'] = $pending['at'] + (int)$pending['body']['expires_in']; $store->data['credentials'] = $c; unset($store->data['pendingRefresh']); $store->save(); } return $c; } function picsart_runtime_account($store, $path, $body = null) { picsart_runtime_require(str_starts_with($path, '/') && !str_contains($path, '..'), 'PROVIDER_URL', __('Unexpected Picsart request.', 'picsart-commerce')); $c = picsart_runtime_token($store); $args = ['method'=>$body === null ? 'GET' : 'POST', 'headers'=>['Authorization'=>'Bearer ' . $c['accessToken'], 'Content-Type'=>'application/json', 'X-Platform'=>'web', 'platform'=>'web', 'X-Touchpoint'=>'product-videos']]; if ($body !== null) $args['body'] = wp_json_encode($body); $r = picsart_runtime_http('https://api.picsart.com/gw-v2' . $path, $args); picsart_runtime_require($r['status'] >= 200 && $r['status'] < 300 && ($r['body']['status'] ?? '') === 'success' && array_key_exists('response', $r['body']), 'PICSART_API', __('Picsart could not complete this request. No automatic retry was made.', 'picsart-commerce'), $r['status'] === 401 ? 401 : 502); return $r['body']['response']; }