1
0
Fork 0
mirror of synced 2024-07-01 20:50:49 +12:00

Merge branch 'master' into 0.13.x

This commit is contained in:
Damodar Lohani 2022-02-22 06:43:07 +00:00
commit 53de779e89
31 changed files with 243 additions and 75 deletions

View file

@ -1,3 +1,12 @@
# Version 0.12.3
## Bugs
- Fix update membership roles (#2799)
- Fix migration to 0.12.x to populate search fields (#2799)
## Security
- Fix URL schema Validation to only allow http/https (#2801)
# Version 0.12.2 # Version 0.12.2
## Bugs ## Bugs

View file

@ -59,7 +59,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \ --entrypoint="install" \
appwrite/appwrite:0.12.2 appwrite/appwrite:0.12.3
``` ```
### Windows ### Windows
@ -71,7 +71,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^ --volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^ --entrypoint="install" ^
appwrite/appwrite:0.12.2 appwrite/appwrite:0.12.3
``` ```
#### PowerShell #### PowerShell
@ -81,7 +81,7 @@ docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock , --volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw , --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" , --entrypoint="install" ,
appwrite/appwrite:0.12.2 appwrite/appwrite:0.12.3
``` ```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。 运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。

View file

@ -62,7 +62,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \ --volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \ --entrypoint="install" \
appwrite/appwrite:0.12.2 appwrite/appwrite:0.12.3
``` ```
### Windows ### Windows
@ -74,7 +74,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^ --volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^ --entrypoint="install" ^
appwrite/appwrite:0.12.2 appwrite/appwrite:0.12.3
``` ```
#### PowerShell #### PowerShell
@ -84,7 +84,7 @@ docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock , --volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw , --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" , --entrypoint="install" ,
appwrite/appwrite:0.12.2 appwrite/appwrite:0.12.3
``` ```
Once the Docker installation completes, 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 installation completes. Once the Docker installation completes, 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 installation completes.

View file

@ -1541,6 +1541,17 @@ $collections = [
'array' => false, 'array' => false,
'filters' => ['encrypt'], 'filters' => ['encrypt'],
], ],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
], ],
'indexes' => [ 'indexes' => [
[ [
@ -1564,6 +1575,13 @@ $collections = [
'lengths' => [Database::LENGTH_KEY], 'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC], 'orders' => [Database::ORDER_ASC],
], ],
[
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
], ],
], ],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -144,7 +144,7 @@ App::get('/v1/avatars/image')
->label('sdk.description', '/docs/references/avatars/get-image.md') ->label('sdk.description', '/docs/references/avatars/get-image.md')
->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE) ->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE)
->param('url', '', new URL(), 'Image URL which you want to crop.') ->param('url', '', new URL(['http', 'https']), 'Image URL which you want to crop.')
->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000.', true) ->param('width', 400, new Range(0, 2000), 'Resize preview image width, Pass an integer between 0 to 2000.', true)
->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000.', true) ->param('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000.', true)
->inject('response') ->inject('response')
@ -213,7 +213,7 @@ App::get('/v1/avatars/favicon')
->label('sdk.description', '/docs/references/avatars/get-favicon.md') ->label('sdk.description', '/docs/references/avatars/get-favicon.md')
->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE) ->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE)
->param('url', '', new URL(), 'Website URL which you want to fetch the favicon from.') ->param('url', '', new URL(['http', 'https']), 'Website URL which you want to fetch the favicon from.')
->inject('response') ->inject('response')
->action(function ($url, $response) { ->action(function ($url, $response) {
/** @var Appwrite\Utopia\Response $response */ /** @var Appwrite\Utopia\Response $response */

View file

@ -585,7 +585,7 @@ App::post('/v1/projects/:projectId/webhooks')
->param('projectId', null, new UID(), 'Project unique ID.') ->param('projectId', null, new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.') ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new WhiteList(array_keys(Config::getParam('events'), true), true)), 'Events list.') ->param('events', null, new ArrayList(new WhiteList(array_keys(Config::getParam('events'), true), true)), 'Events list.')
->param('url', null, new URL(), 'Webhook URL.') ->param('url', null, new URL(['http', 'https']), 'Webhook URL.')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.') ->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
@ -707,7 +707,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->param('webhookId', null, new UID(), 'Webhook unique ID.') ->param('webhookId', null, new UID(), 'Webhook unique ID.')
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.') ->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new WhiteList(array_keys(Config::getParam('events'), true), true)), 'Events list.') ->param('events', null, new ArrayList(new WhiteList(array_keys(Config::getParam('events'), true), true)), 'Events list.')
->param('url', null, new URL(), 'Webhook URL.') ->param('url', null, new URL(['http', 'https']), 'Webhook URL.')
->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.') ->param('security', false, new Boolean(true), 'Certificate verification, false for disabled or true for enabled.')
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true) ->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true) ->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)

View file

@ -63,7 +63,9 @@ App::post('/v1/teams')
]))); ])));
if (!$isPrivilegedUser && !$isAppUser) { // Don't add user on server mode if (!$isPrivilegedUser && !$isAppUser) { // Don't add user on server mode
$membershipId = $dbForProject->getId();
$membership = new Document([ $membership = new Document([
'$id' => $membershipId,
'$read' => ['user:'.$user->getId(), 'team:'.$team->getId()], '$read' => ['user:'.$user->getId(), 'team:'.$team->getId()],
'$write' => ['user:'.$user->getId(), 'team:'.$team->getId().'/owner'], '$write' => ['user:'.$user->getId(), 'team:'.$team->getId().'/owner'],
'userId' => $user->getId(), 'userId' => $user->getId(),
@ -73,6 +75,7 @@ App::post('/v1/teams')
'joined' => \time(), 'joined' => \time(),
'confirm' => true, 'confirm' => true,
'secret' => '', 'secret' => '',
'search' => implode(' ', [$membershipId, $user->getId()])
]); ]);
$membership = $dbForProject->createDocument('memberships', $membership); $membership = $dbForProject->createDocument('memberships', $membership);
@ -353,8 +356,9 @@ App::post('/v1/teams/:teamId/memberships')
$secret = Auth::tokenGenerator(); $secret = Auth::tokenGenerator();
$membershipId = $dbForProject->getId();
$membership = new Document([ $membership = new Document([
'$id' => $dbForProject->getId(), '$id' => $membershipId,
'$read' => ['role:all'], '$read' => ['role:all'],
'$write' => ['user:'.$invitee->getId(), 'team:'.$team->getId().'/owner'], '$write' => ['user:'.$invitee->getId(), 'team:'.$team->getId().'/owner'],
'userId' => $invitee->getId(), 'userId' => $invitee->getId(),
@ -364,6 +368,7 @@ App::post('/v1/teams/:teamId/memberships')
'joined' => ($isPrivilegedUser || $isAppUser) ? \time() : 0, 'joined' => ($isPrivilegedUser || $isAppUser) ? \time() : 0,
'confirm' => ($isPrivilegedUser || $isAppUser), 'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => Auth::hash($secret), 'secret' => Auth::hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
]); ]);
if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership
@ -458,8 +463,27 @@ App::get('/v1/teams/:teamId/memberships')
} }
} }
$memberships = $dbForProject->find('memberships', [new Query('teamId', Query::TYPE_EQUAL, [$teamId])], $limit, $offset, [], [$orderType], $cursorMembership ?? null, $cursorDirection); $queries = [new Query('teamId', Query::TYPE_EQUAL, [$teamId])];
$sum = $dbForProject->count('memberships', [new Query('teamId', Query::TYPE_EQUAL, [$teamId])], APP_LIMIT_COUNT);
if (!empty($search)) {
$queries[] = new Query('search', Query::TYPE_SEARCH, [$search]);
}
$memberships = $dbForProject->find(
collection: 'memberships',
queries: $queries,
limit: $limit,
offset: $offset,
orderTypes: [$orderType],
cursor: $cursorMembership ?? null,
cursorDirection: $cursorDirection
);
$sum = $dbForProject->count(
collection:'memberships',
queries: $queries,
max: APP_LIMIT_COUNT
);
$memberships = array_filter($memberships, fn(Document $membership) => !empty($membership->getAttribute('userId'))); $memberships = array_filter($memberships, fn(Document $membership) => !empty($membership->getAttribute('userId')));
@ -565,25 +589,40 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$isAppUser = Auth::isAppUser(Authorization::getRoles()); $isAppUser = Auth::isAppUser(Authorization::getRoles());
$isOwner = Authorization::isRole('team:'.$team->getId().'/owner');; $isOwner = Authorization::isRole('team:' . $team->getId() . '/owner');;
if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server) if (!$isOwner && !$isPrivilegedUser && !$isAppUser) { // Not owner, not admin, not app (server)
throw new Exception('User is not allowed to modify roles', 401, Exception::USER_UNAUTHORIZED); throw new Exception('User is not allowed to modify roles', 401, Exception::USER_UNAUTHORIZED);
} }
// Update the roles /**
* Update the roles
*/
$membership->setAttribute('roles', $roles); $membership->setAttribute('roles', $roles);
$membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership);
// TODO sync updated membership in the user $profile object using TYPE_REPLACE /**
* Replace membership on profile
*/
$memberships = array_filter($profile->getAttribute('memberships'), fn (Document $m) => $m->getId() !== $membership->getId());
$profile
->setAttribute('memberships', $memberships)
->setAttribute('memberships', $membership, Document::SET_TYPE_APPEND);
Authorization::skip(fn () => $dbForProject->updateDocument('users', $profile->getId(), $profile));
$audits $audits
->setParam('userId', $user->getId()) ->setParam('userId', $user->getId())
->setParam('event', 'teams.memberships.update') ->setParam('event', 'teams.memberships.update')
->setParam('resource', 'team/'.$teamId) ->setParam('resource', 'team/' . $teamId);
;
$response->dynamic($membership, Response::MODEL_MEMBERSHIP); $response->dynamic(
$membership
->setAttribute('email', $profile->getAttribute('email'))
->setAttribute('name', $profile->getAttribute('name')),
Response::MODEL_MEMBERSHIP
);
}); });
App::patch('/v1/teams/:teamId/memberships/:membershipId/status') App::patch('/v1/teams/:teamId/memberships/:membershipId/status')

