1
0
Fork 0
mirror of synced 2024-06-14 00:34:51 +12:00

Merge branch '1.5.x' into feat-migrations-service-fixes

This commit is contained in:
Bradley Schofield 2024-02-24 14:20:31 +00:00
commit d13cb6f949
25 changed files with 318 additions and 146 deletions

View file

@ -141,7 +141,7 @@ jobs:
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
sleep 10
sleep 25
- name: Run ${{matrix.service}} Tests
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug

2
.gitmodules vendored
View file

@ -1,4 +1,4 @@
[submodule "app/console"]
path = app/console
url = https://github.com/appwrite/console
branch = chore-update-sdk
branch = 1.5.x

View file

@ -29,7 +29,7 @@ ENV VITE_APPWRITE_GROWTH_ENDPOINT=$VITE_APPWRITE_GROWTH_ENDPOINT
RUN npm ci
RUN npm run build
FROM appwrite/base:0.4.4 as final
FROM appwrite/base:0.9.0 as final
LABEL maintainer="team@appwrite.io"

View file

@ -6,10 +6,12 @@ return [
'magicSession',
'recovery',
'invitation',
'mfaChallenge'
],
'sms' => [
'verification',
'login',
'invitation'
'invitation',
'mfaChallenge'
]
];

View file

@ -1,4 +1,4 @@
<p>{{hello}}</p>
<p>{{hello}},</p>
<p>{{optionButton}}</p>

View file

@ -0,0 +1,16 @@
<p>{{hello}},</p>
<p>{{description}}</p>
<table border="0" cellspacing="0" cellpadding="0" style="padding-top: 10px; padding-bottom: 10px; display: inline-block;">
<tr>
<td align="center" style="border-radius: 8px; background-color: #ffffff;">
<p style="font-size: 24px; text-indent: 18px; letter-spacing: 18px; font-family: Inter; color: #414146; text-decoration: none; border-radius: 8px; padding: 24px 12px; border: 1px solid #EDEDF0; display: inline-block; font-weight: bold; ">{{otp}}</p>
</td>
</tr>
</table>
<p>{{clientInfo}}</p>
<p style="margin-bottom: 0px;">{{thanks}}</p>
<p style="margin-top: 0px;">{{signature}}</p>

View file

@ -1,4 +1,4 @@
<p>{{hello}}</p>
<p>{{hello}},</p>
<p>{{description}}</p>

View file

