diff --git a/CHANGES.md b/CHANGES.md
index d19c8490b7..fb46b2b7d1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,8 @@
+# Version 1.3.8
+
+## Bugs
+- Fix audit user internal [#5809](https://github.com/appwrite/appwrite/pull/5809)
+
# Version 1.3.7
## Bugs
diff --git a/README-CN.md b/README-CN.md
index d3f52058b5..b634458547 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -66,7 +66,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
- appwrite/appwrite:1.3.7
+ appwrite/appwrite:1.3.8
```
### Windows
@@ -78,7 +78,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
- appwrite/appwrite:1.3.7
+ appwrite/appwrite:1.3.8
```
#### PowerShell
@@ -88,7 +88,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
- appwrite/appwrite:1.3.7
+ appwrite/appwrite:1.3.8
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
diff --git a/README.md b/README.md
index b8fffd110a..2b82d49eb0 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
- appwrite/appwrite:1.3.7
+ appwrite/appwrite:1.3.8
```
### Windows
@@ -87,7 +87,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
- appwrite/appwrite:1.3.7
+ appwrite/appwrite:1.3.8
```
#### PowerShell
@@ -97,7 +97,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
- appwrite/appwrite:1.3.7
+ appwrite/appwrite:1.3.8
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
@@ -128,6 +128,12 @@ Choose from one of the providers below:
Gitpod
+
+
+
+ Akamai
+
+ |
diff --git a/app/config/providers.php b/app/config/providers.php
index 62430faae1..5e4dc0aa11 100644
--- a/app/config/providers.php
+++ b/app/config/providers.php
@@ -201,6 +201,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false,
],
+ 'oidc' => [
+ 'name' => 'OpenID Connect',
+ 'developers' => 'https://openid.net/connect/faq/',
+ 'icon' => 'icon-oidc',
+ 'enabled' => true,
+ 'sandbox' => false,
+ 'form' => 'oidc.phtml',
+ 'beta' => false,
+ 'mock' => false,
+ ],
'okta' => [
'name' => 'Okta',
'developers' => 'https://developer.okta.com/',
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index 3cd2c7bd70..37485b92d8 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -1427,7 +1427,7 @@ App::get('/v1/account/logs')
$audit = new EventAudit($dbForProject);
- $logs = $audit->getLogsByUser($user->getId(), $limit, $offset);
+ $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset);
$output = [];
diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php
index d16bd57f4c..6bfc08731f 100644
--- a/app/controllers/api/databases.php
+++ b/app/controllers/api/databases.php
@@ -566,7 +566,7 @@ App::get('/v1/databases/:databaseId/logs')
$output[$i] = new Document([
'event' => $log['event'],
- 'userId' => ID::custom($log['userId']),
+ 'userId' => ID::custom($log['data']['userId']),
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
@@ -917,7 +917,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs')
$output[$i] = new Document([
'event' => $log['event'],
- 'userId' => $log['userId'],
+ 'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
@@ -3133,7 +3133,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen
$output[$i] = new Document([
'event' => $log['event'],
- 'userId' => $log['userId'],
+ 'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index cdac9ba2fb..a06ab6b2a0 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -1043,7 +1043,7 @@ App::get('/v1/teams/:teamId/logs')
$output[$i] = new Document([
'event' => $log['event'],
- 'userId' => $log['userId'],
+ 'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php
index cd217afe31..d84d83ff77 100644
--- a/app/controllers/api/users.php
+++ b/app/controllers/api/users.php
@@ -577,7 +577,7 @@ App::get('/v1/users/:userId/logs')
$audit = new Audit($dbForProject);
- $logs = $audit->getLogsByUser($user->getId(), $limit, $offset);
+ $logs = $audit->getLogsByUser($user->getInternalId(), $limit, $offset);
$output = [];
diff --git a/app/init.php b/app/init.php
index fc7541513d..132acb62ce 100644
--- a/app/init.php
+++ b/app/init.php
@@ -101,7 +101,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return
const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 506;
-const APP_VERSION_STABLE = '1.3.7';
+const APP_VERSION_STABLE = '1.3.8';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
diff --git a/app/workers/audits.php b/app/workers/audits.php
index b25430ec41..57614e60f6 100644
--- a/app/workers/audits.php
+++ b/app/workers/audits.php
@@ -40,8 +40,7 @@ class AuditsV1 extends Worker
$dbForProject = $this->getProjectDB($project->getId());
$audit = new Audit($dbForProject);
$audit->log(
- userInternalId: $user->getInternalId(),
- userId: $user->getId(),
+ userId: $user->getInternalId(),
// Pass first, most verbose event pattern
event: $event,
resource: $resource,
@@ -49,6 +48,7 @@ class AuditsV1 extends Worker
ip: $ip,
location: '',
data: [
+ 'userId' => $user->getId(),
'userName' => $userName,
'userEmail' => $userEmail,
'mode' => $mode,
diff --git a/composer.json b/composer.json
index 0ba26540bd..b02a6111bc 100644
--- a/composer.json
+++ b/composer.json
@@ -45,12 +45,12 @@
"appwrite/php-runtimes": "0.11.*",
"utopia-php/abuse": "0.25.*",
"utopia-php/analytics": "0.2.*",
- "utopia-php/audit": "0.26.*",
+ "utopia-php/audit": "0.27.*",
"utopia-php/cache": "0.8.*",
"utopia-php/cli": "0.13.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.36.*",
- "utopia-php/domains": "1.1.*",
+ "utopia-php/domains": "0.3.*",
"utopia-php/framework": "0.28.*",
"utopia-php/image": "0.5.*",
"utopia-php/locale": "0.4.*",
diff --git a/composer.lock b/composer.lock
index 0c6b961ea2..5e6640b9fb 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "3eadbfe5543aafdf8682ea0465159e3c",
+ "content-hash": "a15f62920dedaf99e321ffd94e89d9f4",
"packages": [
{
"name": "adhocore/jwt",
@@ -1906,16 +1906,16 @@
},
{
"name": "utopia-php/audit",
- "version": "0.26.0",
+ "version": "0.27.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
- "reference": "e7228080f14df28737fbb050c180c26d86ac0403"
+ "reference": "bdf89d7fe381bd4c891ad217612580a35e8c7642"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/audit/zipball/e7228080f14df28737fbb050c180c26d86ac0403",
- "reference": "e7228080f14df28737fbb050c180c26d86ac0403",
+ "url": "https://api.github.com/repos/utopia-php/audit/zipball/bdf89d7fe381bd4c891ad217612580a35e8c7642",
+ "reference": "bdf89d7fe381bd4c891ad217612580a35e8c7642",
"shasum": ""
},
"require": {
@@ -1947,9 +1947,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
- "source": "https://github.com/utopia-php/audit/tree/0.26.0"
+ "source": "https://github.com/utopia-php/audit/tree/0.27.0"
},
- "time": "2023-04-27T15:43:50+00:00"
+ "time": "2023-05-15T07:04:48+00:00"
},
{
"name": "utopia-php/cache",
@@ -2164,23 +2164,25 @@
},
{
"name": "utopia-php/domains",
- "version": "v1.1.0",
+ "version": "0.3.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
- "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73"
+ "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/domains/zipball/1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73",
- "reference": "1665e1d9932afa3be63b5c1e0dcfe01fe77d8e73",
+ "url": "https://api.github.com/repos/utopia-php/domains/zipball/aaa8c9a96c69ccb397997b1f4f2299c66f77eefb",
+ "reference": "aaa8c9a96c69ccb397997b1f4f2299c66f77eefb",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=8.0",
+ "utopia-php/framework": "0.*.*"
},
"require-dev": {
- "phpunit/phpunit": "^7.0"
+ "laravel/pint": "1.2.*",
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"autoload": {
@@ -2196,6 +2198,10 @@
{
"name": "Eldad Fux",
"email": "eldad@appwrite.io"
+ },
+ {
+ "name": "Wess Cope",
+ "email": "wess@appwrite.io"
}
],
"description": "Utopia Domains library is simple and lite library for parsing web domains. This library is aiming to be as simple and easy to learn and use.",
@@ -2212,9 +2218,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
- "source": "https://github.com/utopia-php/domains/tree/master"
+ "source": "https://github.com/utopia-php/domains/tree/0.3.2"
},
- "time": "2020-02-23T07:40:02+00:00"
+ "time": "2023-07-19T16:39:24+00:00"
},
{
"name": "utopia-php/framework",
@@ -3031,16 +3037,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
- "version": "0.33.6",
+ "version": "0.33.7",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
- "reference": "237fe97b68090a244382c36f96482c352880a38c"
+ "reference": "9f5db4a637b23879ceacea9ed2d33b0486771ffc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/237fe97b68090a244382c36f96482c352880a38c",
- "reference": "237fe97b68090a244382c36f96482c352880a38c",
+ "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9f5db4a637b23879ceacea9ed2d33b0486771ffc",
+ "reference": "9f5db4a637b23879ceacea9ed2d33b0486771ffc",
"shasum": ""
},
"require": {
@@ -3076,9 +3082,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
- "source": "https://github.com/appwrite/sdk-generator/tree/0.33.6"
+ "source": "https://github.com/appwrite/sdk-generator/tree/0.33.7"
},
- "time": "2023-07-10T16:27:53+00:00"
+ "time": "2023-07-12T12:15:43+00:00"
},
{
"name": "doctrine/deprecations",
diff --git a/public/images/integrations/akamai-logo.svg b/public/images/integrations/akamai-logo.svg
new file mode 100644
index 0000000000..cdcf31b4c2
--- /dev/null
+++ b/public/images/integrations/akamai-logo.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/Appwrite/Auth/OAuth2/Oidc.php b/src/Appwrite/Auth/OAuth2/Oidc.php
new file mode 100644
index 0000000000..de2eab65c8
--- /dev/null
+++ b/src/Appwrite/Auth/OAuth2/Oidc.php
@@ -0,0 +1,296 @@
+getAuthorizationEndpoint() . '?' . \http_build_query([
+ 'client_id' => $this->appID,
+ 'redirect_uri' => $this->callback,
+ 'state' => \json_encode($this->state),
+ 'scope' => \implode(' ', $this->getScopes()),
+ 'response_type' => 'code',
+ ]);
+ }
+
+ /**
+ * @param string $code
+ *
+ * @return array
+ */
+ protected function getTokens(string $code): array
+ {
+ if (empty($this->tokens)) {
+ $headers = ['Content-Type: application/x-www-form-urlencoded'];
+ $this->tokens = \json_decode($this->request(
+ 'POST',
+ $this->getTokenEndpoint(),
+ $headers,
+ \http_build_query([
+ 'code' => $code,
+ 'client_id' => $this->appID,
+ 'client_secret' => $this->getClientSecret(),
+ 'redirect_uri' => $this->callback,
+ 'scope' => \implode(' ', $this->getScopes()),
+ 'grant_type' => 'authorization_code'
+ ])
+ ), true);
+ }
+ return $this->tokens;
+ }
+
+
+ /**
+ * @param string $refreshToken
+ *
+ * @return array
+ */
+ public function refreshTokens(string $refreshToken): array
+ {
+ $headers = ['Content-Type: application/x-www-form-urlencoded'];
+ $this->tokens = \json_decode($this->request(
+ 'POST',
+ $this->getTokenEndpoint(),
+ $headers,
+ \http_build_query([
+ 'refresh_token' => $refreshToken,
+ 'client_id' => $this->appID,
+ 'client_secret' => $this->getClientSecret(),
+ 'grant_type' => 'refresh_token'
+ ])
+ ), true);
+
+ if (empty($this->tokens['refresh_token'])) {
+ $this->tokens['refresh_token'] = $refreshToken;
+ }
+
+ return $this->tokens;
+ }
+
+ /**
+ * @param string $accessToken
+ *
+ * @return string
+ */
+ public function getUserID(string $accessToken): string
+ {
+ $user = $this->getUser($accessToken);
+
+ if (isset($user['sub'])) {
+ return $user['sub'];
+ }
+
+ return '';
+ }
+
+ /**
+ * @param string $accessToken
+ *
+ * @return string
+ */
+ public function getUserEmail(string $accessToken): string
+ {
+ $user = $this->getUser($accessToken);
+
+ if (isset($user['email'])) {
+ return $user['email'];
+ }
+
+ return '';
+ }
+
+ /**
+ * Check if the User email is verified
+ *
+ * @param string $accessToken
+ *
+ * @return bool
+ */
+ public function isEmailVerified(string $accessToken): bool
+ {
+ $user = $this->getUser($accessToken);
+
+ return $user['email_verified'] ?? false;
+ }
+
+ /**
+ * @param string $accessToken
+ *
+ * @return string
+ */
+ public function getUserName(string $accessToken): string
+ {
+ $user = $this->getUser($accessToken);
+
+ if (isset($user['name'])) {
+ return $user['name'];
+ }
+
+ return '';
+ }
+
+ /**
+ * @param string $accessToken
+ *
+ * @return array
+ */
+ protected function getUser(string $accessToken): array
+ {
+ if (empty($this->user)) {
+ $headers = ['Authorization: Bearer ' . \urlencode($accessToken)];
+ $user = $this->request('GET', $this->getUserinfoEndpoint(), $headers);
+ $this->user = \json_decode($user, true);
+ }
+
+ return $this->user;
+ }
+
+ /**
+ * Extracts the Client Secret from the JSON stored in appSecret
+ *
+ * @return string
+ */
+ protected function getClientSecret(): string
+ {
+ $secret = $this->getAppSecret();
+
+ return $secret['clientSecret'] ?? '';
+ }
+
+ /**
+ * Extracts the well known endpoint from the JSON stored in appSecret.
+ *
+ * @return string
+ */
+ protected function getWellKnownEndpoint(): string
+ {
+ $secret = $this->getAppSecret();
+ return $secret['wellKnownEndpoint'] ?? '';
+ }
+
+ /**
+ * Extracts the authorization endpoint from the JSON stored in appSecret.
+ *
+ * If one is not provided, it will be retrieved from the well-known configuration.
+ *
+ * @return string
+ */
+ protected function getAuthorizationEndpoint(): string
+ {
+ $secret = $this->getAppSecret();
+
+ $endpoint = $secret['authorizationEndpoint'] ?? '';
+ if (!empty($endpoint)) {
+ return $endpoint;
+ }
+
+ $wellKnownConfiguration = $this->getWellKnownConfiguration();
+ return $wellKnownConfiguration['authorization_endpoint'] ?? '';
+ }
+
+ /**
+ * Extracts the token endpoint from the JSON stored in appSecret.
+ *
+ * If one is not provided, it will be retrieved from the well-known configuration.
+ *
+ * @return string
+ */
+ protected function getTokenEndpoint(): string
+ {
+ $secret = $this->getAppSecret();
+
+ $endpoint = $secret['tokenEndpoint'] ?? '';
+ if (!empty($endpoint)) {
+ return $endpoint;
+ }
+
+ $wellKnownConfiguration = $this->getWellKnownConfiguration();
+ return $wellKnownConfiguration['token_endpoint'] ?? '';
+ }
+
+ /**
+ * Extracts the userinfo endpoint from the JSON stored in appSecret.
+ *
+ * If one is not provided, it will be retrieved from the well-known configuration.
+ *
+ * @return string
+ */
+ protected function getUserinfoEndpoint(): string
+ {
+ $secret = $this->getAppSecret();
+ $endpoint = $secret['userinfoEndpoint'] ?? '';
+ if (!empty($endpoint)) {
+ return $endpoint;
+ }
+
+ $wellKnownConfiguration = $this->getWellKnownConfiguration();
+ return $wellKnownConfiguration['userinfo_endpoint'] ?? '';
+ }
+
+ /**
+ * Get the well-known configuration using the well known endpoint
+ */
+ protected function getWellKnownConfiguration(): array
+ {
+ if (empty($this->wellKnownConfiguration)) {
+ $response = $this->request('GET', $this->getWellKnownEndpoint());
+ $this->wellKnownConfiguration = \json_decode($response, true);
+ }
+
+ return $this->wellKnownConfiguration;
+ }
+
+ /**
+ * Decode the JSON stored in appSecret
+ *
+ * @return array
+ */
+ protected function getAppSecret(): array
+ {
+ try {
+ $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
+ } catch (\Throwable $th) {
+ throw new \Exception('Invalid secret');
+ }
+ return $secret;
+ }
+}
diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php
index 7a1f3cad24..ffb5ce6ac4 100644
--- a/src/Appwrite/Migration/Migration.php
+++ b/src/Appwrite/Migration/Migration.php
@@ -63,6 +63,7 @@ abstract class Migration
'1.3.5' => 'V18',
'1.3.6' => 'V18',
'1.3.7' => 'V18',
+ '1.3.8' => 'V18',
];
/**
diff --git a/src/Appwrite/Migration/Version/V18.php b/src/Appwrite/Migration/Version/V18.php
index be628e0fbb..839269f940 100644
--- a/src/Appwrite/Migration/Version/V18.php
+++ b/src/Appwrite/Migration/Version/V18.php
@@ -133,6 +133,16 @@ class V18 extends Migration
Console::warning("'options' from {$id}: {$th->getMessage()}");
}
break;
+ case 'audit':
+ try {
+ /**
+ * Delete 'userInternalId' attribute
+ */
+ $this->projectDB->deleteAttribute($id, 'userInternalId');
+ } catch (\Throwable $th) {
+ Console::warning("'userInternalId' from {$id}: {$th->getMessage()}");
+ }
+ break;
default:
break;
}
@@ -195,6 +205,34 @@ class V18 extends Migration
Console::warning($th->getMessage());
}
break;
+ case 'audit':
+ /**
+ * Set the userId to the userInternalId and add userId to data
+ */
+ try {
+ $userId = $document->getAttribute('userId');
+ $data = $document->getAttribute('data', []);
+ $mode = $data['mode'] ?? 'default';
+ $user = match ($mode) {
+ 'admin' => $this->consoleDB->getDocument('users', $userId),
+ default => $this->projectDB->getDocument('users', $userId),
+ };
+
+ if ($user->isEmpty()) {
+ // The audit userId could already be an internal Id.
+ // Otherwise, the user could have been deleted.
+ // Nonetheless, there's nothing else we can do here.
+ break;
+ }
+ $internalId = $user->getInternalId();
+ $document->setAttribute('userId', $internalId);
+ $data = $document->getAttribute('data', []);
+ $data['userId'] = $user->getId();
+ $document->setAttribute('data', $data);
+ } catch (\Throwable $th) {
+ Console::warning($th->getMessage());
+ }
+ break;
}
return $document;
diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php
index b08686fa48..f6d10653d8 100644
--- a/tests/e2e/Services/Account/AccountBase.php
+++ b/tests/e2e/Services/Account/AccountBase.php
@@ -1296,7 +1296,7 @@ trait AccountBase
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
- $expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::format(new \DateTime($response['body']['expire']))), 0);
+ $expireTime = strpos($lastEmail['text'], 'expire=' . urlencode($response['body']['expire']), 0);
$this->assertNotFalse($expireTime);