View file

@ -70,7 +70,7 @@ const APP_LIMIT_ENCRYPTION = 20000000; //20MB
const APP_LIMIT_COMPRESSION = 20000000; //20MB const APP_LIMIT_COMPRESSION = 20000000; //20MB
const APP_LIMIT_PREVIEW = 10000000; //10MB file size limit for preview endpoint const APP_LIMIT_PREVIEW = 10000000; //10MB file size limit for preview endpoint
const APP_CACHE_BUSTER = 201; const APP_CACHE_BUSTER = 201;
const APP_VERSION_STABLE = '0.13.0'; const APP_VERSION_STABLE = '0.12.3';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip'; const APP_DATABASE_ATTRIBUTE_IP = 'ip';

View file

@ -494,7 +494,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<input type="hidden" data-forms-key-value data-ls-attrs="name={{$index}}" data-ls-bind="{{var}}" /> <input type="hidden" data-forms-key-value data-ls-attrs="name={{$index}}" data-ls-bind="{{var}}" />
</div> </div>
<div class="col span-2"> <div class="col span-2">
<button type="button" data-remove class="reverse danger round pull-end"><i class="icon-cancel"></i></button> <button type="button" data-remove class="close pull-end is-margin-top-10"><i class="icon-cancel"></i></button>
</div> </div>
</div> </div>
</div> </div>
@ -507,7 +507,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
<input type="hidden" data-ls-attrs="data-forms-key-value"/> <input type="hidden" data-ls-attrs="data-forms-key-value"/>
</div> </div>
<div class="col span-2"> <div class="col span-2">
<button type="button" data-remove class="reverse danger round pull-end"><i class="icon-cancel"></i></button> <button type="button" data-remove class="close pull-end is-margin-top-10"><i class="icon-cancel"></i></button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -241,11 +241,6 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</p> </p>
<?php endif; ?> <?php endif; ?>
</div> </div>
<hr />
<div class="text-align-center text-size-small text-bold text-success" data-ls-if="!!({{console-project.serviceStatusFor<?php echo ucFirst($this->escape($key)); ?>}})">Enabled</div>
<div class="text-align-center text-size-small text-bold text-danger" data-ls-if="(!{{console-project.serviceStatusFor<?php echo ucFirst($this->escape($key)); ?>}})">Disabled</div>
</div> </div>
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
@ -548,7 +543,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<div class="margin-bottom-tiny"> <div class="margin-bottom-tiny">
<span data-ls-bind="{{member.name}}"></span> &nbsp;&nbsp;<span class="tag" data-ls-bind="{{member.roles.0}}"></span> &nbsp;&nbsp;<span data-ls-if="false === {{member.confirm}}" class="tag red">Pending Approval</span> <span data-ls-bind="{{member.name}}"></span> &nbsp;&nbsp;<span class="tag" data-ls-bind="{{member.roles.0}}"></span> &nbsp;&nbsp;<span data-ls-if="false === {{member.confirm}}" class="tag red">Pending Approval</span>
</div> </div>
<span class="text-size-small text-fade" data-ls-bind="{{member.email}}"></small> <span class="text-size-small text-fade" data-ls-bind="{{member.email}}"></span>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -79,13 +79,13 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
</td> </td>
<td data-title="Name: "> <td data-title="Name: ">
<a data-ls-attrs="href=/console/users/user?id={{user.$id}}&project={{router.params.project}}"> <a data-ls-attrs="href=/console/users/user?id={{user.$id}}&project={{router.params.project}}">
<span data-ls-bind="{{user.name}}"></span> <span data-ls-bind="{{user.name}}" data-ls-attrs="title={{user.name}}"></span>
<span data-ls-if="{{user.name|escape}} === '' && {{user.email}} !== ''">Unknown</span> <span data-ls-if="{{user.name|escape}} === '' && {{user.email}} !== ''">Unknown</span>
<span data-ls-if="{{user.name|escape}} === '' && {{user.email}} === ''">Anonymous User</span> <span data-ls-if="{{user.name|escape}} === '' && {{user.email}} === ''">Anonymous User</span>
</a> </a>
</td> </td>
<td data-title="Email: "> <td data-title="Email: ">
<small data-ls-bind="{{user.email}}"></span> <small data-ls-bind="{{user.email}}" data-ls-attrs="title={{user.email}}"></span>
</td> </td>
<td data-title="Status: "> <td data-title="Status: ">
<span data-ls-if="{{user.emailVerification}} === true && {{user.status}} === true"> <span data-ls-if="{{user.emailVerification}} === true && {{user.status}} === true">
@ -245,7 +245,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<img src="" data-ls-attrs="src={{team.name|avatar}}" data-size="45" alt="Collection Avatar" class="avatar margin-end pull-start" loading="lazy" width="30" height="30" /> <img src="" data-ls-attrs="src={{team.name|avatar}}" data-size="45" alt="Collection Avatar" class="avatar margin-end pull-start" loading="lazy" width="30" height="30" />
</td> </td>
<td data-title="Name: "> <td data-title="Name: ">
<a data-ls-attrs="href=/console/users/teams/team?id={{team.$id}}&project={{router.params.project}}" data-ls-bind="{{team.name}}"></a> <a data-ls-attrs="href=/console/users/teams/team?id={{team.$id}}&project={{router.params.project}}" data-ls-bind="{{team.name}}" data-ls-attrs="title={{team.name}}"></a>
</td> </td>
<td data-title="Members: "><span data-ls-bind="{{team.sum}} members"></span></td> <td data-title="Members: "><span data-ls-bind="{{team.sum}} members"></span></td>
<td data-title="Date Created: "><small data-ls-bind="{{team.dateCreated|dateText}}"></small></td> <td data-title="Date Created: "><small data-ls-bind="{{team.dateCreated|dateText}}"></small></td>
@ -406,7 +406,7 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<img src="<?php echo $this->escape($icon); ?>?buster=<?php echo APP_CACHE_BUSTER; ?>" alt="Email/Password Logo" class="pull-start provider margin-end" /> <img src="<?php echo $this->escape($icon); ?>?buster=<?php echo APP_CACHE_BUSTER; ?>" alt="Email/Password Logo" class="pull-start provider margin-end" />
<span class="text-size-small"><?php echo $this->escape($name); ?><?php if(!$enabled): ?> <spann class="text-fade text-size-xs">soon</span><?php endif; ?> <span class="text-size-small"><?php echo $this->escape($name); ?><?php if(!$enabled): ?> <span class="text-fade text-size-xs">soon</span><?php endif; ?>
<?php if( in_array($key, ['usersAuthMagicURL', 'usersAuthInvites']) && !$smtpEnabled): ?> <?php if( in_array($key, ['usersAuthMagicURL', 'usersAuthInvites']) && !$smtpEnabled): ?>
<p class="margin-bottom-no text-one-liner text-size-small text-danger"> <p class="margin-bottom-no text-one-liner text-size-small text-danger">