@ -4,36 +4,42 @@
"settings.direction": "ltr",
"emails.sender": "%s Team",
"emails.verification.subject": "Account Verification",
"emails.verification.hello": "Hey {{user}}",
"emails.verification.body": "Follow this link to verify your email address.",
"emails.verification.hello": "Hello {{user}}",
"emails.verification.body": "Follow this link to verify your email address to your {{b}}{{project}}{{/b}} account.",
"emails.verification.footer": "If you didnt ask to verify this address, you can ignore this message.",
"emails.verification.thanks": "Thanks",
"emails.verification.signature": "{{project}} team",
"emails.magicSession.subject": "{{project}} Login",
"emails.magicSession.hello": "Hello,",
"emails.magicSession.optionButton": "Click the button below to securely sign in to your {{project}} account. This link will expire in 1 hour.",
"emails.magicSession.hello": "Hello {{user}}",
"emails.magicSession.optionButton": "Click the button below to securely sign in to your {{b}}{{project}}{{/b}} account. This link will expire in 1 hour.",
"emails.magicSession.buttonText": "Sign in to {{project}}",
"emails.magicSession.optionUrl": "If you are unable to sign in using the button above, please visit the following link:",
"emails.magicSession.clientInfo": "This sign in was requested using {{agentClient}} on {{agentDevice}} {{agentOs}}. If you didn't request the sign in, you can safely ignore this email.",
"emails.magicSession.securityPhrase": "Security phrase for this email is {{phrase}}. You can trust this email if this phrase matches the phrase shown during sign in.",
"emails.magicSession.clientInfo": "This sign in was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the sign in, you can safely ignore this email.",
"emails.magicSession.securityPhrase": "Security phrase for this email is {{b}}{{phrase}}{{/b}}. You can trust this email if this phrase matches the phrase shown during sign in.",
"emails.magicSession.thanks": "Thanks,",
"emails.magicSession.signature": "{{project}} team",
"emails.otpSession.subject": "OTP for {{project}} Login",
"emails.otpSession.hello": "Hello,",
"emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{project}} account. This code will expire in 15 minutes.",
"emails.otpSession.clientInfo": "This sign in was requested using {{agentClient}} on {{agentDevice}} {{agentOs}}. If you didn't request the sign in, you can safely ignore this email.",
"emails.otpSession.securityPhrase": "Security phrase for this email is {{phrase}}. You can trust this email if this phrase matches the phrase shown during sign in.",
"emails.otpSession.hello": "Hello {{user}}",
"emails.otpSession.description": "Enter the following verification code when prompted to securely sign in to your {{b}}{{project}}{{/b}} account. This code will expire in 15 minutes.",
"emails.otpSession.clientInfo": "This sign in was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the sign in, you can safely ignore this email.",
"emails.otpSession.securityPhrase": "Security phrase for this email is {{b}}{{phrase}}{{/b}}. You can trust this email if this phrase matches the phrase shown during sign in.",
"emails.otpSession.thanks": "Thanks,",
"emails.otpSession.signature": "{{project}} team",
"emails.mfaChallenge.subject": "Verification Code for {{project}}",
"emails.mfaChallenge.hello": "Hello {{user}}",
"emails.mfaChallenge.description": "Enter the following verification code to verify your email and activate two-step verification in {{b}}{{project}}{{/b}}. This code will expire in 15 minutes.",
"emails.mfaChallenge.clientInfo": "This verification code was requested using {{b}}{{agentClient}}{{/b}} on {{b}}{{agentDevice}}{{/b}} {{b}}{{agentOs}}{{/b}}. If you didn't request the verification code, you can safely ignore this email.",
"emails.mfaChallenge.thanks": "Thanks,",
"emails.mfaChallenge.signature": "{{project}} team",
"emails.recovery.subject": "Password Reset",
"emails.recovery.hello": "Hello {{user}}",
"emails.recovery.body": "Follow this link to reset your {{project}} password.",
"emails.recovery.body": "Follow this link to reset your {{b}}{{project}}{{/b}} password.",
"emails.recovery.footer": "If you didnt ask to reset your password, you can ignore this message.",
"emails.recovery.thanks": "Thanks",
"emails.recovery.signature": "{{project}} team",
"emails.invitation.subject": "Invitation to %s Team at %s",
"emails.invitation.hello": "Hello",
"emails.invitation.body": "This mail was sent to you because {{owner}} wanted to invite you to become a member of the {{team}} team at {{project}}.",
"emails.invitation.hello": "Hello {{user}}",
"emails.invitation.body": "This mail was sent to you because {{b}}{{owner}}{{/b}} wanted to invite you to become a member of the {{b}}{{team}}{{/b}} team at {{b}}{{project}}{{/b}}.",
"emails.invitation.footer": "If you are not interested, you can ignore this message.",
"emails.invitation.thanks": "Thanks",
"emails.invitation.signature": "{{project}} team",

@ -1 +1 @@
Subproject commit 44edd461c6036cb462047c1424b80f0903cdc15e
Subproject commit c72ba12e479b0d3d9b3f4e48e01625c12a38fd7f

View file