View file

@ -295,8 +295,8 @@
data-param-user-id="{{router.params.id}}" data-param-user-id="{{router.params.id}}"
data-event="load,users.update"> data-event="load,users.update">
<div data-ls-if="{{sessions.sessions.length}} === 0" style="display: none" class="margin-top-xxl margin-bottom-xxl text-align-center"> <div class="box margin-top margin-bottom" data-ls-if="{{sessions.sessions.length}} === 0" style="display: none" class="margin-top-xxl margin-bottom-xxl text-align-center">
No sessions available. <h3 class="text-bold margin-bottom-no">No sessions available.</h3>
</div> </div>
<div data-ls-if="{{sessions.sessions.length}} !== 0" style="display: none"> <div data-ls-if="{{sessions.sessions.length}} !== 0" style="display: none">
@ -372,8 +372,8 @@
data-param-user-id="{{router.params.id}}" data-param-user-id="{{router.params.id}}"
data-event="load,logs-load"> data-event="load,logs-load">
<div data-ls-if="{{logs.logs.length}} === 0" style="display: none" class="margin-top-xxl margin-bottom-xxl text-align-center"> <div class="box margin-top margin-bottom" data-ls-if="{{logs.logs.length}} === 0" style="display: none" class="margin-top-xxl margin-bottom-xxl text-align-center">
No logs available. <h3 class="text-bold margin-bottom-no">No logs available.</h3>
</div> </div>
<div class="box" data-ls-if="{{logs.logs.length}} !== 0" style="display: none"> <div class="box" data-ls-if="{{logs.logs.length}} !== 0" style="display: none">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -164,17 +164,6 @@
} }
} }
button.close {
width: 30px;
height: 30px;
line-height: 30px;
padding: 0;
margin: 0;
background: var(--config-color-normal);
color: var(--config-color-background-fade);
border-radius: 50%;
}
.paging { .paging {
form { form {
padding: 0; padding: 0;

View file

@ -239,6 +239,20 @@ button,
} }
} }
button.close {
width: 30px;
height: 30px;
line-height: 30px;
padding: 0;
margin: 0;
background: var(--config-color-normal);
color: var(--config-color-background-fade);
border-radius: 50%;
.icon-cancel::before{line-height:1;}
&.is-margin-top-10{margin-top:10px;}
}
label { label {
margin-bottom: 15px; margin-bottom: 15px;
display: block; display: block;
@ -622,6 +636,10 @@ input[type=checkbox], input[type=radio] {
} }
} }
input[type=checkbox] {
&:after {border-radius:4px;}
}
.input-copy { .input-copy {
position: relative; position: relative;

View file

@ -290,7 +290,7 @@ class Realtime extends Adapter
$channels[] = 'documents'; $channels[] = 'documents';
$channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents'; $channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents';
$channels[] = 'documents.' . $payload->getId(); $channels[] = 'collections.' . $payload->getAttribute('$collection') . '.documents.' . $payload->getId();
$roles = ($collection->getAttribute('permission') === 'collection') ? $collection->getRead() : $payload->getRead(); $roles = ($collection->getAttribute('permission') === 'collection') ? $collection->getRead() : $payload->getRead();

View file

@ -9,10 +9,20 @@ use Utopia\Validator;
* *
* Validate that an variable is a valid URL * Validate that an variable is a valid URL
* *
* @package Utopia\Validator * @package Appwrite\Network\Validator
*/ */
class URL extends Validator class URL extends Validator
{ {
protected array $allowedSchemes;
/**
* @param array $allowedSchemes
*/
public function __construct(array $allowedSchemes = [])
{
$this->allowedSchemes = $allowedSchemes;
}
/** /**
* Get Description * Get Description
* *
@ -22,6 +32,10 @@ class URL extends Validator
*/ */
public function getDescription(): string public function getDescription(): string
{ {
if (!empty($this->allowedSchemes)) {
return 'Value must be a valid URL with following schemes (' . \implode(', ', $this->allowedSchemes) . ')';
}
return 'Value must be a valid URL'; return 'Value must be a valid URL';
} }
@ -39,6 +53,10 @@ class URL extends Validator
return false; return false;
} }
if (!empty($this->allowedSchemes) && !\in_array(\parse_url($value, PHP_URL_SCHEME), $this->allowedSchemes)) {
return false;
}
return true; return true;
} }

View file

@ -19,9 +19,9 @@ class Func extends Model
->addRule('execute', [ ->addRule('execute', [
'type' => self::TYPE_STRING, 'type' => self::TYPE_STRING,
'description' => 'Execution permissions.', 'description' => 'Execution permissions.',
'default' => '', 'default' => [],
'example' => 'role:member', 'example' => 'role:member',
'array' => false, 'array' => true,
]) ])
->addRule('name', [ ->addRule('name', [
'type' => self::TYPE_STRING, 'type' => self::TYPE_STRING,

View file

@ -259,6 +259,14 @@ trait AvatarsBase
$this->assertEquals(400, $response['headers']['status-code']); $this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'invalid://appwrite.io/images/apple.png'
]);
$this->assertEquals(400, $response['headers']['status-code']);
// TODO Add test for non-image file (PDF, WORD) // TODO Add test for non-image file (PDF, WORD)
return []; return [];

View file

@ -25,6 +25,7 @@ class FunctionsCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [ ], $this->getHeaders()), [
'functionId' => 'unique()', 'functionId' => 'unique()',
'execute' => ['role:all'],
'name' => 'Test', 'name' => 'Test',
'runtime' => 'php-8.0', 'runtime' => 'php-8.0',
'vars' => [ 'vars' => [
@ -58,6 +59,9 @@ class FunctionsCustomServerTest extends Scope
'account.create', 'account.create',
'account.delete', 'account.delete',
], $response1['body']['events']); ], $response1['body']['events']);
$this->assertEquals([
'role:all'
], $response1['body']['execute']);
$this->assertEquals('0 0 1 1 *', $response1['body']['schedule']); $this->assertEquals('0 0 1 1 *', $response1['body']['schedule']);
$this->assertEquals(10, $response1['body']['timeout']); $this->assertEquals(10, $response1['body']['timeout']);