@ -1259,15 +1259,16 @@ App::post('/v1/account/tokens/magic-url')
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
'user' => '',
'team' => '',
// {{user}}, {{redirect}} and {{project}} are required in default and custom templates
'user' => $user->getAttribute('name'),
'project' => $project->getAttribute('name'),
'redirect' => $url,
'agentDevice' => '<strong>' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '</strong>',
'agentClient' => '<strong>' . ($agentClient['clientName'] ?? 'UNKNOWN') . '</strong>',
'agentOs' => '<strong>' . ($agentOs['osName'] ?? 'UNKNOWN') . '</strong>',
'phrase' => '<strong>' . (!empty($phrase) ? $phrase : '') . '</strong>'
'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN',
'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN',
'agentOs' => $agentOs['osName'] ?? 'UNKNOWN',
'phrase' => !empty($phrase) ? $phrase : '',
// TODO: remove unnecessary team variable from this email
'team' => '',
];
$queueForMails
@ -1487,15 +1488,16 @@ App::post('/v1/account/tokens/email')
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
/* {{user}} ,{{team}}, {{project}} and {{otp}} are required in the templates */
'user' => '',
'team' => '',
// {{user}}, {{project}} and {{otp}} are required in the templates
'user' => $user->getAttribute('name'),
'project' => $project->getAttribute('name'),
'otp' => $tokenSecret,
'agentDevice' => '<strong>' . ( $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN') . '</strong>',
'agentClient' => '<strong>' . ($agentClient['clientName'] ?? 'UNKNOWN') . '</strong>',
'agentOs' => '<strong>' . ($agentOs['osName'] ?? 'UNKNOWN') . '</strong>',
'phrase' => '<strong>' . (!empty($phrase) ? $phrase : '') . '</strong>'
'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN',
'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN',
'agentOs' => $agentOs['osName'] ?? 'UNKNOWN',
'phrase' => !empty($phrase) ? $phrase : '',
// TODO: remove unnecessary team variable from this email
'team' => '',
];
$queueForMails
@ -2953,11 +2955,12 @@ App::post('/v1/account/recovery')
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
// {{user}}, {{redirect}} and {{project}} are required in default and custom templates
'user' => $profile->getAttribute('name'),
'team' => '',
'redirect' => $url,
'project' => $projectName
'project' => $projectName,
// TODO: remove unnecessary team variable from this email
'team' => ''
];
$queueForMails
@ -3200,11 +3203,12 @@ App::post('/v1/account/verification')
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
/* {{user}}, {{team}}, {{redirect}} and {{project}} are required in default and custom templates */
// {{user}}, {{redirect}} and {{project}} are required in default and custom templates
'user' => $user->getAttribute('name'),
'team' => '',
'redirect' => $url,
'project' => $projectName
'project' => $projectName,
// TODO: remove unnecessary team variable from this email
'team' => '',
];
$queueForMails
@ -3696,7 +3700,7 @@ App::post('/v1/account/mfa/challenge')
->label('audits.userId', '{response.userId}')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'create2FAChallenge')
->label('sdk.method', 'createChallenge')
->label('sdk.description', '/docs/references/account/create-2fa-challenge.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
@ -3707,11 +3711,13 @@ App::post('/v1/account/mfa/challenge')
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('locale')
->inject('project')
->inject('request')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForMails')
->inject('locale')
->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails, Locale $locale) {
->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Locale $locale, Document $project, Request $request, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails) {
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$code = Auth::codeGenerator();
@ -3743,6 +3749,22 @@ App::post('/v1/account/mfa/challenge')
throw new Exception(Exception::USER_PHONE_NOT_VERIFIED);
}
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.mfaChallenge-' . $locale->default] ?? [];
if (!empty($customTemplate)) {
$message = $customTemplate['message'] ?? $message;
}
$messageContent = Template::fromString($locale->getText("sms.verification.body"));
$messageContent
->setParam('{{project}}', $project->getAttribute('name'))
->setParam('{{secret}}', $code);
$messageContent = \strip_tags($messageContent->render());
$message = $message->setParam('{{token}}', $messageContent);
$message = $message->render();
$queueForMessaging
->setType(MESSAGE_SEND_TYPE_INTERNAL)
->setMessage(new Document([
@ -3751,7 +3773,8 @@ App::post('/v1/account/mfa/challenge')
'content' => $code,
],
]))
->setRecipients([$user->getAttribute('phone')]);
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS);
break;
case 'email':
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
@ -3764,9 +3787,85 @@ App::post('/v1/account/mfa/challenge')
throw new Exception(Exception::USER_EMAIL_NOT_VERIFIED);
}
$subject = $locale->getText("emails.mfaChallenge.subject");
$customTemplate = $project->getAttribute('templates', [])['email.mfaChallenge-' . $locale->default] ?? [];
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$agentOs = $detector->getOS();
$agentClient = $detector->getClient();
$agentDevice = $detector->getDevice();
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/email-mfa-challenge.tpl');
$message
->setParam('{{hello}}', $locale->getText("emails.mfaChallenge.hello"))
->setParam('{{description}}', $locale->getText("emails.mfaChallenge.description"))
->setParam('{{clientInfo}}', $locale->getText("emails.mfaChallenge.clientInfo"))
->setParam('{{thanks}}', $locale->getText("emails.mfaChallenge.thanks"))
->setParam('{{signature}}', $locale->getText("emails.mfaChallenge.signature"));
$body = $message->render();
$smtp = $project->getAttribute('smtp', []);
$smtpEnabled = $smtp['enabled'] ?? false;
$senderEmail = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$senderName = App::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyTo = "";
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
$senderEmail = $smtp['senderEmail'];
}
if (!empty($smtp['senderName'])) {
$senderName = $smtp['senderName'];
}
if (!empty($smtp['replyTo'])) {
$replyTo = $smtp['replyTo'];
}
$queueForMails
->setSmtpHost($smtp['host'] ?? '')
->setSmtpPort($smtp['port'] ?? '')
->setSmtpUsername($smtp['username'] ?? '')
->setSmtpPassword($smtp['password'] ?? '')
->setSmtpSecure($smtp['secure'] ?? '');
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
}
if (!empty($customTemplate['senderName'])) {
$senderName = $customTemplate['senderName'];
}
if (!empty($customTemplate['replyTo'])) {
$replyTo = $customTemplate['replyTo'];
}
$body = $customTemplate['message'] ?? '';
$subject = $customTemplate['subject'] ?? $subject;
}
$queueForMails
->setSmtpReplyTo($replyTo)
->setSmtpSenderEmail($senderEmail)
->setSmtpSenderName($senderName);
}
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
// {{user}}, {{project}} and {{otp}} are required in the templates
'user' => $user->getAttribute('name'),
'project' => $project->getAttribute('name'),
'otp' => $code,
'agentDevice' => $agentDevice['deviceBrand'] ?? $agentDevice['deviceBrand'] ?? 'UNKNOWN',
'agentClient' => $agentClient['clientName'] ?? 'UNKNOWN',
'agentOs' => $agentOs['osName'] ?? 'UNKNOWN'
];
$queueForMails
->setSubject("{$code} is your 6-digit code")
->setBody($code)
->setSubject($subject)
->setBody($body)
->setVariables($emailVariables)
->setRecipient($user->getAttribute('email'))
->trigger();
break;
@ -3841,6 +3940,9 @@ App::put('/v1/account/mfa/challenge')
$dbForProject->updateDocument('sessions', $sessionId, $session->setAttribute('factors', $provider, Document::SET_TYPE_APPEND));
$queueForEvents
->setParam('userId', $user->getId());
$response->dynamic($session, Response::MODEL_SESSION);
});