View file

@ -834,6 +834,17 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']); $this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/webhooks', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Webhook Test',
'events' => ['account.create', 'account.update.email'],
'url' => 'invalid://appwrite.io',
]);
$this->assertEquals(400, $response['headers']['status-code']);
return $data; return $data;
} }
@ -979,6 +990,17 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']); $this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PUT, '/projects/'.$id.'/webhooks/'.$webhookId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Webhook Test Update',
'events' => ['account.delete', 'account.sessions.delete', 'storage.files.create'],
'url' => 'invalid://appwrite.io/new',
]);
$this->assertEquals(400, $response['headers']['status-code']);
return $data; return $data;
} }

View file

@ -80,8 +80,8 @@ class RealtimeCustomClientTest extends Scope
'collections.1.documents', 'collections.1.documents',
'collections.2.documents', 'collections.2.documents',
'documents', 'documents',
'documents.1', 'collections.1.documents.1',
'documents.2', 'collections.2.documents.2',
], $headers); ], $headers);
$response = json_decode($client->receive(), true); $response = json_decode($client->receive(), true);
@ -100,8 +100,8 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains('collections.1.documents', $response['data']['channels']); $this->assertContains('collections.1.documents', $response['data']['channels']);
$this->assertContains('collections.2.documents', $response['data']['channels']); $this->assertContains('collections.2.documents', $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.1', $response['data']['channels']); $this->assertContains('collections.1.documents.1', $response['data']['channels']);
$this->assertContains('documents.2', $response['data']['channels']); $this->assertContains('collections.2.documents.2', $response['data']['channels']);
$this->assertEquals($userId, $response['data']['user']['$id']); $this->assertEquals($userId, $response['data']['user']['$id']);
$client->close(); $client->close();
@ -606,7 +606,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertArrayHasKey('timestamp', $response['data']); $this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']); $this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents.' . $document['body']['$id'], $response['data']['channels']);
$this->assertContains('collections.' . $actors['body']['$id'] . '.documents', $response['data']['channels']); $this->assertContains('collections.' . $actors['body']['$id'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.create', $response['data']['event']); $this->assertEquals('database.documents.create', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']); $this->assertNotEmpty($response['data']['payload']);
@ -638,7 +638,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertArrayHasKey('timestamp', $response['data']); $this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']); $this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $data['documentId'], $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents.' . $data['documentId'], $response['data']['channels']);
$this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.update', $response['data']['event']); $this->assertEquals('database.documents.update', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']); $this->assertNotEmpty($response['data']['payload']);
@ -676,7 +676,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertArrayHasKey('timestamp', $response['data']); $this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']); $this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents.' . $document['body']['$id'], $response['data']['channels']);
$this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.delete', $response['data']['event']); $this->assertEquals('database.documents.delete', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']); $this->assertNotEmpty($response['data']['payload']);
@ -767,7 +767,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertArrayHasKey('timestamp', $response['data']); $this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']); $this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents.' . $document['body']['$id'], $response['data']['channels']);
$this->assertContains('collections.' . $actors['body']['$id'] . '.documents', $response['data']['channels']); $this->assertContains('collections.' . $actors['body']['$id'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.create', $response['data']['event']); $this->assertEquals('database.documents.create', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']); $this->assertNotEmpty($response['data']['payload']);
@ -798,7 +798,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertArrayHasKey('timestamp', $response['data']); $this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']); $this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $data['documentId'], $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents.' . $data['documentId'], $response['data']['channels']);
$this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.update', $response['data']['event']); $this->assertEquals('database.documents.update', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']); $this->assertNotEmpty($response['data']['payload']);
@ -836,7 +836,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertArrayHasKey('timestamp', $response['data']); $this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']); $this->assertCount(3, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']); $this->assertContains('documents', $response['data']['channels']);
$this->assertContains('documents.' . $document['body']['$id'], $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents.' . $document['body']['$id'], $response['data']['channels']);
$this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']); $this->assertContains('collections.' . $data['actorsId'] . '.documents', $response['data']['channels']);
$this->assertEquals('database.documents.delete', $response['data']['event']); $this->assertEquals('database.documents.delete', $response['data']['event']);
$this->assertNotEmpty($response['data']['payload']); $this->assertNotEmpty($response['data']['payload']);

View file

@ -28,6 +28,48 @@ trait TeamsBaseClient
$this->assertEquals($this->getUser()['email'], $response['body']['memberships'][0]['email']); $this->assertEquals($this->getUser()['email'], $response['body']['memberships'][0]['email']);
$this->assertEquals('owner', $response['body']['memberships'][0]['roles'][0]); $this->assertEquals('owner', $response['body']['memberships'][0]['roles'][0]);
$membershipId = $response['body']['memberships'][0]['$id'];
$response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid.'/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $this->getUser()['$id']
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['sum']);
$this->assertNotEmpty($response['body']['memberships'][0]);
$this->assertEquals($this->getUser()['name'], $response['body']['memberships'][0]['name']);
$this->assertEquals($this->getUser()['email'], $response['body']['memberships'][0]['email']);
$this->assertEquals('owner', $response['body']['memberships'][0]['roles'][0]);
$response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid.'/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => $membershipId
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['sum']);
$this->assertNotEmpty($response['body']['memberships'][0]);
$this->assertEquals($this->getUser()['name'], $response['body']['memberships'][0]['name']);
$this->assertEquals($this->getUser()['email'], $response['body']['memberships'][0]['email']);
$this->assertEquals('owner', $response['body']['memberships'][0]['roles'][0]);
$response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid.'/memberships', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'search' => 'unknown'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['sum']);
$this->assertEmpty($response['body']['memberships']);
$this->assertEquals(0, $response['body']['sum']);
/** /**
* Test for FAILURE * Test for FAILURE
*/ */

View file

@ -17,10 +17,7 @@ use PHPUnit\Framework\TestCase;
class URLTest extends TestCase class URLTest extends TestCase
{ {
/** protected ?URL $url;
* @var Domain
*/
protected $url = null;
public function setUp():void public function setUp():void
{ {
@ -32,9 +29,9 @@ class URLTest extends TestCase
$this->url = null; $this->url = null;
} }
public function testIsValid() public function testIsValid(): void
{ {
// Assertions $this->assertEquals('Value must be a valid URL', $this->url->getDescription());
$this->assertEquals(true, $this->url->isValid('http://example.com')); $this->assertEquals(true, $this->url->isValid('http://example.com'));
$this->assertEquals(true, $this->url->isValid('https://example.com')); $this->assertEquals(true, $this->url->isValid('https://example.com'));
$this->assertEquals(true, $this->url->isValid('htts://example.com')); // does not validate protocol $this->assertEquals(true, $this->url->isValid('htts://example.com')); // does not validate protocol
@ -45,4 +42,13 @@ class URLTest extends TestCase
$this->assertEquals(true, $this->url->isValid('http://www.example.com/foo%2\u00c2\u00a9zbar')); $this->assertEquals(true, $this->url->isValid('http://www.example.com/foo%2\u00c2\u00a9zbar'));
$this->assertEquals(true, $this->url->isValid('http://www.example.com/?q=%3Casdf%3E')); $this->assertEquals(true, $this->url->isValid('http://www.example.com/?q=%3Casdf%3E'));
} }
public function testIsValidAllowedSchemes(): void
{
$this->url = new URL(['http', 'https']);
$this->assertEquals('Value must be a valid URL with following schemes (http, https)', $this->url->getDescription());
$this->assertEquals(true, $this->url->isValid('http://example.com'));
$this->assertEquals(true, $this->url->isValid('https://example.com'));
$this->assertEquals(false, $this->url->isValid('gopher://www.example.com'));
}
} }