View file

@ -766,11 +766,12 @@ App::post('/v1/messaging/providers/apns')
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', false, new Boolean(), 'Use APNS sandbox environment.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, bool $sandbox, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
$credentials = [];
@ -803,6 +804,10 @@ App::post('/v1/messaging/providers/apns')
$enabled = false;
}
$options = [
'sandbox' => $sandbox
];
$provider = new Document([
'$id' => $providerId,
'name' => $name,
@ -810,6 +815,7 @@ App::post('/v1/messaging/providers/apns')
'type' => MESSAGE_TYPE_PUSH,
'enabled' => $enabled,
'credentials' => $credentials,
'options' => $options
]);
try {
@ -1808,10 +1814,11 @@ App::patch('/v1/messaging/providers/apns/:providerId')
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
->param('teamId', '', new Text(0), 'APNS team ID.', true)
->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
->param('sandbox', null, new Boolean(), 'Use APNS sandbox environment.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, Event $queueForEvents, Database $dbForProject, Response $response) {
->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, ?bool $sandbox, Event $queueForEvents, Database $dbForProject, Response $response) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
@ -1847,6 +1854,14 @@ App::patch('/v1/messaging/providers/apns/:providerId')
$provider->setAttribute('credentials', $credentials);
$options = $provider->getAttribute('options');
if (!\is_null($sandbox)) {
$options['sandbox'] = $sandbox;
}
$provider->setAttribute('options', $options);
if (!\is_null($enabled)) {
if ($enabled) {
if (
@ -2133,21 +2148,26 @@ App::patch('/v1/messaging/topics/:topicId')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOPIC)
->param('topicId', '', new UID(), 'Topic ID.')
->param('name', '', new Text(128), 'Topic Name.', true)
->param('name', null, new Text(128), 'Topic Name.', true)
->param('subscribe', null, new Roles(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of role strings with subscribe permission. By default all users are granted with any subscribe permission. [learn more about roles](https://appwrite.io/docs/permissions#permission-roles). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 64 characters long.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $topicId, string $name, Event $queueForEvents, Database $dbForProject, Response $response) {
->action(function (string $topicId, ?string $name, ?array $subscribe, Event $queueForEvents, Database $dbForProject, Response $response) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
throw new Exception(Exception::TOPIC_NOT_FOUND);
}
if (!empty($name)) {
if (!\is_null($name)) {
$topic->setAttribute('name', $name);
}
if (!\is_null($subscribe)) {
$topic->setAttribute('subscribe', $subscribe);
}
$topic = $dbForProject->updateDocument('topics', $topicId, $topic);
$queueForEvents

View file

@ -513,7 +513,7 @@ App::post('/v1/users/:userId/targets')
Permission::delete(Role::user($user->getId())),
],
'providerId' => $providerId ?? null,
'providerInternalId' => $provider->getInternalId() ?? null,
'providerInternalId' => $provider->isEmpty() ? null : $provider->getInternalId(),
'providerType' => $providerType,
'userId' => $userId,
'userInternalId' => $user->getInternalId(),
@ -1662,7 +1662,7 @@ App::post('/v1/users/:userId/sessions')
->inject('queueForEvents')
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user === false || $user->isEmpty()) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
@ -1731,7 +1731,7 @@ App::post('/v1/users/:userId/tokens')
->action(function (string $userId, int $length, int $expire, Request $request, Response $response, Database $dbForProject, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user === false || $user->isEmpty()) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}

View file

@ -50,7 +50,7 @@
"utopia-php/cache": "0.9.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.48.2",
"utopia-php/database": "0.48.*",
"utopia-php/domains": "0.5.*",
"utopia-php/dsn": "0.2.*",
"utopia-php/framework": "0.33.*",

62
composer.lock generated
View file

@ -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": "9e0e07159d27e4b86511aaab851532de",
"content-hash": "f19c09e7e233fe0f767bf5255fb46b86",
"packages": [
{
"name": "adhocore/jwt",
@ -65,16 +65,16 @@
},
{
"name": "appwrite/appwrite",
"version": "10.1.0",
"version": "10.0.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "da579af70723cfc117b5af84375bdef117e27312"
"reference": "461eedf4efd502dc905c3055f36f0e3583f67390"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/da579af70723cfc117b5af84375bdef117e27312",
"reference": "da579af70723cfc117b5af84375bdef117e27312",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/461eedf4efd502dc905c3055f36f0e3583f67390",
"reference": "461eedf4efd502dc905c3055f36f0e3583f67390",
"shasum": ""
},
"require": {
@ -99,10 +99,10 @@
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/10.1.0",
"source": "https://github.com/appwrite/sdk-for-php/tree/10.0.0",
"url": "https://appwrite.io/support"
},
"time": "2023-11-20T09:56:12+00:00"
"time": "2023-09-07T23:28:31+00:00"
},
{
"name": "appwrite/php-clamav",
@ -1552,16 +1552,16 @@
},
{
"name": "utopia-php/database",
"version": "0.48.2",
"version": "0.48.4",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4"
"reference": "02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4",
"reference": "0a231a2874fdbc0cf2ae2170b3f132fdee0ddfd4",
"url": "https://api.github.com/repos/utopia-php/database/zipball/02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0",
"reference": "02f20bd901b8fab26d7dc2c58f7da1d6a08d21c0",
"shasum": ""
},
"require": {
@ -1569,7 +1569,7 @@
"ext-pdo": "*",
"php": ">=8.0",
"utopia-php/cache": "0.9.*",
"utopia-php/framework": "0.*.*",
"utopia-php/framework": "0.33.*",
"utopia-php/mongo": "0.3.*"
},
"require-dev": {
@ -1602,9 +1602,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.48.2"
"source": "https://github.com/utopia-php/database/tree/0.48.4"
},
"time": "2024-02-02T14:10:14+00:00"
"time": "2024-02-23T03:22:55+00:00"
},
{
"name": "utopia-php/domains",
@ -1962,20 +1962,20 @@
},
{
"name": "utopia-php/migration",
"version": "dev-dev",
"version": "0.3.6",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "0847fad35006c16f2aa572c4fa890cc8a0e7f8f2"
"reference": "f78273b38bade23db5866e5c7cb5f55427ba82af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/0847fad35006c16f2aa572c4fa890cc8a0e7f8f2",
"reference": "0847fad35006c16f2aa572c4fa890cc8a0e7f8f2",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/f78273b38bade23db5866e5c7cb5f55427ba82af",
"reference": "f78273b38bade23db5866e5c7cb5f55427ba82af",
"shasum": ""
},
"require": {
"appwrite/appwrite": "10.1.0",
"appwrite/appwrite": "10.0.*",
"php": "8.*",
"utopia-php/cli": "0.*"
},
@ -1990,19 +1990,7 @@
"Utopia\\Migration\\": "src/Migration"
}
},
"autoload-dev": {
"psr-4": {
"Utopia\\Tests\\": "tests/Migration"
}
},
"scripts": {
"lint": [
"./vendor/bin/pint --test"
],
"format": [
"./vendor/bin/pint"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
@ -2015,10 +2003,10 @@
"utopia"
],
"support": {
"source": "https://github.com/utopia-php/migration/tree/dev",
"issues": "https://github.com/utopia-php/migration/issues"
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/0.3.6"
},
"time": "2024-02-23T12:02:32+00:00"
"time": "2023-11-02T15:13:03+00:00"
},
{
"name": "utopia-php/mongo",
@ -5461,9 +5449,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"utopia-php/migration": 20
},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {

View file

@ -30,31 +30,34 @@ class V20 extends Migration
foreach (['subQueryIndexes', 'subQueryPlatforms', 'subQueryDomains', 'subQueryKeys', 'subQueryWebhooks', 'subQuerySessions', 'subQueryTokens', 'subQueryMemberships', 'subQueryVariables', 'subQueryChallenges', 'subQueryProjectVariables', 'subQueryTargets', 'subQueryTopicTargets'] as $name) {
Database::addFilter(
$name,
fn() => null,
fn() => []
fn () => null,
fn () => []
);
}
$this->migrateUsageMetrics('project.$all.network.requests', 'network.requests');
$this->migrateUsageMetrics('project.$all.network.outbound', 'network.outbound');
$this->migrateUsageMetrics('project.$all.network.inbound', 'network.inbound');
$this->migrateUsageMetrics('users.$all.count.total', 'users');
$this->migrateSessionsMetric();
Console::log('Migrating Project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->projectDB->setNamespace("_{$this->project->getInternalId()}");
Console::info('Migrating Collections');
$this->migrateCollections();
Console::info('Migrating Functions');
$this->migrateFunctions();
// No need to migrate stats for console
if ($this->project->getInternalId() !== 'console') {
$this->migrateUsageMetrics('project.$all.network.requests', 'network.requests');
$this->migrateUsageMetrics('project.$all.network.outbound', 'network.outbound');
$this->migrateUsageMetrics('project.$all.network.inbound', 'network.inbound');
$this->migrateUsageMetrics('users.$all.count.total', 'users');
$this->migrateSessionsMetric();
Console::info('Migrating Databases');
$this->migrateDatabases();
Console::info('Migrating Functions');
$this->migrateFunctions();
Console::info('Migrating Buckets');
$this->migrateBuckets();
Console::info('Migrating Databases');
$this->migrateDatabases();
Console::info('Migrating Buckets');
$this->migrateBuckets();
}
Console::info('Migrating Documents');
$this->forEachDocument([$this, 'fixDocument']);
@ -75,25 +78,27 @@ class V20 extends Migration
};
// Support database array type migration (user collections)
foreach (
$this->documentsIterator('attributes', [
Query::equal('array', [true]),
]) as $attribute
) {
$foundIndex = false;
if ($collectionType === 'projects') {
foreach (
$this->documentsIterator('indexes', [
Query::equal('databaseInternalId', [$attribute['databaseInternalId']]),
Query::equal('collectionInternalId', [$attribute['collectionInternalId']]),
]) as $index
$this->documentsIterator('attributes', [
Query::equal('array', [true]),
]) as $attribute
) {
if (in_array($attribute['key'], $index['attributes'])) {
$this->projectDB->deleteIndex($index['collectionId'], $index['$id']);
$foundIndex = true;
$foundIndex = false;
foreach (
$this->documentsIterator('indexes', [
Query::equal('databaseInternalId', [$attribute['databaseInternalId']]),
Query::equal('collectionInternalId', [$attribute['collectionInternalId']]),
]) as $index
) {
if (in_array($attribute['key'], $index['attributes'])) {
$this->projectDB->deleteIndex($index['collectionId'], $index['$id']);
$foundIndex = true;
}
}
if ($foundIndex === true) {
$this->projectDB->updateAttribute($attribute['collectionInternalId'], $attribute['key'], $attribute['type']);
}
}
if ($foundIndex === true) {
$this->projectDB->updateAttribute($attribute['collectionInternalId'], $attribute['key'], $attribute['type']);
}
}
@ -323,7 +328,6 @@ class V20 extends Migration
*/
protected function createInfMetric(string $metric, int $value): void
{
try {
/**
* Creating inf metric
@ -351,7 +355,6 @@ class V20 extends Migration
*/
protected function migrateUsageMetrics(string $from, string $to): void
{
/**
* inf metric
*/
@ -411,7 +414,6 @@ class V20 extends Migration
*/
private function migrateFunctions(): void
{
$this->migrateUsageMetrics('deployment.$all.storage.size', 'deployments.storage');
$this->migrateUsageMetrics('builds.$all.compute.total', 'builds');
$this->migrateUsageMetrics('builds.$all.compute.time', 'builds.compute');
@ -539,6 +541,14 @@ class V20 extends Migration
$duration = $this->project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $duration);
$document->setAttribute('expire', $expire);
$factors = match ($document->getAttribute('provider')) {
Auth::SESSION_PROVIDER_ANONYMOUS => ['anonymous'],
Auth::SESSION_PROVIDER_PHONE => ['phone'],
default => ['password'],
};
$document->setAttribute('factors', $factors);
break;
}
return $document;

View file

@ -32,6 +32,14 @@ class Mails extends Action
->callback(fn (Message $message, Registry $register, Log $log) => $this->action($message, $register, $log));
}
/**
* @var array<string, string>
*/
protected array $richTextParams = [
'b' => '<strong>',
'/b' => '</strong>',
];
/**
* @param Message $message
* @param Registry $register
@ -81,6 +89,9 @@ class Mails extends Action
// TODO: hotfix for redirect param
$bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: $key !== 'redirect');
}
foreach ($this->richTextParams as $key => $value) {
$bodyTemplate->setParam('{{' . $key . '}}', $value, escapeHtml: false);
}
$body = $bodyTemplate->render();
$subjectTemplate = Template::fromString($subject);

View file

@ -469,6 +469,7 @@ class Messaging extends Action
private function getPushAdapter(Document $provider): ?PushAdapter
{
$credentials = $provider->getAttribute('credentials');
$options = $provider->getAttribute('options');
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
@ -477,6 +478,7 @@ class Messaging extends Action
$credentials['authKeyId'],
$credentials['teamId'],
$credentials['bundleId'],
$options['sandbox']
),
'fcm' => new FCM(\json_encode($credentials['serviceAccountJSON'])),
default => null

View file

@ -116,6 +116,7 @@ abstract class Format
case 'account':
switch ($method) {
case 'createOAuth2Session':
case 'createOAuth2Token':
switch ($param) {
case 'provider':
return 'OAuthProvider';
@ -219,11 +220,11 @@ abstract class Format
return 'MessageStatus';
}
break;
case 'createSMTPProvider':
case 'updateSMTPProvider':
case 'createSmtpProvider':
case 'updateSmtpProvider':
switch ($param) {
case 'encryption':
return 'SMTPEncryption';
return 'SmtpEncryption';
}
break;
}

View file

@ -59,6 +59,10 @@ class V17 extends Filter
private function convertOldQueries(array $content): array
{
if (!isset($content['queries'])) {
return $content;
}
$parsed = [];
foreach ($content['queries'] as $query) {
try {

View file

@ -2226,7 +2226,7 @@ class AccountCustomClientTest extends Scope
/**
* @depends testUpdatePhone
*/
#[Retry(count: 2)]
#[Retry(count: 3)]
public function testPhoneVerification(array $data): array
{
$session = $data['session'] ?? '';

View file

@ -2973,7 +2973,7 @@ trait DatabasesBase
$this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']);
$this->assertEquals('Invalid document structure: Attribute "probability" has invalid format. Value must be a valid range between 0 and 1', $badProbability['body']['message']);
$this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between -9,223,372,036,854,775,808 and 10', $tooHigh['body']['message']);
$this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,808', $tooLow['body']['message']);
$this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and 9,223,372,036,854,775,807', $tooLow['body']['message']);
}
/**

View file

@ -135,7 +135,7 @@ class FunctionsCustomClientTest extends Scope
\sleep(1);
}
$this->assertEquals('ready', $deployment['body']['status']);
$this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body']));
$function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [
'content-type' => 'application/json',
@ -266,7 +266,7 @@ class FunctionsCustomClientTest extends Scope
\sleep(1);
}
$this->assertEquals('ready', $deployment['body']['status']);
$this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body']));
$function = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
'content-type' => 'application/json',

View file

@ -1812,8 +1812,8 @@ trait Base
}
}';
case self::$CREATE_SMTP_PROVIDER:
return 'mutation createSMTPProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean! $fromName: String!, $fromEmail: String!, $replyToName: String, $replyToEmail: String) {
messagingCreateSMTPProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, replyToName: $replyToName, replyToEmail: $replyToEmail) {
return 'mutation createSmtpProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean! $fromName: String!, $fromEmail: String!, $replyToName: String, $replyToEmail: String) {
messagingCreateSmtpProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, replyToName: $replyToName, replyToEmail: $replyToEmail) {
_id
name
provider
@ -1936,8 +1936,8 @@ trait Base
}
}';
case self::$UPDATE_SMTP_PROVIDER:
return 'mutation updateSMTPProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean!, $fromName: String, $fromEmail: String, $enabled: Boolean) {
messagingUpdateSMTPProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, enabled: $enabled) {
return 'mutation updateSmtpProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean!, $fromName: String, $fromEmail: String, $enabled: Boolean) {
messagingUpdateSmtpProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, enabled: $enabled) {
_id
name
provider

View file

@ -98,16 +98,22 @@ trait MessagingBase
];
$providers = [];
foreach (\array_keys($providersParams) as $key) {
foreach ($providersParams as $key => $params) {
$response = $this->client->call(Client::METHOD_POST, '/messaging/providers/' . $key, \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), $providersParams[$key]);
]), $params);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals($providersParams[$key]['name'], $response['body']['name']);
\array_push($providers, $response['body']);
$this->assertEquals($params['name'], $response['body']['name']);
$providers[] = $response['body'];
switch ($key) {
case 'apns':
$this->assertEquals(false, $response['body']['options']['sandbox']);
break;
}
}
return $providers;
@ -341,6 +347,17 @@ trait MessagingBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('android-app', $response['body']['name']);
$response2 = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topics['private']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'name' => 'ios-app',
'subscribe' => [Role::user('some-user')->toString()],
]);
$this->assertEquals(200, $response2['headers']['status-code']);
$this->assertEquals('ios-app', $response2['body']['name']);
return $response['body']['$id'];
}

View file

@ -21,11 +21,6 @@ class ExtensionsTest extends TestCase
$this->assertEquals(true, extension_loaded('yaml'));
}
public function testOPCache(): void
{
$this->assertEquals(true, extension_loaded('Zend OPcache'));
}
public function testDOM(): void
{
$this->assertEquals(true, extension_loaded('dom'));