1
0
Fork 0
mirror of synced 2024-06-02 10:54:44 +12:00

Merge branch 'master' of github.com:appwrite/appwrite into feat-functions-refactor

This commit is contained in:
Christy Jacob 2022-02-18 13:38:54 +04:00
commit 4ad47061dd
143 changed files with 2994 additions and 6148 deletions

View file

@ -1,9 +1,10 @@
name: "Tests"
on: [pull_request]
jobs:
tests:
name: Unit & E2E
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- name: Checkout repository
@ -18,29 +19,16 @@ jobs:
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Prepare Docker
- name: Build Appwrite
# Upstream bug causes buildkit pulls to fail so prefetch base images
# https://github.com/moby/moby/issues/41864
run: |
export COMPOSE_INTERACTIVE_NO_CLI
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
echo "_APP_FUNCTIONS_RUNTIMES=php-8.0" >> .env
docker pull composer:2.0
docker pull php:8.0-cli-alpine
docker compose pull
- name: Prepare Cache
uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
- name: Build Appwrite
run: docker compose build --progress=plain
- name: Start Appwrite
run: |
docker compose build --progress=plain
docker compose up -d
sleep 30
sleep 10
- name: Doctor
run: docker compose exec -T appwrite doctor
@ -49,3 +37,9 @@ jobs:
- name: Run Tests
run: docker compose exec -T appwrite test --debug
- name: Teardown
if: always()
run: |
docker compose down -v
docker ps -aq | xargs docker rm --force

View file

@ -10,6 +10,19 @@
- Updated endpoints to reflect the new terminology
- Updated UI with these changes
- Updated event names from `function.tags.*` to `function.deployments.*`
# Version 0.12.2
## Bugs
- Fix security vulnerability in the Console (#2778)
- Fix security vulnerability in the ACME-Challenge (#2780)
## Upgrades
- Upgraded `redis` extenstion to version 5.3.6
- Upgraded `swoole` extenstion to version 4.8.6
- Upgraded `imagick` extenstion to version 3.7.0
- Upgraded GEO IP database to version February 2022
# Version 0.12.1
## Bugs
@ -95,6 +108,21 @@
- Upgraded Redis to 6.2
- Upgraded InfluxDB to 1.4.0
- Upgraded Telegraf to 1.3.0
# Version 0.11.1
## Bugs
- Fix security vulnerability in the Console (#2777)
- Fix security vulnerability in the ACME-Challenge (#2779)
## Upgrades
- Upgraded redis extenstion to version 5.3.6
- Upgraded swoole extenstion to version 4.8.6
- Upgraded imagick extenstion to version 3.7.0
- Upgraded yaml extenstion to version 2.2.2
- Upgraded maxminddb extenstion to version 1.11.0
- Upgraded GEO IP database to version February 2022
# Version 0.11.0
## Features

View file

@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM node:16-alpine as node
FROM node:16.13.2-alpine3.15 as node
WORKDIR /usr/local/src/
@ -24,7 +24,7 @@ COPY public /usr/local/src/public
RUN npm ci
RUN npm run build
FROM php:8.0.14-cli-alpine as compile
FROM php:8.0.14-cli-alpine3.15 as compile
ARG DEBUG=false
ENV DEBUG=$DEBUG
@ -123,7 +123,7 @@ RUN \
./configure && \
make && make install
FROM php:8.0.14-cli-alpine as final
FROM php:8.0.14-cli-alpine3.15 as final
LABEL maintainer="team@appwrite.io"

171
README-CN.md Normal file
View file

@ -0,0 +1,171 @@
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img width="260" height="39" src="https://appwrite.io/images/appwrite.svg" alt="Appwrite Logo"></a>
<br />
<br />
<b>适用于[Flutter/Vue/Angular/React/iOS/Android/* 等等平台 *]的完整后端服务</b>
<br />
<br />
</p>
<!-- [![Hacktoberfest](https://img.shields.io/static/v1?label=hacktoberfest&message=friendly&color=90a88b&style=flat-square)](https://hacktoberfest.appwrite.io) -->
[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord?r=Github)
[![Docker Pulls](https://img.shields.io/docker/pulls/appwrite/appwrite?color=f02e65&style=flat-square)](https://hub.docker.com/r/appwrite/appwrite)
[![Build Status](https://img.shields.io/travis/com/appwrite/appwrite?style=flat-square)](https://travis-ci.com/appwrite/appwrite)
[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![翻译](https://img.shields.io/badge/translate-f02e65?style=flat-square)](docs/tutorials/add-translations.md)
[![周边商店](https://img.shields.io/badge/swag%20store-f02e65?style=flat-square)](https://store.appwrite.io)
[English](README.md) | 简体中文
Appwrite是一个基于Docker的端到端开发者平台其容器化的微服务库可应用于网页端移动端以及后端。Appwrite 通过视觉化界面极简了从零编写 API 的繁琐过程,在保证软件安全的前提下为开发者创造了一个高效的开发环境。
Appwrite 可以提供给开发者用户验证,外部授权,用户数据读写检索,文件储存, 图像处理,云函数计算,[等多种服务]https/ /appwrite.io/docs
![Appwrite](public/images/github.png)
更多信息请到 Appwrite 官网查看: [https://appwrite.io](https://appwrite.io)
内容:
- [安装](#安装)
- [Unix](#unix)
- [Windows](#windows)
- [CMD](#cmd)
- [PowerShell](#powershell)
- [从旧版本升级](#从旧版本升级)
- [快速入门](#入门)
- [软件服务](#软件服务)
- [开发套件](#开发套件)
- [客户端](#客户端)
- [服务器](#服务器)
- [开发者社区](#开发者社区)
- [软件架构]](#软件架构)
- [贡献代码](#贡献代码)
- [安全](#安全)
- [订阅我们](#订阅我们)
- [版权说明](#版权说明)
## 安装
Appwrite 的容器化服务器只需要一行指令就可以运行。您可以使用 docker-compose 在本地主机上运行 Appwrite也可以在任何其他容器化工具如 Kubernetes、Docker Swarm 或 Rancher上运行 Appwrite。
开始运行 Appwrite 服务器的最简单方法是运行我们的 docker-compose 文件。在运行安装命令之前,请确保您的机器上安装了 [Docker](https://dockerdocs.cn/get-docker/index.html)
### Unix
```bash
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:0.12.2
```
### Windows
#### CMD
```cmd
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:0.12.2
```
#### PowerShell
```powershell
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:0.12.2
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
需要自定义容器构架,请查看我们的 Docker [环境变量](https://appwrite.io/docs/environment-variables) 文档。您还可以参考我们的 [docker-compose.yml](https://gist.github.com/eldadfux/977869ff6bdd7312adfd4e629ee15cc5#file-docker-compose-yml) 文件手动设置环境。
### 从旧版本升级
如果您从旧版本升级 Appwrite 服务器,则应在设置完成后使用 Appwrite 迁移工具。有关这方面的更多信息,请查看 [安装文档](https://appwrite.io/docs/installation)。
## 入门
开始使用 Appwrite 只需要在控制台创建一个新项目,选择开发平台,然后抓取我们的开发套件。您可以从以下的教程中找到你喜欢的平台开始使用 Appwrite。
* [开始使用 Web](https://appwrite.io/docs/getting-started-for-web)
* [开始使用 Flutter](https://appwrite.io/docs/getting-started-for-flutter)
* [开始使用 Apple](https://appwrite.io/docs/getting-started-for-apple)
* [开始使用 Android](https://appwrite.io/docs/getting-started-for-android)
* [开始使用 Server](https://appwrite.io/docs/getting-started-for-server)
* [开始使用 CLI](https://appwrite.io/docs/command-line)
### 软件服务
* [**帐户**](https://appwrite.io/docs/client/account) -管理当前用户的帐户和登录方式。跟踪和管理用户 Session登录设备登录方法和查看相关记录。
* [**用户**](https://appwrite.io/docs/server/users) - 在以管理员模式登录时管理和列出所有用户。
* [**团队**](https://appwrite.io/docs/client/teams) - 管理用户分组。邀请成员,管理团队中的用户权限和用户角色。
* [**数据库**](https://appwrite.io/docs/client/database) - 管理数据库文档和文档集。用检索界面来对文档和文档集进行读取,创建,更新,和删除。
* [**贮存**](https://appwrite.io/docs/client/storage) - 管理文件的阅读、创建、删除和预览。设置文件的预览来满足程序的个性化需求。所有文件都由 ClamAV 扫描并安全存储和加密。
* [**云函数**](https://appwrite.io/docs/server/functions) - 在安全隔离的环境中运行自定义代码。这些代码可以被事件CRON或者手动操作触发。
* [**语言适配**](https://appwrite.io/docs/client/locale) - 根据用户所在的的国家和地区做出合适的语言适配。
* [**头像**](https://appwrite.io/docs/client/avatars) -管理用户头像、国家旗帜、浏览器图标、信用卡符号,和生成二维码。
如需完整的 API 界面文档,请访问 [https://appwrite.io/docs](https://appwrite.io/docs)。如需更多教程、新闻和公告,请订阅我们的 [博客](https://medium.com/appwrite-io) 和 加入我们的[Discord 社区](https://discord.gg/GSeTUeA)。
### 开发套件
以下是当前支持的平台和语言列表。如果您想帮助我们为您选择的平台添加支持,您可以访问我们的 [SDK 生成器](https://github.com/appwrite/sdk-generator) 项目并查看我们的 [贡献指南]( https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md
#### 客户端
* ✅ &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (由 Appwrite 团队维护)
* ✅ &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (由 Appwrite 团队维护)
* ✅ &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
* ✅ &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (由 Appwrite 团队维护)
#### 服务器
* ✅ &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (由 Appwrite 团队维护)
* ✅ &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (由 Appwr实验 团队维护)
* ✅ &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) - (由 Appwrite 团队维护)
* ✅ &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) - **公测** (由 Appwrite 团队维护)
* ✅ &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (由 Appwrite 团队维护)
* ✅ &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (由 Appwrite 团队维护)
* ✅ &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) - **公测** (由 Appwrite 团队维护)
* ✅ &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) - **公测** (由 Appwrite 团队维护)
* ✅ &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **公测** (由 Appwrite 团队维护)
#### 开发者社区
* ✅ &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (维护者 [Michael Gangolf](https://github.com/m1ga/))
* ✅ &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (维护者 [fenix-hub @GodotNuts](https://github.com/fenix-hub))
找不到需要的的 SDK - 欢迎通过发起PR来帮助我们完善Appwrite的软件生态环境 [SDK 生成器](https://github.com/appwrite/sdk-generator)!
## 软件架构
![Appwrite 软件架构](docs/specs/overview.drawio.svg)
Appwrite 使用高拓展性的微服务架构。此外Appwrite 支持多种 APIREST、WebSocket 和 即将推出的 GraphQL来迎合您的个性化开发习惯。
Appwrite API 界面层利用后台缓存和任务委派来提供极速的响应时间。后台的 Worker 代理还允许您使用消息队列来处理负载,并精确控制硬件合理分配和成本。您可以在 [贡献指南](CONTRIBUTING.md#architecture-1) 中了解有关我们架构的更多信息。
## 贡献代码
所有代码贡献 - 包括来自具有直接提交更改权限的贡献者 - 都必须提交PR请求并在合并分支之前得到核心开发人员的批准。这是为了确保正确审查所有代码。
我们欢迎所有人提交PR如果您愿意提供帮助可以在 [贡献指南](CONTRIBUTING.md) 中了解有关如何为项目做出贡献的更多信息。
## 安全
为了保护您的隐私请避免在GitHub 上发布安全问题。发送问题至 security@appwrite.io我们将为您做更细致的解答。
## 订阅我们
加入我们在世界各地不断发展的社区!请参阅我们的官方 [博客](https://medium.com/appwrite-io)。在 [Twitter](https://twitter.com/appwrite)、[Facebook 页面](https://www.facebook.com/appwrite.io)、[Facebook 群组](https://www.facebook)、[开发者社区](https://dev.to/appwrite) 等平台订阅我们或加入我们的 [Discord 社区](https://discord.gg/GSeTUeA) 以获得更多帮助,想法和讨论。
## 版权说明
版权详情,访问 [BSD 3-Clause License](./LICENSE)。

View file

@ -9,13 +9,16 @@
</p>
<!-- [![Hacktoberfest](https://img.shields.io/static/v1?label=hacktoberfest&message=friendly&color=90a88b&style=flat-square)](https://hacktoberfest.appwrite.io) -->
<!-- [![Build Status](https://img.shields.io/travis/com/appwrite/appwrite?style=flat-square)](https://travis-ci.com/appwrite/appwrite) -->
[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord?r=Github)
[![Docker Pulls](https://img.shields.io/docker/pulls/appwrite/appwrite?color=f02e65&style=flat-square)](https://hub.docker.com/r/appwrite/appwrite)
[![Build Status](https://img.shields.io/travis/com/appwrite/appwrite?style=flat-square)](https://travis-ci.com/appwrite/appwrite)
[![Build Status](https://img.shields.io/github/workflow/status/appwrite/appwrite/Tests?label=tests&style=flat-square)](https://github.com/appwrite/appwrite/actions)
[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![Translate](https://img.shields.io/badge/translate-f02e65?style=flat-square)](docs/tutorials/add-translations.md)
[![Swag Store](https://img.shields.io/badge/swag%20store-f02e65?style=flat-square)](https://store.appwrite.io)
English | [简体中文](README-CN.md)
[**Appwrite 0.12 has been released! Learn what's new!**](https://dev.to/appwrite/its-here-announcing-the-release-of-appwrite-012-5c8b)
Appwrite is an end-to-end backend server for Web, Mobile, Native, or Backend apps packaged as a set of Docker<nobr> microservices. Appwrite abstracts the complexity and repetitiveness required to build a modern backend API from scratch and allows you to build secure apps faster.
@ -59,7 +62,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:0.12.1
appwrite/appwrite:0.12.2
```
### Windows
@ -71,7 +74,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:0.12.1
appwrite/appwrite:0.12.2
```
#### PowerShell
@ -81,7 +84,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:0.12.1
appwrite/appwrite:0.12.2
```
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

@ -4,11 +4,10 @@
| Version | Supported |
| ------- | ------------------ |
| < 0.5 | :x: |
| 0.6.x | :white_check_mark: |
| 0.7.x | :white_check_mark: |
| 0.8.0 | :white_check_mark: |
| <= 0.10 | :x: |
| 0.11.x | :white_check_mark: |
| 0.12.x | :white_check_mark: |
## Reporting a Vulnerability
For security issues, kindly email us at security@appwrite.io instead of posting a public issue in GitHub.
For security issues, kindly email us at security@appwrite.io instead of posting a public issue in GitHub.

View file

@ -7,35 +7,35 @@ return [
'name' => 'Email/Password',
'key' => 'emailPassword',
'icon' => '/images/users/email.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateSession',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateSession',
'enabled' => true,
],
'magic-url' => [
'name' => 'Magic URL',
'key' => 'usersAuthMagicURL',
'icon' => '/images/users/magic-url.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateMagicURLSession',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateMagicURLSession',
'enabled' => true,
],
'anonymous' => [
'name' => 'Anonymous',
'key' => 'anonymous',
'icon' => '/images/users/anonymous.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateAnonymousSession',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateAnonymousSession',
'enabled' => true,
],
'invites' => [
'name' => 'Invites',
'key' => 'invites',
'icon' => '/images/users/invites.png',
'docs' => 'https://appwrite.io/docs/client/teams?sdk=web#teamsCreateMembership',
'docs' => 'https://appwrite.io/docs/client/teams?sdk=web-default#teamsCreateMembership',
'enabled' => true,
],
'jwt' => [
'name' => 'JWT',
'key' => 'JWT',
'icon' => '/images/users/jwt.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateJWT',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web-default#accountCreateJWT',
'enabled' => true,
],
'phone' => [

View file

@ -99,8 +99,8 @@ $collections = [
'$id' => '_fulltext_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [1024],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -574,8 +574,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -1106,8 +1106,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_deleted_email',
@ -1158,7 +1158,29 @@ $collections = [
'filters' => [],
],
[
'$id' => 'providerToken',
'$id' => 'providerAccessToken',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => 'providerAccessTokenExpiry',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'providerRefreshToken',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
@ -1422,8 +1444,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -1510,6 +1532,17 @@ $collections = [
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => 'search',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
@ -1533,6 +1566,13 @@ $collections = [
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
],
],
@ -1720,8 +1760,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -2023,8 +2063,8 @@ $collections = [
'$id' => '_key_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [2048],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -2296,8 +2336,8 @@ $collections = [
'$id' => '_fulltext_search',
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [16384],
'orders' => [Database::ORDER_ASC],
'lengths' => [],
'orders' => [],
],
],
],
@ -2473,7 +2513,7 @@ $collections = [
],
],
],
'realtime' => [
'$collection' => Database::METADATA,
'$id' => 'realtime',
@ -2525,4 +2565,4 @@ $collections = [
],
];
return $collections;
return $collections;

View file

@ -82,6 +82,11 @@ return [
'model' => Response::MODEL_SESSION,
'note' => '',
],
'account.sessions.update' => [
'description' => 'This event triggers when the account session is updated.',
'model' => Response::MODEL_SESSION,
'note' => '',
],
'database.collections.create' => [
'description' => 'This event triggers when a database collection is created.',
'model' => Response::MODEL_COLLECTION,

View file

@ -116,7 +116,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '0.3.1',
'version' => '0.3.3',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@ -370,7 +370,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '0.2.2',
'version' => '0.2.5',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,

View file

@ -127,7 +127,7 @@ return [ // Ordered by ABC.
'icon' => 'icon-windows',
'enabled' => true,
'sandbox' => false,
'form' => false,
'form' => 'microsoft.phtml',
'beta' => false,
'mock' => false,
],
@ -287,6 +287,16 @@ return [ // Ordered by ABC.
'beta' => false,
'mock' => false
],
'stripe' => [
'name' => 'Stripe',
'developers' => 'https://stripe.com/docs/api',
'icon' => 'icon-stripe',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false
],
// Keep Last
'mock' => [
'name' => 'Mock',

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

@ -152,7 +152,7 @@ return [
],
[
'name' => '_APP_LOGGING_PROVIDER',
'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appsignal\'',
'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appsignal\', \'logowl\'',
'introduction' => '0.12.0',
'default' => '',
'required' => false,
@ -161,7 +161,7 @@ return [
],
[
'name' => '_APP_LOGGING_CONFIG',
'description' => 'This variable configures authentication to 3rd party error logging providers. If using Sentry, this should be \'SENTRY_API_KEY;SENTRY_APP_ID\'. If using Raygun, this should be Raygun API key. If using AppSignal, this should be AppSignal API key.',
'description' => 'This variable configures authentication to 3rd party error logging providers. If using Sentry, this should be \'SENTRY_API_KEY;SENTRY_APP_ID\'. If using Raygun, this should be Raygun API key. If using AppSignal, this should be AppSignal API key. If using LogOwl, this should be LogOwl Service Ticket.',
'introduction' => '0.12.0',
'default' => '',
'required' => false,

View file

@ -3,7 +3,6 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Detector\Detector;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
@ -12,6 +11,7 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Template\Template;
use Appwrite\URL\URL as URLParser;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
@ -23,6 +23,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@ -315,7 +316,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(1024), 'OAuth2 code.')
->param('code', '', new Text(2048), 'OAuth2 code.')
->param('state', '', new Text(2048), 'Login state params.', true)
->inject('request')
->inject('response')
@ -342,7 +343,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(1024), 'OAuth2 code.')
->param('code', '', new Text(2048), 'OAuth2 code.')
->param('state', '', new Text(2048), 'Login state params.', true)
->inject('request')
->inject('response')
@ -370,7 +371,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->label('abuse-key', 'ip:{ip}')
->label('docs', false)
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('code', '', new Text(1024), 'OAuth2 code.')
->param('code', '', new Text(2048), 'OAuth2 code.')
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
->inject('request')
->inject('response')
@ -430,7 +431,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
$state['failure'] = null;
$accessToken = $oauth2->getAccessToken($code);
$refreshToken =$oauth2->getRefreshToken($code);
$accessTokenExpiry = $oauth2->getAccessTokenExpiry($code);
if (empty($accessToken)) {
if (!empty($state['failure'])) {
@ -528,7 +532,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'userId' => $user->getId(),
'provider' => $provider,
'providerUid' => $oauth2ID,
'providerToken' => $accessToken,
'providerAccessToken' => $accessToken,
'providerRefreshToken' => $refreshToken,
'providerAccessTokenExpiry' => \time() + (int) $accessTokenExpiry,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@ -1198,6 +1204,7 @@ App::get('/v1/account/logs')
'account.update.password',
'account.update.prefs',
'account.sessions.create',
'account.sessions.update',
'account.sessions.delete',
'account.recovery.create',
'account.recovery.update',
@ -1593,8 +1600,8 @@ App::delete('/v1/account/sessions/:sessionId')
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
$sessions = $user->getAttribute('sessions', []);
@ -1647,6 +1654,108 @@ App::delete('/v1/account/sessions/:sessionId')
throw new Exception('Session not found', 404);
});
App::patch('/v1/account/sessions/:sessionId')
->desc('Update Session (Refresh Tokens)')
->groups(['api', 'account'])
->label('scope', 'account')
->label('event', 'account.sessions.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateSession')
->label('sdk.description', '/docs/references/account/update-session.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->param('sessionId', null, new UID(), 'Session ID. Use the string \'current\' to update the current device session.')
->inject('request')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('audits')
->inject('events')
->inject('usage')
->action(function ($sessionId, $request, $response, $user, $dbForProject, $project, $locale, $audits, $events, $usage) {
/** @var Appwrite\Utopia\Request $request */
/** @var boolean $force */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForProject */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Stats\Stats $usage */
$sessionId = ($sessionId === 'current')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $key => $session) {/** @var Document $session */
if ($sessionId == $session->getId()) {
// Comment below would skip re-generation if token is still valid
// We decided to not include this because developer can get expiration date from the session
// I kept code in comment because it might become relevant in the future
// $expireAt = (int) $session->getAttribute('providerAccessTokenExpiry');
// if(\time() < $expireAt - 5) { // 5 seconds time-sync and networking gap, to be safe
// return $response->noContent();
// }
$provider = $session->getAttribute('provider');
$refreshToken = $session->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('providers', [])[$provider.'Appid'] ?? '';
$appSecret = $project->getAttribute('providers', [])[$provider.'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\'.\ucfirst($provider);
if (!\class_exists($className)) {
throw new Exception('Provider is not supported', 501);
}
$oauth2 = new $className($appId, $appSecret, '', [], []);
$oauth2->refreshTokens($refreshToken);
$session
->setAttribute('providerAccessToken', $oauth2->getAccessToken(''))
->setAttribute('providerRefreshToken', $oauth2->getRefreshToken(''))
->setAttribute('providerAccessTokenExpiry', \time() + (int) $oauth2->getAccessTokenExpiry(''))
;
$dbForProject->updateDocument('sessions', $sessionId, $session);
$user->setAttribute("sessions", $sessions);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.update')
->setParam('resource', 'user/' . $user->getId())
;
$events
->setParam('eventData', $response->output($session, Response::MODEL_SESSION))
;
$usage
->setParam('users.sessions.update', 1)
->setParam('users.update', 1)
;
return $response->dynamic($session, Response::MODEL_SESSION);
}
}
throw new Exception('Session not found', 404);
});
App::delete('/v1/account/sessions')
->desc('Delete All Account Sessions')
->groups(['api', 'account'])

View file

@ -144,7 +144,7 @@ App::get('/v1/avatars/image')
->label('sdk.description', '/docs/references/avatars/get-image.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->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('height', 400, new Range(0, 2000), 'Resize preview image height, Pass an integer between 0 to 2000.', true)
->inject('response')
@ -213,7 +213,7 @@ App::get('/v1/avatars/favicon')
->label('sdk.description', '/docs/references/avatars/get-favicon.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->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')
->action(function ($url, $response) {
/** @var Appwrite\Utopia\Response $response */

View file

@ -27,10 +27,10 @@ use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Structure as StructureException;
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
@ -166,8 +166,6 @@ App::post('/v1/database/collections')
$collectionId = $collectionId == 'unique()' ? $dbForProject->getId() : $collectionId;
try {
$dbForProject->createCollection('collection_' . $collectionId);
$collection = $dbForProject->createDocument('collections', new Document([
'$id' => $collectionId,
'$read' => $read ?? [], // Collection permissions for collection documents (based on permission model)
@ -179,8 +177,12 @@ App::post('/v1/database/collections')
'name' => $name,
'search' => implode(' ', [$collectionId, $name]),
]));
$dbForProject->createCollection('collection_' . $collectionId);
} catch (DuplicateException $th) {
throw new Exception('Collection already exists', 409);
} catch (LimitException $th) {
throw new Exception('Collection limit exceeded', 400);
}
$audits
@ -2009,14 +2011,18 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId')
$roles = Authorization::getRoles();
if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) {
foreach ($data['$read'] as $read) {
if (!Authorization::isRole($read)) {
throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400);
if(!is_null($read)) {
foreach ($data['$read'] as $read) {
if (!Authorization::isRole($read)) {
throw new Exception('Read permissions must be one of: ('.\implode(', ', $roles).')', 400);
}
}
}
foreach ($data['$write'] as $write) {
if (!Authorization::isRole($write)) {
throw new Exception('Write permissions must be one of: ('.\implode(', ', $roles).')', 400);
if(!is_null($write)) {
foreach ($data['$write'] as $write) {
if (!Authorization::isRole($write)) {
throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400);
}
}
}
}

View file

@ -2,8 +2,8 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Event\Event;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\Database\Validator\UID;
use Utopia\Storage\Storage;
use Utopia\Storage\Validator\File;
@ -50,7 +50,7 @@ App::post('/v1/functions')
->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, 900), 'Function maximum execution time in seconds.', true)
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true)
->inject('response')
->inject('dbForProject')
->action(function ($functionId, $name, $execute, $runtime, $vars, $events, $schedule, $timeout, $response, $dbForProject) {
@ -298,7 +298,7 @@ App::put('/v1/functions/:functionId')
->param('vars', [], new Assoc(), 'Key-value JSON object that will be passed to the function as environment variables.', true)
->param('events', [], new ArrayList(new WhiteList(array_keys(Config::getParam('events')), true)), 'Events list.', true)
->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true)
->param('timeout', 15, new Range(1, 900), 'Maximum execution time in seconds.', true)
->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true)
->inject('response')
->inject('dbForProject')
->inject('project')

View file

@ -2,10 +2,10 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
@ -537,7 +537,7 @@ App::delete('/v1/projects/:projectId')
->inject('deletes')
->action(function ($projectId, $password, $response, $user, $dbForConsole, $deletes) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Document $user */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Appwrite\Event\Event $deletes */
@ -582,7 +582,7 @@ App::post('/v1/projects/:projectId/webhooks')
->param('projectId', null, new UID(), 'Project unique ID.')
->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('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('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)
@ -704,7 +704,7 @@ App::put('/v1/projects/:projectId/webhooks/:webhookId')
->param('webhookId', null, new UID(), 'Webhook unique ID.')
->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('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('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)

View file

@ -1,31 +1,31 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\HexColor;
use Utopia\Cache\Cache;
use Utopia\Cache\Adapter\Filesystem;
use Appwrite\ClamAV\Network;
use Utopia\Config\Config;
use Utopia\Database\Validator\Authorization;
use Appwrite\Database\Validator\CustomId;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Image\Image;
use Utopia\Storage\Storage;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Utopia\Storage\Compression\Algorithms\GZIP;
use Utopia\Image\Image;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\HexColor;
App::post('/v1/storage/files')
->desc('Create File')

View file

@ -1,26 +1,27 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Detector\Detector;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Exception;
use Utopia\Config\Config;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Exception;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
App::post('/v1/teams')
->desc('Create Team')
@ -62,7 +63,9 @@ App::post('/v1/teams')
])));
if (!$isPrivilegedUser && !$isAppUser) { // Don't add user on server mode
$membershipId = $dbForProject->getId();
$membership = new Document([
'$id' => $membershipId,
'$read' => ['user:'.$user->getId(), 'team:'.$team->getId()],
'$write' => ['user:'.$user->getId(), 'team:'.$team->getId().'/owner'],
'userId' => $user->getId(),
@ -72,6 +75,7 @@ App::post('/v1/teams')
'joined' => \time(),
'confirm' => true,
'secret' => '',
'search' => implode(' ', [$membershipId, $user->getId()])
]);
$membership = $dbForProject->createDocument('memberships', $membership);
@ -352,8 +356,9 @@ App::post('/v1/teams/:teamId/memberships')
$secret = Auth::tokenGenerator();
$membershipId = $dbForProject->getId();
$membership = new Document([
'$id' => $dbForProject->getId(),
'$id' => $membershipId,
'$read' => ['role:all'],
'$write' => ['user:'.$invitee->getId(), 'team:'.$team->getId().'/owner'],
'userId' => $invitee->getId(),
@ -363,6 +368,7 @@ App::post('/v1/teams/:teamId/memberships')
'joined' => ($isPrivilegedUser || $isAppUser) ? \time() : 0,
'confirm' => ($isPrivilegedUser || $isAppUser),
'secret' => Auth::hash($secret),
'search' => implode(' ', [$membershipId, $invitee->getId()])
]);
if ($isPrivilegedUser || $isAppUser) { // Allow admin to create membership
@ -457,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);
$sum = $dbForProject->count('memberships', [new Query('teamId', Query::TYPE_EQUAL, [$teamId])], APP_LIMIT_COUNT);
$queries = [new Query('teamId', Query::TYPE_EQUAL, [$teamId])];
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')));
@ -564,25 +589,40 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
$isPrivilegedUser = Auth::isPrivilegedUser(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)
throw new Exception('User is not allowed to modify roles', 401);
}
// Update the roles
/**
* Update the roles
*/
$membership->setAttribute('roles', $roles);
$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
->setParam('userId', $user->getId())
->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')
@ -761,7 +801,11 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
throw new Exception('Team not found', 404);
}
if (!$dbForProject->deleteDocument('memberships', $membership->getId())) {
try {
$dbForProject->deleteDocument('memberships', $membership->getId());
} catch (AuthorizationException $exception) {
throw new Exception('Unauthorized permissions', 401);
} catch (\Exception $exception) {
throw new Exception('Failed to remove membership from DB', 500);
}
@ -782,7 +826,7 @@ App::delete('/v1/teams/:teamId/memberships/:membershipId')
if ($membership->getAttribute('confirm')) { // Count only confirmed members
$team->setAttribute('sum', \max($team->getAttribute('sum', 0) - 1, 0));
$team = $dbForProject->updateDocument('teams', $team->getId(), $team);
Authorization::skip(fn() => $dbForProject->updateDocument('teams', $team->getId(), $team));
}
$audits

View file

@ -2,25 +2,25 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Detector\Detector;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Exception;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Exception;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\UID;
use Appwrite\Detector\Detector;
use Appwrite\Database\Validator\CustomId;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
App::post('/v1/users')
->desc('Create User')
@ -289,6 +289,7 @@ App::get('/v1/users/:userId/logs')
'account.update.password',
'account.update.prefs',
'account.sessions.create',
'account.sessions.update',
'account.sessions.delete',
'account.recovery.create',
'account.recovery.update',

View file

@ -13,15 +13,13 @@ use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Appwrite\Auth\Auth;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Response\Filters\V06;
use Appwrite\Utopia\Response\Filters\V07;
use Appwrite\Utopia\Response\Filters\V08;
use Appwrite\Utopia\Response\Filters\V11;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Appwrite\Utopia\Request\Filters\V12;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
@ -159,15 +157,6 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
$responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
switch($responseFormat) {
case version_compare ($responseFormat , '0.6.2', '<=') :
Response::setFilter(new V06());
break;
case version_compare ($responseFormat , '0.7.2', '<=') :
Response::setFilter(new V07());
break;
case version_compare ($responseFormat , '0.8.0', '<=') :
Response::setFilter(new V08());
break;
case version_compare ($responseFormat , '0.11.0', '<=') :
Response::setFilter(new V11());
break;
@ -337,8 +326,8 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
if($logger) {
if($error->getCode() >= 500 || $error->getCode() === 0) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
/** @var Appwrite\Database\Document $user */
} catch(\Throwable $th) {
// All good, user is optional information for logger
}
@ -525,8 +514,25 @@ App::get('/.well-known/acme-challenge')
->inject('request')
->inject('response')
->action(function ($request, $response) {
$uriChunks = \explode('/', $request->getURI());
$token = $uriChunks[\count($uriChunks) - 1];
$validator = new Text(100, [
...Text::NUMBERS,
...Text::ALPHABET_LOWER,
...Text::ALPHABET_UPPER,
'-',
'_'
]);
if (!$validator->isValid($token) || \count($uriChunks) !== 4) {
throw new Exception('Invalid challenge token.', 400);
}
$filePath = '/.well-known/acme-challenge' . $token;
$base = \realpath(APP_STORAGE_CERTIFICATES);
$path = \str_replace('/.well-known/acme-challenge/', '', $request->getURI());
$path = \str_replace('/.well-known/acme-challenge/', '', $filePath);
$absolute = \realpath($base.'/.well-known/acme-challenge/'.$path);
if (!$base) {

View file

@ -10,6 +10,7 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Storage\Validator\File;
use Utopia\Validator\WhiteList;
App::get('/v1/mock/tests/foo')
->desc('Get Foo')
@ -478,11 +479,13 @@ App::get('/v1/mock/tests/general/oauth2/token')
->label('docs', false)
->label('sdk.mock', true)
->param('client_id', '', new Text(100), 'OAuth2 Client ID.')
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.')
->param('client_secret', '', new Text(100), 'OAuth2 scope list.')
->param('code', '', new Text(100), 'OAuth2 state.')
->param('grant_type', 'authorization_code', new WhiteList(['refresh_token', 'authorization_code']), 'OAuth2 Grant Type.', true)
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.', true)
->param('code', '', new Text(100), 'OAuth2 state.', true)
->param('refresh_token', '', new Text(100), 'OAuth2 refresh token.', true)
->inject('response')
->action(function ($client_id, $redirectURI, $client_secret, $code, $response) {
->action(function ($client_id, $client_secret, $grantType, $redirectURI, $code, $refreshToken, $response) {
/** @var Appwrite\Utopia\Response $response */
if ($client_id != '1') {
@ -493,11 +496,27 @@ App::get('/v1/mock/tests/general/oauth2/token')
throw new Exception('Invalid client secret');
}
if ($code != 'abcdef') {
throw new Exception('Invalid token');
}
$responseJson = [
'access_token' => '123456',
'refresh_token' => 'tuvwxyz',
'expires_in' => 14400
];
$response->json(['access_token' => '123456']);
if($grantType === 'authorization_code') {
if ($code !== 'abcdef') {
throw new Exception('Invalid token');
}
$response->json($responseJson);
} else if($grantType === 'refresh_token') {
if ($refreshToken !== 'tuvwxyz') {
throw new Exception('Invalid refresh token');
}
$response->json($responseJson);
} else {
throw new Exception('Invalid grant type');
}
});
App::get('/v1/mock/tests/general/oauth2/user')
@ -517,7 +536,7 @@ App::get('/v1/mock/tests/general/oauth2/user')
$response->json([
'id' => 1,
'name' => 'User Name',
'email' => 'user@localhost.test',
'email' => 'useroauth@localhost.test',
]);
});

View file

@ -1,13 +1,13 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Messaging\Adapter\Realtime;
use Utopia\App;
use Utopia\Exception;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Exception;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;

View file

@ -193,8 +193,8 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$logger = $app->getResource("logger");
if($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->getResource('user');
/** @var Appwrite\Database\Document $user */
} catch(\Throwable $_th) {
// All good, user is optional information for logger
}

View file

@ -21,7 +21,6 @@ use Appwrite\Extend\PDO;
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database as DatabaseOld;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
@ -62,7 +61,7 @@ const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10000;
const APP_CACHE_BUSTER = 201;
const APP_VERSION_STABLE = '0.12.1';
const APP_VERSION_STABLE = '0.13.0';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@ -87,6 +86,9 @@ const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
// Database Worker Types
const DATABASE_TYPE_CREATE_ATTRIBUTE = 'createAttribute';
const DATABASE_TYPE_CREATE_INDEX = 'createIndex';
@ -160,43 +162,6 @@ if(!empty($user) || !empty($pass)) {
Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '').':'.App::getEnv('_APP_REDIS_PORT', ''));
}
/**
* Old DB Filters
*/
DatabaseOld::addFilter('json',
function($value) {
if(!is_array($value)) {
return $value;
}
return json_encode($value);
},
function($value) {
return json_decode($value, true);
}
);
DatabaseOld::addFilter('encrypt',
function($value) {
$key = App::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$tag = null;
return json_encode([
'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
'iv' => bin2hex($iv),
'tag' => bin2hex($tag),
'version' => '1',
]);
},
function($value) {
$value = json_decode($value, true);
$key = App::getEnv('_APP_OPENSSL_KEY_V'.$value['version']);
return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag']));
}
);
/**
* New DB Filters
*/
@ -493,11 +458,12 @@ $register->set('geodb', function () {
});
$register->set('db', function () { // This is usually for our workers or CLI commands scope
$dbHost = App::getEnv('_APP_DB_HOST', '');
$dbPort = App::getEnv('_APP_DB_PORT', '');
$dbUser = App::getEnv('_APP_DB_USER', '');
$dbPass = App::getEnv('_APP_DB_PASS', '');
$dbScheme = App::getEnv('_APP_DB_SCHEMA', '');
$pdo = new PDO("mysql:host={$dbHost};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
$pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
PDO::ATTR_TIMEOUT => 3, // Seconds
PDO::ATTR_PERSISTENT => true,

View file

@ -91,13 +91,32 @@ $server->error($logError);
function getDatabase(Registry &$register, string $namespace)
{
$db = $register->get('dbPool')->get();
$redis = $register->get('redisPool')->get();
$attempts = 0;
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace);
do {
try {
$attempts++;
$db = $register->get('dbPool')->get();
$redis = $register->get('redisPool')->get();
$cache = new Cache(new RedisCache($redis));
$database = new Database(new MariaDB($db), $cache);
$database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$database->setNamespace($namespace);
if (!$database->exists($database->getDefaultDatabase(), 'realtime')) {
throw new Exception('Collection not ready');
}
break; // leave loop if successful
} catch(\Exception $e) {
Console::warning("Database not ready. Retrying connection ({$attempts})...");
if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) {
throw new \Exception('Failed to connect to database: '. $e->getMessage());
}
sleep(DATABASE_RECONNECT_SLEEP);
}
} while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS);
return [
$database,
@ -106,6 +125,7 @@ function getDatabase(Registry &$register, string $namespace)
$register->get('redisPool')->put($redis);
}
];
};
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument, $logError) {

View file

@ -1,18 +1,17 @@
<?php
global $cli, $register, $projectDB, $console;
global $cli, $register;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
use Appwrite\Database\Adapter\Redis as RedisAdapter;
use Appwrite\Migration\Migration;
use Utopia\App;
use Utopia\Cache\Cache;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Text;
Config::load('collections.old', __DIR__ . '/../config/collections.old.php');
$cli
->task('migrate')
->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true)
@ -23,86 +22,36 @@ $cli
Console::exit(1);
return;
}
$options = [];
if (str_starts_with($version, '0.12.')) {
Console::error('--------------------');
Console::error('WARNING');
Console::error('--------------------');
Console::warning('Migrating to Version 0.12.x introduces a major breaking change within the Database Service!');
Console::warning('Before migrating, please read about the breaking changes here:');
Console::info('https://dev.to/appwrite/appwrite-012-migration-post-3cha');
$confirm = Console::confirm("If you want to proceed, type 'yes':");
if ($confirm != 'yes') {
Console::exit(1);
return;
}
Console::log('');
Console::log('Collections');
Console::log('--------------------');
Console::warning('Be aware that following actions will happen during the migration:');
Console::warning('- Nested Document rules will be migrated to String attributes');
Console::warning('- Numeric rules will be migrated to float attributes');
Console::warning('- Wildcard and Markdown rules will be converted to string attributes');
Console::info("Do you want to migrate your Database Collections?");
$options['migrateCollections'] = Console::confirm("Type 'yes' or 'no':");
if ($options['migrateCollections'] === 'yes') {
Console::log('');
Console::log('Documents');
Console::log('------------------');
Console::warning('Be aware that following actions will happen during the migration:');
Console::warning('- Nested Documents will be stored as JSON values');
Console::warning('- All Numeric values will be converted to float');
Console::warning('- All Wildcard and Markdown values will be converted to string');
Console::info("Do you want to migrate your Database Documents?");
$options['migrateDocuments'] = Console::confirm("Type 'yes' or 'no':");
} else {
$options['migrateDocuments'] = 'no';
}
if (
!in_array($options['migrateDocuments'], ['yes', 'no'])
|| !in_array($options['migrateCollections'], ['yes', 'no'])
) {
Console::error("You must reply with 'yes' or 'no'!");
Console::exit(1);
return;
}
}
Config::load('collectionsold', __DIR__ . '/../config/collections.old.php');
$app = new App('UTC');
Console::success('Starting Data Migration to version ' . $version);
$db = $register->get('db', true);
$cache = $register->get('cache', true);
$cache->flushAll();
$consoleDB = new Database();
$consoleDB
->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache))
->setNamespace('app_console') // Main DB
->setMocks(Config::getParam('collectionsold', []));
$cache = new Cache(new RedisCache($cache));
$projectDB = new Database();
$projectDB
->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache))
->setMocks(Config::getParam('collectionsold', []));
$projectDB = new Database(new MariaDB($db), $cache);
$projectDB->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$console = $consoleDB->getDocument('console');
$consoleDB = new Database(new MariaDB($db), $cache);
$consoleDB->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$consoleDB->setNamespace('_project_console');
$console = $app->getResource('console');
$limit = 30;
$sum = 30;
$offset = 0;
$projects = [$console];
$count = 0;
$totalProjects = $consoleDB->count('projects') + 1;
$class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version];
$migration = new $class($register->get('db'), $register->get('cache'), $options);
$migration = new $class();
while ($sum > 0) {
while (!empty($projects)) {
foreach ($projects as $project) {
try {
$migration
@ -114,23 +63,14 @@ $cli
}
}
$projects = $consoleDB->getCollection([
'limit' => $limit,
'offset' => $offset,
'filters' => [
'$collection=' . Database::SYSTEM_COLLECTION_PROJECTS,
],
]);
$sum = \count($projects);
$projects = $consoleDB->find('projects', limit: $limit, offset: $offset);
$offset = $offset + $limit;
$count = $count + $sum;
if ($sum > 0) {
Console::log('Fetched ' . $count . '/' . $consoleDB->getSum() . ' projects...');
}
Console::log('Migrated ' . $count . '/' . $totalProjects . ' projects...');
}
$cache->flushAll();
Swoole\Event::wait(); // Wait for Coroutines to finish
Console::success('Data Migration Completed');

View file

@ -527,15 +527,6 @@ $logs = $this->getParam('logs', null);
<hr class="margin-top-small" />
<div class="row">
<div class="col span-1"><input name="permission" value="document" type="radio" class="margin-top-no" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-11">
<b>Document Level</b>
<p class="text-fade margin-top-tiny">With Document Level permissions, you have granular access control over every document. Users will only be able to access documents for which they have explicit permissions.</p>
<p class="text-fade margin-top-tiny">In this permission level, document permissions take precedence and collection permissions are ignored.</p>
</div>
</div>
<div class="row">
<div class="col span-1"><input name="permission" value="collection" type="radio" class="margin-top-tiny" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-11">
@ -554,6 +545,15 @@ $logs = $this->getParam('logs', null);
</div>
</div>
<div class="row">
<div class="col span-1"><input name="permission" value="document" type="radio" class="margin-top-no" data-ls-bind="{{project-collection.permission}}" /></div>
<div class="col span-11">
<b>Document Level</b>
<p class="text-fade margin-top-tiny">With Document Level permissions, you have granular access control over every document. Users will only be able to access documents for which they have explicit permissions.</p>
<p class="text-fade margin-top-tiny">In this permission level, document permissions take precedence and collection permissions are ignored.</p>
</div>
</div>
<hr class="margin-top-no" />
<button>Update</button>
@ -880,10 +880,10 @@ $logs = $this->getParam('logs', null);
<div class="margin-bottom-large">
<template x-if="!(array || required)">
<input name="xdefault" class="button switch" type="checkbox" />
<input name="xdefault" class="button switch" type="checkbox" />
</template>
<template x-if="(array || required)">
<input name="xdefault" class="button switch" type="checkbox" disabled />
<input name="" class="button switch" type="checkbox" disabled />
</template>
&nbsp; Default Value <span class="tooltip" data-tooltip="Whether this attribute is set to true or false on creation"><i class="icon-info-circled"></i></span>
</div>

View file

@ -169,8 +169,11 @@ $logs = $this->getParam('logs', null);
</template>
<template x-if="attr.format === 'enum'">
<select
:required="attr.required"
:name="attr.key"
data-cast-to="string">
<option :disabled="attr.required" selected label=" "></option>
<template x-for="element in attr.elements">
<option
:value="element"
@ -261,9 +264,12 @@ $logs = $this->getParam('logs', null);
</template>
<template x-if="attr.format === 'enum'">
<select
:required="attr.required"
:name="attr.key"
data-cast-to="string">
<template x-for="element in attr.elements">
<option :disabled="attr.required" selected label=" "></option>
<option
:value="element"
x-text="element"

View file

@ -108,7 +108,7 @@
<label for="collection-name">Name</label>
<input type="text" class="full-width" id="collection-name" name="name" required autocomplete="off" maxlength="128" />
<input type="hidden" id="collection-permission" name="permission" required value="document" />
<input type="hidden" id="collection-permission" name="permission" required value="collection" />
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />

View file

@ -137,7 +137,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true);
</div>
</div>
<p class="margin-top-small"><i class="icon-info-circled"></i> Data is aggregated and updated every 15 minutes</p>
<p class="margin-top-small text-size-small"><i class="icon-info-circled"></i> Data is aggregated and updated every 15 minutes</p>
</div>
</div>
</div>

View file

@ -1,4 +1,6 @@
<?php
use Appwrite\Utopia\View;
$providers = $this->getParam('providers', []);
$auth = $this->getParam('auth', []);
$smtpEnabled = $this->getParam('smtpEnabled', false);
@ -475,9 +477,12 @@ $smtpEnabled = $this->getParam('smtpEnabled', false);
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret">App Secret</label>
<input name="secret" data-forms-show-secret id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="password" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}">
<?php else: ?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Bundle ID <span class="tooltip" data-tooltip="Attribute internal display name"><i class="icon-info-circled"></i></span></label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="com.company.appname" />
<input name="secret" data-forms-oauth-apple id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />
<?php
$form = new View(__DIR__.'/oauth/'.$this->escape($form));
echo $form
->setParam("provider", $provider)
->render();
?>
<?php endif; ?>
<div class="info row thin margin-bottom margin-top">

View file

@ -0,0 +1,13 @@
<?php
$provider = $this->getParam('provider', '');
?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Bundle ID <span class="tooltip" data-tooltip="Attribute internal display name"><i class="icon-info-circled"></i></span></label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="com.company.appname" />
<input name="secret" data-forms-oauth-custom="<?php echo $this->escape(ucfirst($provider)); ?>" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />
<div>
<div class="row thin">
<div class="col span-6"><label>Key ID</label><input id="oauth2AppleKeyId" type="text" placeholder="SHAB13ROFN"></div>
<div class="col span-6"><label>Team ID</label><input id="oauth2AppleTeamId" type="text" placeholder="ELA2CD3AED"></div>
</div><label>P8 File</label><textarea id="oauth2AppleP8" class="margin-bottom-no"></textarea>
</div>

View file

@ -0,0 +1,12 @@
<?php
$provider = $this->getParam('provider', '');
?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Application (Client) ID<span class="tooltip" data-tooltip="Provided by AzureAD"><i class="icon-info-circled"></i></span></label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="Application ID" />
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>ClientSecret">Client Secret <span class="tooltip" data-tooltip="Created by you in AzureAD Portal"><i class="icon-info-circled"></i></span></label>
<input name="appSecret" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>ClientSecret" type="password" autocomplete="off" placeholder="Client Secret" />
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>TenantId">Target Tenant<span class="tooltip" data-tooltip="'common', 'organizations', 'consumers' or your TenantId"><i class="icon-info-circled"></i></span><a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints">More info</a></label>
<input name="appSecret" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>TenantId" type="text" autocomplete="off" placeholder="'common', 'organizations', 'consumers' or your TenantId" />
<?php /*Hidden input for the final secret. Gets filled with a JSON via JS. */ ?>
<input name="secret" data-forms-oauth-custom="<?php echo $this->escape(ucfirst($provider)); ?>" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />

View file

@ -475,6 +475,10 @@ services:
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_AGGREGATION_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
appwrite-schedule:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>

View file

@ -71,7 +71,7 @@
"slickdeals/statsd": "3.1.0"
},
"require-dev": {
"appwrite/sdk-generator": "0.17.1",
"appwrite/sdk-generator": "0.17.2",
"phpunit/phpunit": "9.5.10",
"swoole/ide-helper": "4.8.5",
"textalk/websocket": "1.5.5",

24
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": "e28c5946b102f637cc247b026f42ae0e",
"content-hash": "70ce232aec13929c9cc7901fa34282a0",
"packages": [
{
"name": "adhocore/jwt",
@ -3077,16 +3077,16 @@
},
{
"name": "appwrite/sdk-generator",
"version": "0.17.1",
"version": "0.17.2",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "3542c6ed0f808b6a9f6735a8aad7ccda961bea29"
"reference": "37bc6fc1b4b4940c7659748d7d2d5110da5457a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/3542c6ed0f808b6a9f6735a8aad7ccda961bea29",
"reference": "3542c6ed0f808b6a9f6735a8aad7ccda961bea29",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/37bc6fc1b4b4940c7659748d7d2d5110da5457a4",
"reference": "37bc6fc1b4b4940c7659748d7d2d5110da5457a4",
"shasum": ""
},
"require": {
@ -3120,9 +3120,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.17.1"
"source": "https://github.com/appwrite/sdk-generator/tree/0.17.2"
},
"time": "2022-01-07T12:55:37+00:00"
"time": "2022-01-28T08:25:10+00:00"
},
{
"name": "composer/pcre",
@ -5213,16 +5213,16 @@
},
{
"name": "sebastian/global-state",
"version": "5.0.5",
"version": "5.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2"
"reference": "19c519631c5a511b7ed0ad64a6713fdb3fd25fe4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2",
"reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/19c519631c5a511b7ed0ad64a6713fdb3fd25fe4",
"reference": "19c519631c5a511b7ed0ad64a6713fdb3fd25fe4",
"shasum": ""
},
"require": {
@ -5273,7 +5273,7 @@
"type": "github"
}
],
"time": "2022-02-14T08:28:10+00:00"
"time": "2022-02-10T07:01:19+00:00"
},
{
"name": "sebastian/lines-of-code",

View file

@ -497,6 +497,10 @@ services:
- _APP_INFLUXDB_HOST
- _APP_INFLUXDB_PORT
- _APP_USAGE_SYNC_INTERVAL
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
appwrite-schedule:
entrypoint: schedule

View file

@ -81,6 +81,23 @@ class [PROVIDER NAME] extends OAuth2
*/
private $endpoint = '[ENDPOINT API URL]';
/**
* @var array
*/
protected $scopes = [
// [ARRAY_OF_REQUIRED_SCOPES]
];
/**
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
*/
@ -101,14 +118,31 @@ class [PROVIDER NAME] extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
// TODO: Fire request to oauth API to generate access_token
$accessToken = "[FETCHED ACCESS TOKEN]";
return $accessToken;
if(empty($this->tokens)) {
// TODO: Fire request to oauth API to generate access_token
// Make sure to use '$this->getScopes()' to include all scopes properly
$this->tokens = "[FETCH TOKEN RESPONSE]";
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
// TODO: Fire request to oauth API to generate access_token using refresh token
$this->tokens = "[FETCH TOKEN RESPONSE]";
return $this->tokens;
}
/**
@ -118,8 +152,10 @@ class [PROVIDER NAME] extends OAuth2
*/
public function getUserID(string $accessToken): string
{
// TODO: Fetch user from oauth API and select the user ID
$userId = "[FETCHED USER ID]";
$user = $this->getUser($accessToken);
// TODO: Pick user ID from $user response
$userId = "[USER ID]";
return $userId;
}
@ -131,8 +167,10 @@ class [PROVIDER NAME] extends OAuth2
*/
public function getUserEmail(string $accessToken): string
{
// TODO: Fetch user from oauth API and select the user's email
$userEmail = "[FETCHED USER EMAIL]";
$user = $this->getUser($accessToken);
// TODO: Pick user email from $user response
$userEmail = "[USER EMAIL]";
return $userEmail;
}
@ -144,16 +182,35 @@ class [PROVIDER NAME] extends OAuth2
*/
public function getUserName(string $accessToken): string
{
// TODO: Fetch user from oauth API and select the username
$username = "[FETCHED USERNAME]";
$user = $this->getUser($accessToken);
// TODO: Pick username from $user response
$username = "[USERNAME]";
return $username;
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken)
{
if (empty($this->user)) {
// TODO: Fire request to oauth API to get information about users
$this->user = "[FETCH USER RESPONSE]";
}
return $this->user;
}
}
```
> If you copy this template, make sure to replace all placeholders wrapped like `[THIS]` and to implement everything marked as `TODO:`.
> If your OAuth2 provider has different endpoints for getting username/email/id, you can fire specific requests from specific get-method, and stop using `getUser` method.
Please mention in your documentation what resources or API docs you used to implement the provider's OAuth2 protocol.
## 3. Test your provider
@ -175,4 +232,34 @@ If everything goes well, raise a pull request and be ready to respond to any fee
First of all, commit the changes with the message `Added XXX OAuth2 Provider` and push it. This will publish a new branch to your forked version of Appwrite. If you visit it at `github.com/YOUR_USERNAME/appwrite`, you will see a new alert saying you are ready to submit a pull request. Follow the steps GitHub provides, and at the end, you will have your pull request submitted.
## 🤕 Stuck ?
If you need any help with the contribution, feel free to head over to [our discord channel](https://appwrite.io/discord) and we'll be happy to help you out.
## 😉 Need more freedom
If your OAuth provider requires special configuration apart from `clientId` and `clientSecret` you can create a custom form. Currently this is being realized through putting all custom fields as JSON into the `clientSecret` field to keep the project API stable. You can implement your custom form following these steps:
1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/providers.php`.
```php
<?php
$provider = $this->getParam('provider', '');
?>
<label for="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid">Application (Client) ID<span class="tooltip" data-tooltip="Provided by AzureAD"><i class="icon-info-circled"></i></span></label>
<input name="appId" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Appid" type="text" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Appid}}" placeholder="Application ID" />
<?php /*Hidden input for the final secret. Gets filled with a JSON via JS. */ ?>
<input name="secret" data-forms-oauth-custom="<?php echo $this->escape(ucfirst($provider)); ?>" id="oauth2<?php echo $this->escape(ucfirst($provider)); ?>Secret" type="hidden" autocomplete="off" data-ls-bind="{{console-project.provider<?php echo $this->escape(ucfirst($provider)); ?>Secret}}" />
<!-- [Your custom form inputs go here] -->
```
2. Add the config for creating the JSON in `public/scripts/views/forms/oauth-custom.js` using this template
```js
{
"[Provider]":{
"[JSON property name 1]":"[html element Id 1]",
"[JSON property name 2]":"[html element Id 2]"
}
}
```
3. In your provider class `src/Appwrite/Auth/OAuth2/[Provider].php` add logic to decode the JSON using the same property names.

View file

@ -57,7 +57,7 @@ const configApp = {
'public/scripts/views/forms/move-down.js',
'public/scripts/views/forms/move-up.js',
'public/scripts/views/forms/nav.js',
'public/scripts/views/forms/oauth-apple.js',
'public/scripts/views/forms/oauth-custom.js',
'public/scripts/views/forms/password-meter.js',
'public/scripts/views/forms/pell.js',
'public/scripts/views/forms/required.js',

17
package-lock.json generated
View file

@ -12,7 +12,7 @@
"chart.js": "^3.7.0",
"markdown-it": "^12.3.2",
"pell": "^1.0.6",
"prismjs": "^1.25.0",
"prismjs": "^1.26.0",
"turndown": "^7.1.1"
},
"devDependencies": {
@ -3566,9 +3566,12 @@
}
},
"node_modules/prismjs": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.26.0.tgz",
"integrity": "sha512-HUoH9C5Z3jKkl3UunCyiD5jwk0+Hz0fIgQ2nbwU2Oo/ceuTAQAg+pPVnfdt2TJWRVLcxKh9iuoYDUSc8clb5UQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
@ -7978,9 +7981,9 @@
"dev": true
},
"prismjs": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.26.0.tgz",
"integrity": "sha512-HUoH9C5Z3jKkl3UunCyiD5jwk0+Hz0fIgQ2nbwU2Oo/ceuTAQAg+pPVnfdt2TJWRVLcxKh9iuoYDUSc8clb5UQ=="
},
"process-nextick-args": {
"version": "2.0.1",

View file

@ -20,7 +20,7 @@
"chart.js": "^3.7.0",
"markdown-it": "^12.3.2",
"pell": "^1.0.6",
"prismjs": "^1.25.0",
"prismjs": "^1.26.0",
"turndown": "^7.1.1"
}
}

View file

@ -3776,10 +3776,14 @@ list["filters-"+filter.key]=params[key][i];}}}}
return list;};let apply=function(params){let cached=container.get(name);cached=cached?cached.params:[];params=Object.assign(cached,params);container.set(name,{name:name,params:params,query:serialize(params),forward:parseInt(params.offset)+parseInt(params.limit),backward:parseInt(params.offset)-parseInt(params.limit),keys:flatten(params)},true,name);document.dispatchEvent(new CustomEvent(name+"-changed",{bubbles:false,cancelable:true}));};switch(element.tagName){case"INPUT":break;case"TEXTAREA":break;case"BUTTON":element.addEventListener("click",function(){apply(JSON.parse(expression.parse(element.dataset["params"]||"{}")));});break;case"FORM":element.addEventListener("input",function(){apply(form.toJson(element));});element.addEventListener("change",function(){apply(form.toJson(element));});element.addEventListener("reset",function(){setTimeout(function(){apply(form.toJson(element));},0);});events=events.trim().split(",");for(let y=0;y<events.length;y++){if(events[y]==="init"){element.addEventListener("rendered",function(){apply(form.toJson(element));},{once:true});}else{}
element.setAttribute("data-event","none");}
break;default:break;}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-headers",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.value=key.value.toLowerCase()+":"+value.value.toLowerCase();};let syncB=function(){let split=element.value.toLowerCase().split(":");key.value=split[0]||"";value.value=split[1]||"";key.value=key.value.trim();value.value=value.value.trim();};syncB();}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-key-value",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";key.required=true;value.type="text";value.className="margin-bottom-no";value.placeholder="Value";value.required=true;wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.name=key.value;element.value=value.value;};let syncB=function(){key.value=element.name||"";value.value=element.value||"";};syncB();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-down",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-down]")).map(function(obj){obj.addEventListener("click",function(){if(element.nextElementSibling){console.log('down',element.offsetHeight);element.parentNode.insertBefore(element.nextElementSibling,element);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-up",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-up]")).map(function(obj){obj.addEventListener("click",function(){if(element.previousElementSibling){console.log('up',element);element.parentNode.insertBefore(element,element.previousElementSibling);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-nav",repeat:false,controller:function(element,view,container,document){let titles=document.querySelectorAll('[data-forms-nav-anchor]');let links=element.querySelectorAll('[data-forms-nav-link]');let minLink=null;let check=function(){let minDistance=null;let minElement=null;for(let i=0;i<titles.length;++i){let title=titles[i];let distance=title.getBoundingClientRect().top;console.log(i);if((minDistance===null||minDistance>=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');}
console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-apple",controller:function(element){let container=document.createElement("div");let row=document.createElement("div");let col1=document.createElement("div");let col2=document.createElement("div");let keyID=document.createElement("input");let keyLabel=document.createElement("label");let teamID=document.createElement("input");let teamLabel=document.createElement("label");let p8=document.createElement("textarea");let p8Label=document.createElement("label");keyLabel.textContent='Key ID';teamLabel.textContent='Team ID';p8Label.textContent='P8 File';row.classList.add('row');row.classList.add('thin');container.appendChild(row);container.appendChild(p8Label);container.appendChild(p8);row.appendChild(col1);row.appendChild(col2);col1.classList.add('col');col1.classList.add('span-6');col1.appendChild(keyLabel);col1.appendChild(keyID);col2.classList.add('col');col2.classList.add('span-6');col2.appendChild(teamLabel);col2.appendChild(teamID);keyID.type='text';keyID.placeholder='SHAB13ROFN';teamID.type='text';teamID.placeholder='ELA2CD3AED';p8.accept='.p8';p8.classList.add('margin-bottom-no');element.parentNode.insertBefore(container,element.nextSibling);element.addEventListener('change',sync);keyID.addEventListener('change',update);teamID.addEventListener('change',update);p8.addEventListener('change',update);function update(){let json={};json.keyID=keyID.value;json.teamID=teamID.value;json.p8=p8.value;element.value=JSON.stringify(json);}
console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantId":"oauth2MicrosoftTenantId"},"Apple":{"keyId":"oauth2AppleKeyId","teamId":"oauth2AppleTeamId","p8":"oauth2AppleP8"}}
let provider=element.getAttribute("data-forms-oauth-custom");if(!provider||!providers.hasOwnProperty(provider)){console.error("Provider for custom form not set or unkown")}
let config=providers[provider];element.addEventListener('change',sync);let elements={};for(const key in config){if(Object.hasOwnProperty.call(config,key)){elements[key]=document.getElementById(config[key]);elements[key].addEventListener('change',update);}}
function update(){let json={};for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){json[key]=elements[key].value}}
element.value=JSON.stringify(json);}
function sync(){if(!element.value){return;}
let json={};try{json=JSON.parse(element.value);}catch(error){console.error('Failed to parse secret key');}
teamID.value=json.teamID||'';keyID.value=json.keyID||'';p8.value=json.p8||'';}
for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){elements[key].value=json[key]||'';}}}
sync();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-password-meter",controller:function(element,window){var calc=function(password){var score=0;if(!password)return score;var letters=new window.Object();for(var i=0;i<password.length;i++){letters[password[i]]=(letters[password[i]]||0)+1;score+=5.0/letters[password[i]];}
var variations={digits:/\d/.test(password),lower:/[a-z]/.test(password),upper:/[A-Z]/.test(password),nonWords:/\W/.test(password)};var variationCount=0;for(var check in variations){if(variations.hasOwnProperty(check)){variationCount+=variations[check]===true?1:0;}}
score+=(variationCount-1)*10;return parseInt(score);};var callback=function(){var score=calc(this.value);if(""===this.value)return(meter.className="password-meter");if(score>60)return(meter.className="password-meter strong");if(score>30)return(meter.className="password-meter medium");if(score>=0)return(meter.className="password-meter weak");};var meter=window.document.createElement("div");meter.className="password-meter";element.parentNode.insertBefore(meter,element.nextSibling);element.addEventListener("change",callback);element.addEventListener("keypress",callback);element.addEventListener("keyup",callback);element.addEventListener("keydown",callback);}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-pell",controller:function(element,window,document,markdown,rtl){var div=document.createElement("div");element.className="pell hide";div.className="input pell";element.parentNode.insertBefore(div,element);element.tabIndex=-1;var turndownService=new TurndownService();turndownService.addRule("underline",{filter:["u"],replacement:function(content){return"__"+content+"__";}});var editor=window.pell.init({element:div,onChange:function onChange(html){alignText();element.value=turndownService.turndown(html);},defaultParagraphSeparator:"p",actions:[{name:"bold",icon:'<i class="icon-bold"></i>'},{name:"underline",icon:'<i class="icon-underline"></i>'},{name:"italic",icon:'<i class="icon-italic"></i>'},{name:"olist",icon:'<i class="icon-list-numbered"></i>'},{name:"ulist",icon:'<i class="icon-list-bullet"></i>'},{name:"link",icon:'<i class="icon-link"></i>'}]});var clean=function(e){e.stopPropagation();e.preventDefault();var clipboardData=e.clipboardData||window.clipboardData;console.log(clipboardData.getData("Text"));window.pell.exec("insertText",clipboardData.getData("Text"));return true;};var alignText=function(){let paragraphs=editor.content.querySelectorAll('p,li');let last='';for(let paragraph of paragraphs){var content=paragraph.textContent;if(content.trim()===''){content=last.textContent;}

View file

@ -778,10 +778,14 @@ list["filters-"+filter.key]=params[key][i];}}}}
return list;};let apply=function(params){let cached=container.get(name);cached=cached?cached.params:[];params=Object.assign(cached,params);container.set(name,{name:name,params:params,query:serialize(params),forward:parseInt(params.offset)+parseInt(params.limit),backward:parseInt(params.offset)-parseInt(params.limit),keys:flatten(params)},true,name);document.dispatchEvent(new CustomEvent(name+"-changed",{bubbles:false,cancelable:true}));};switch(element.tagName){case"INPUT":break;case"TEXTAREA":break;case"BUTTON":element.addEventListener("click",function(){apply(JSON.parse(expression.parse(element.dataset["params"]||"{}")));});break;case"FORM":element.addEventListener("input",function(){apply(form.toJson(element));});element.addEventListener("change",function(){apply(form.toJson(element));});element.addEventListener("reset",function(){setTimeout(function(){apply(form.toJson(element));},0);});events=events.trim().split(",");for(let y=0;y<events.length;y++){if(events[y]==="init"){element.addEventListener("rendered",function(){apply(form.toJson(element));},{once:true});}else{}
element.setAttribute("data-event","none");}
break;default:break;}}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-headers",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";value.type="text";value.className="margin-bottom-no";value.placeholder="Value";wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.value=key.value.toLowerCase()+":"+value.value.toLowerCase();};let syncB=function(){let split=element.value.toLowerCase().split(":");key.value=split[0]||"";value.value=split[1]||"";key.value=key.value.trim();value.value=value.value.trim();};syncB();}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-forms-key-value",controller:function(element){let key=document.createElement("input");let value=document.createElement("input");let wrap=document.createElement("div");let cell1=document.createElement("div");let cell2=document.createElement("div");key.type="text";key.className="margin-bottom-no";key.placeholder="Key";key.required=true;value.type="text";value.className="margin-bottom-no";value.placeholder="Value";value.required=true;wrap.className="row thin margin-bottom-small";cell1.className="col span-6";cell2.className="col span-6";element.parentNode.insertBefore(wrap,element);cell1.appendChild(key);cell2.appendChild(value);wrap.appendChild(cell1);wrap.appendChild(cell2);key.addEventListener("input",function(){syncA();});value.addEventListener("input",function(){syncA();});element.addEventListener("change",function(){syncB();});let syncA=function(){element.name=key.value;element.value=value.value;};let syncB=function(){key.value=element.name||"";value.value=element.value||"";};syncB();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-down",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-down]")).map(function(obj){obj.addEventListener("click",function(){if(element.nextElementSibling){console.log('down',element.offsetHeight);element.parentNode.insertBefore(element.nextElementSibling,element);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-move-up",controller:function(element){Array.prototype.slice.call(element.querySelectorAll("[data-move-up]")).map(function(obj){obj.addEventListener("click",function(){if(element.previousElementSibling){console.log('up',element);element.parentNode.insertBefore(element,element.previousElementSibling);element.scrollIntoView({block:'center'});}});});}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-nav",repeat:false,controller:function(element,view,container,document){let titles=document.querySelectorAll('[data-forms-nav-anchor]');let links=element.querySelectorAll('[data-forms-nav-link]');let minLink=null;let check=function(){let minDistance=null;let minElement=null;for(let i=0;i<titles.length;++i){let title=titles[i];let distance=title.getBoundingClientRect().top;console.log(i);if((minDistance===null||minDistance>=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');}
console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-apple",controller:function(element){let container=document.createElement("div");let row=document.createElement("div");let col1=document.createElement("div");let col2=document.createElement("div");let keyID=document.createElement("input");let keyLabel=document.createElement("label");let teamID=document.createElement("input");let teamLabel=document.createElement("label");let p8=document.createElement("textarea");let p8Label=document.createElement("label");keyLabel.textContent='Key ID';teamLabel.textContent='Team ID';p8Label.textContent='P8 File';row.classList.add('row');row.classList.add('thin');container.appendChild(row);container.appendChild(p8Label);container.appendChild(p8);row.appendChild(col1);row.appendChild(col2);col1.classList.add('col');col1.classList.add('span-6');col1.appendChild(keyLabel);col1.appendChild(keyID);col2.classList.add('col');col2.classList.add('span-6');col2.appendChild(teamLabel);col2.appendChild(teamID);keyID.type='text';keyID.placeholder='SHAB13ROFN';teamID.type='text';teamID.placeholder='ELA2CD3AED';p8.accept='.p8';p8.classList.add('margin-bottom-no');element.parentNode.insertBefore(container,element.nextSibling);element.addEventListener('change',sync);keyID.addEventListener('change',update);teamID.addEventListener('change',update);p8.addEventListener('change',update);function update(){let json={};json.keyID=keyID.value;json.teamID=teamID.value;json.p8=p8.value;element.value=JSON.stringify(json);}
console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-oauth-custom",controller:function(element){let providers={"Microsoft":{"clientSecret":"oauth2MicrosoftClientSecret","tenantId":"oauth2MicrosoftTenantId"},"Apple":{"keyId":"oauth2AppleKeyId","teamId":"oauth2AppleTeamId","p8":"oauth2AppleP8"}}
let provider=element.getAttribute("data-forms-oauth-custom");if(!provider||!providers.hasOwnProperty(provider)){console.error("Provider for custom form not set or unkown")}
let config=providers[provider];element.addEventListener('change',sync);let elements={};for(const key in config){if(Object.hasOwnProperty.call(config,key)){elements[key]=document.getElementById(config[key]);elements[key].addEventListener('change',update);}}
function update(){let json={};for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){json[key]=elements[key].value}}
element.value=JSON.stringify(json);}
function sync(){if(!element.value){return;}
let json={};try{json=JSON.parse(element.value);}catch(error){console.error('Failed to parse secret key');}
teamID.value=json.teamID||'';keyID.value=json.keyID||'';p8.value=json.p8||'';}
for(const key in elements){if(Object.hasOwnProperty.call(elements,key)){elements[key].value=json[key]||'';}}}
sync();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-password-meter",controller:function(element,window){var calc=function(password){var score=0;if(!password)return score;var letters=new window.Object();for(var i=0;i<password.length;i++){letters[password[i]]=(letters[password[i]]||0)+1;score+=5.0/letters[password[i]];}
var variations={digits:/\d/.test(password),lower:/[a-z]/.test(password),upper:/[A-Z]/.test(password),nonWords:/\W/.test(password)};var variationCount=0;for(var check in variations){if(variations.hasOwnProperty(check)){variationCount+=variations[check]===true?1:0;}}
score+=(variationCount-1)*10;return parseInt(score);};var callback=function(){var score=calc(this.value);if(""===this.value)return(meter.className="password-meter");if(score>60)return(meter.className="password-meter strong");if(score>30)return(meter.className="password-meter medium");if(score>=0)return(meter.className="password-meter weak");};var meter=window.document.createElement("div");meter.className="password-meter";element.parentNode.insertBefore(meter,element.nextSibling);element.addEventListener("change",callback);element.addEventListener("keypress",callback);element.addEventListener("keyup",callback);element.addEventListener("keydown",callback);}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-pell",controller:function(element,window,document,markdown,rtl){var div=document.createElement("div");element.className="pell hide";div.className="input pell";element.parentNode.insertBefore(div,element);element.tabIndex=-1;var turndownService=new TurndownService();turndownService.addRule("underline",{filter:["u"],replacement:function(content){return"__"+content+"__";}});var editor=window.pell.init({element:div,onChange:function onChange(html){alignText();element.value=turndownService.turndown(html);},defaultParagraphSeparator:"p",actions:[{name:"bold",icon:'<i class="icon-bold"></i>'},{name:"underline",icon:'<i class="icon-underline"></i>'},{name:"italic",icon:'<i class="icon-italic"></i>'},{name:"olist",icon:'<i class="icon-list-numbered"></i>'},{name:"ulist",icon:'<i class="icon-list-bullet"></i>'},{name:"link",icon:'<i class="icon-link"></i>'}]});var clean=function(e){e.stopPropagation();e.preventDefault();var clipboardData=e.clipboardData||window.clipboardData;console.log(clipboardData.getData("Text"));window.pell.exec("insertText",clipboardData.getData("Text"));return true;};var alignText=function(){let paragraphs=editor.content.querySelectorAll('p,li');let last='';for(let paragraph of paragraphs){var content=paragraph.textContent;if(content.trim()===''){content=last.textContent;}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -1,93 +0,0 @@
(function(window) {
"use strict";
window.ls.container.get("view").add({
selector: "data-forms-oauth-apple",
controller: function(element) {
let container = document.createElement("div");
let row = document.createElement("div");
let col1 = document.createElement("div");
let col2 = document.createElement("div");
let keyID = document.createElement("input");
let keyLabel = document.createElement("label");
let teamID = document.createElement("input");
let teamLabel = document.createElement("label");
let p8 = document.createElement("textarea");
let p8Label = document.createElement("label");
keyLabel.textContent = 'Key ID';
teamLabel.textContent = 'Team ID';
p8Label.textContent = 'P8 File';
row.classList.add('row');
row.classList.add('thin');
container.appendChild(row);
container.appendChild(p8Label);
container.appendChild(p8);
row.appendChild(col1);
row.appendChild(col2);
col1.classList.add('col');
col1.classList.add('span-6');
col1.appendChild(keyLabel);
col1.appendChild(keyID);
col2.classList.add('col');
col2.classList.add('span-6');
col2.appendChild(teamLabel);
col2.appendChild(teamID);
keyID.type = 'text';
keyID.placeholder = 'SHAB13ROFN';
teamID.type = 'text';
teamID.placeholder = 'ELA2CD3AED';
p8.accept = '.p8';
p8.classList.add('margin-bottom-no');
element.parentNode.insertBefore(container, element.nextSibling);
element.addEventListener('change', sync);
keyID.addEventListener('change', update);
teamID.addEventListener('change', update);
p8.addEventListener('change', update);
function update() {
let json = {};
json.keyID = keyID.value;
json.teamID = teamID.value;
json.p8 = p8.value;
element.value = JSON.stringify(json);
}
function sync() {
if(!element.value) {
return;
}
let json = {};
try {
json = JSON.parse(element.value);
} catch (error) {
console.error('Failed to parse secret key');
}
teamID.value = json.teamID || '';
keyID.value = json.keyID || '';
p8.value = json.p8 || '';
}
// function syncB() {
// picker.value = element.value;
// }
// element.parentNode.insertBefore(preview, element);
// update();
sync();
}
});
})(window);

View file

@ -0,0 +1,73 @@
(function (window) {
"use strict";
//TODO: Make this generic
window.ls.container.get("view").add({
selector: "data-forms-oauth-custom",
controller: function (element) {
// provider configuration for custom forms. Keys will be property names in JSON, values the elementIDs for the according inputs
let providers = {
"Microsoft": {
"clientSecret": "oauth2MicrosoftClientSecret",
"tenantId": "oauth2MicrosoftTenantId"
},
"Apple": {
"keyId": "oauth2AppleKeyId",
"teamId": "oauth2AppleTeamId",
"p8": "oauth2AppleP8"
}
}
let provider = element.getAttribute("data-forms-oauth-custom");
if (!provider || !providers.hasOwnProperty(provider)) { console.error("Provider for custom form not set or unkown") }
let config = providers[provider];
// Add Change Listeners for element
element.addEventListener('change', sync);
// Get all inputs by id and register change event listener
let elements = {};
for (const key in config) {
if (Object.hasOwnProperty.call(config, key)) {
elements[key] = document.getElementById(config[key]);
elements[key].addEventListener('change', update);
}
}
// Build the JSON based on input in custom input fields
function update() {
let json = {};
for (const key in elements) {
if (Object.hasOwnProperty.call(elements, key)) {
json[key] = elements[key].value
}
}
element.value = JSON.stringify(json);
}
// When the JSON changes (on load) change values in custom input fields
function sync() {
if (!element.value) {
return;
}
let json = {};
try {
json = JSON.parse(element.value);
} catch (error) {
console.error('Failed to parse secret key');
}
for (const key in elements) {
if (Object.hasOwnProperty.call(elements, key)) {
elements[key].value = json[key] || '';
}
}
}
sync();
}
});
})(window);

View file

@ -1,32 +1,31 @@
// .loader {
// height: 0;
// direction: ltr;
// position: fixed;
// top: 0;
// width: 0;
// margin: 0 auto;
// .func-start(0);
// opacity: 0;
// z-index: 4;
// }
.loader {
height: 0;
direction: ltr;
position: fixed;
top: 0;
width: 0;
margin: 0 auto;
.func-start(0);
opacity: 0;
z-index: 4;
}
// .load-start ~ .loader {
// width: 100%;
// background: var(--config-color-focus);
// height: 3px;
// opacity: 1;
// transition: width .3s ease-out, opacity .5s ease-out;
// -moz-transition: width .3s ease-out, opacity .5s ease-out;
// -webkit-transition: width .3s ease-out, opacity .5s ease-out;
// -o-transition: width .3s ease-out, opacity .5s ease-out;
// }
.load-start ~ .loader {
width: 100%;
background: var(--config-color-focus);
height: 3px;
opacity: 1;
transition: width .3s ease-out, opacity .5s ease-out;
-moz-transition: width .3s ease-out, opacity .5s ease-out;
-webkit-transition: width .3s ease-out, opacity .5s ease-out;
-o-transition: width .3s ease-out, opacity .5s ease-out;
}
// .www .load-start ~ .loader {
// background: #fff;
// }
.www .load-start ~ .loader {
background: #fff;
}
/*
.load-start > * {
opacity: 0;
}
@ -37,7 +36,7 @@
-moz-transition: opacity .3s ease-out;
-webkit-transition: opacity .3s ease-out;
-o-transition: opacity .3s ease-out;
}*/
}
[data-ls-if] {
display: none;

View file

@ -62,9 +62,16 @@ abstract class OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
abstract public function getAccessToken(string $code):string;
abstract protected function getTokens(string $code):array;
/**
* @param string $refreshToken
*
* @return array
*/
abstract public function refreshTokens(string $refreshToken):array;
/**
* @param $accessToken
@ -109,6 +116,38 @@ abstract class OAuth2
return $this->scopes;
}
/**
* @param string $code
*
* @return string
*/
public function getAccessToken(string $code):string
{
$tokens = $this->getTokens($code);
return $tokens['access_token'] ?? '';
}
/**
* @param string $code
*
* @return string
*/
public function getRefreshToken(string $code):string
{
$tokens = $this->getTokens($code);
return $tokens['refresh_token'] ?? '';
}
/**
* @param string $code
*
* @return string
*/
public function getAccessTokenExpiry(string $code):string
{
$tokens = $this->getTokens($code);
return $tokens['expires_in'] ?? '';
}
// The parseState function was designed specifically for Amazon OAuth2 Adapter to override.
// The response from Amazon is html encoded and hence it needs to be html_decoded before

View file

@ -15,6 +15,11 @@ class Amazon extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -59,31 +64,54 @@ class Amazon extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8'];
$this->tokens = \json_decode($this->request(
'POST',
'https://api.amazon.com/auth/o2/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'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;charset=UTF-8'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://api.amazon.com/auth/o2/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID ,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback ,
'grant_type' => 'authorization_code'
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
$accessToken = \json_decode($accessToken, true);
), true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -14,6 +14,11 @@ class Apple extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -54,34 +59,60 @@ class Apple extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
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',
'https://appleid.apple.com/auth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->getAppSecret(),
'redirect_uri' => $this->callback,
])
), true);
$this->claims = (isset($this->tokens['id_token'])) ? \explode('.', $this->tokens['id_token']) : [0 => '', 1 => ''];
$this->claims = (isset($this->claims[1])) ? \json_decode(\base64_decode($this->claims[1]), true) : [];
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://appleid.apple.com/auth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->getAppSecret(),
'redirect_uri' => $this->callback,
])
);
), true);
$accessToken = \json_decode($accessToken, true);
$this->claims = (isset($accessToken['id_token'])) ? \explode('.', $accessToken['id_token']) : [0 => '', 1 => ''];
$this->claims = (isset($this->claims[1])) ? \json_decode(\base64_decode($this->claims[1]), true) : [];
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
$this->claims = (isset($this->tokens['id_token'])) ? \explode('.', $this->tokens['id_token']) : [0 => '', 1 => ''];
$this->claims = (isset($this->claims[1])) ? \json_decode(\base64_decode($this->claims[1]), true) : [];
return $this->tokens;
}
/**

View file

@ -13,6 +13,11 @@ class Bitbucket extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -43,32 +48,54 @@ class Bitbucket extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
// Required as per Bitbucket Spec.
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://bitbucket.org/site/oauth2/access_token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
// Required as per Bitbucket Spec.
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://bitbucket.org/site/oauth2/access_token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'authorization_code'
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
// Reference Material
// https://dev.bitly.com/v4_documentation.html
@ -29,6 +30,11 @@ class Bitly extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -54,9 +60,38 @@ class Bitly extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$response = $this->request(
'POST',
$this->resourceEndpoint . 'oauth/access_token',
["Content-Type: application/x-www-form-urlencoded"],
\http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"redirect_uri" => $this->callback,
"state" => \json_encode($this->state)
])
);
$output = [];
\parse_str($response, $output);
$this->tokens = $output;
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$response = $this->request(
'POST',
@ -65,20 +100,20 @@ class Bitly extends OAuth2
\http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"redirect_uri" => $this->callback,
"state" => \json_encode($this->state)
"refresh_token" => $refreshToken,
'grant_type' => 'refresh_token'
])
);
$result = null;
$output = [];
\parse_str($response, $output);
$this->tokens = $output;
if ($response) {
\parse_str($response, $result);
return $result['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -23,6 +23,11 @@ class Box extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -59,32 +64,55 @@ class Box extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$header = "Content-Type: application/x-www-form-urlencoded";
$accessToken = $this->request(
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'token',
$headers,
\http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"grant_type" => "authorization_code",
"scope" => \implode(',', $this->getScopes()),
"redirect_uri" => $this->callback
])
), 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->endpoint . 'token',
[$header],
$headers,
\http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"grant_type" => "authorization_code",
"scope" => \implode(',', $this->getScopes()),
"redirect_uri" => $this->callback
"refresh_token" => $refreshToken,
"grant_type" => "refresh_token",
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (array_key_exists('access_token', $accessToken)) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -18,6 +18,11 @@ class Discord extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -55,31 +60,53 @@ class Discord extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth2/token',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->callback,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'scope' => \implode(' ', $this->getScopes())
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth2/token',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->callback,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'scope' => \implode(' ', $this->getScopes())
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -14,6 +14,11 @@ class Dropbox extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -44,31 +49,54 @@ class Dropbox extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
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',
'https://api.dropboxapi.com/oauth2/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'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'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://api.dropboxapi.com/oauth2/token',
$headers,
\http_build_query([
'code' => $code,
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
'grant_type' => 'refresh_token'
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Facebook extends OAuth2
{
@ -15,6 +16,11 @@ class Facebook extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -47,27 +53,48 @@ class Facebook extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'GET',
'https://graph.facebook.com/' . $this->version . '/oauth/access_token?' . \http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'GET',
'https://graph.facebook.com/'.$this->version.'/oauth/access_token?'.\http_build_query([
'https://graph.facebook.com/' . $this->version . '/oauth/access_token?' . \http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Github extends OAuth2
{
@ -10,6 +11,11 @@ class Github extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -42,31 +48,59 @@ class Github extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$response = $this->request(
'POST',
'https://github.com/login/oauth/access_token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
])
);
$output = [];
\parse_str($response, $output);
$this->tokens = $output;
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$response = $this->request(
'POST',
'https://github.com/login/oauth/access_token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
$output = [];
\parse_str($response, $output);
$this->tokens = $output;
\parse_str($accessToken, $output);
if (isset($output['access_token'])) {
return $output['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -13,6 +13,11 @@ class Gitlab extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -46,28 +51,48 @@ class Gitlab extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
'POST',
'https://gitlab.com/oauth/token?'.\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
])
);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://gitlab.com/oauth/token?' . \http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://gitlab.com/oauth/token?' . \http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**

View file

@ -29,6 +29,11 @@ class Google extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -55,29 +60,49 @@ class Google extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
'POST',
'https://oauth2.googleapis.com/token?'.\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'scope' => null,
'grant_type' => 'authorization_code'
])
);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth2.googleapis.com/token?' . \http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'scope' => null,
'grant_type' => 'authorization_code'
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth2.googleapis.com/token?' . \http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**

View file

@ -10,6 +10,11 @@ class Linkedin extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -57,30 +62,53 @@ class Linkedin extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://www.linkedin.com/oauth/v2/accessToken',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->callback,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://www.linkedin.com/oauth/v2/accessToken',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'redirect_uri' => $this->callback,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -14,6 +14,11 @@ class Microsoft extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -36,7 +41,7 @@ class Microsoft extends OAuth2
*/
public function getLoginURL(): string
{
return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?'.\http_build_query([
return 'https://login.microsoftonline.com/'.$this->getTenantId().'/oauth2/v2.0/authorize?'.\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'state'=> \json_encode($this->state),
@ -49,33 +54,55 @@ class Microsoft extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$accessToken = $this->request(
'POST',
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'grant_type' => 'authorization_code'
])
);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://login.microsoftonline.com/' . $this->getTenantId() . '/oauth2/v2.0/token',
$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 '';
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',
'https://login.microsoftonline.com/' . $this->getTenantId() . '/oauth2/v2.0/token',
$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;
}
/**
@ -141,4 +168,39 @@ class Microsoft extends OAuth2
return $this->user;
}
/**
* Extracts the Client Secret from the JSON stored in appSecret
* @return string
*/
protected function getClientSecret(): string
{
$secret = $this->decodeJson();
return (isset($secret['clientSecret'])) ? $secret['clientSecret'] : '';
}
/**
* Decode the JSON stored in appSecret
* @return array
*/
protected function decodeJson(): array
{
try {
$secret = \json_decode($this->appSecret, true);
} catch (\Throwable $th) {
throw new Exception('Invalid secret');
}
return $secret;
}
/**
* Extracts the Tenant Id from the JSON stored in appSecret. Defaults to 'common' as a fallback
* @return string
*/
protected function getTenantId(): string
{
$secret = $this->decodeJson();
return (isset($secret['tenantId'])) ? $secret['tenantId'] : 'common';
}
}

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Mock extends OAuth2
{
@ -22,6 +23,11 @@ class Mock extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -47,28 +53,49 @@ class Mock extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
'GET',
'http://localhost/'.$this->version.'/mock/tests/general/oauth2/token?'.
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
])
);
$accessToken = \json_decode($accessToken, true); //
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'GET',
'http://localhost/' . $this->version . '/mock/tests/general/oauth2/token?' .
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'code' => $code
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'GET',
'http://localhost/' . $this->version . '/mock/tests/general/oauth2/token?' .
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**

View file

@ -20,6 +20,11 @@ class Notion extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -51,32 +56,50 @@ class Notion extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$headers = [
"Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret),
];
if(empty($this->tokens)) {
$headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'redirect_uri' => $this->callback,
'code' => $code
])
), true);
}
$response = $this->request(
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'redirect_uri' => $this->callback,
'code' => $code
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
])
);
), true);
$response = \json_decode($response, true);
if (isset($response['access_token'])) {
return $response['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -34,6 +34,11 @@ class Paypal extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -74,29 +79,47 @@ class Paypal extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->resourceEndpoint[$this->environment] . 'oauth2/token',
['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)],
\http_build_query([
'code' => $code,
'grant_type' => 'authorization_code',
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->resourceEndpoint[$this->environment] . 'oauth2/token',
['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)],
\http_build_query([
'code' => $code,
'grant_type' => 'authorization_code',
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token',
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -15,6 +15,11 @@ class Salesforce extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -59,32 +64,56 @@ class Salesforce extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$this->tokens = \json_decode($this->request(
'POST',
'https://login.salesforce.com/services/oauth2/token',
$headers,
\http_build_query([
'code' => $code,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = [
"Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret),
"Content-Type: application/x-www-form-urlencoded",
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://login.salesforce.com/services/oauth2/token',
$headers,
\http_build_query([
'code' => $code,
'redirect_uri' => $this->callback ,
'grant_type' => 'authorization_code'
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
);
$accessToken = \json_decode($accessToken, true);
), true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Slack extends OAuth2
{
@ -10,6 +11,11 @@ class Slack extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -46,28 +52,48 @@ class Slack extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
// https://api.slack.com/docs/oauth#step_3_-_exchanging_a_verification_code_for_an_access_token
$accessToken = $this->request(
'GET',
'https://slack.com/api/oauth.access?'.\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'redirect_uri' => $this->callback
])
);
$accessToken = \json_decode($accessToken, true); //
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens)) {
// https://api.slack.com/docs/oauth#step_3_-_exchanging_a_verification_code_for_an_access_token
$this->tokens = \json_decode($this->request(
'GET',
'https://slack.com/api/oauth.access?' . \http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'redirect_uri' => $this->callback
])
), true);
}
return '';
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'GET',
'https://slack.com/api/oauth.access?' . \http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**

View file

@ -31,6 +31,11 @@ class Spotify extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -58,27 +63,50 @@ class Spotify extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$header = "Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret);
$result = \json_decode($this->request(
if(empty($this->tokens)) {
$headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'api/token',
$headers,
\http_build_query([
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'api/token',
[$header],
$headers,
\http_build_query([
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
"refresh_token" => $refreshToken,
"grant_type" => "refresh_token",
])
), true);
if (isset($result['access_token'])) {
return $result['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -0,0 +1,182 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
class Stripe extends OAuth2
{
/**
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var string
*/
protected $stripeAccountId = '';
/**
* @var array
*/
protected $scopes = [
'read_write',
];
/**
* @return string
*/
protected $grantType = [
'authorize' => 'authorization_code',
'refresh' => 'refresh_token',
];
/**
* @return string
*/
public function getName():string
{
return 'stripe';
}
/**
* @return string
*/
public function getLoginURL():string
{
return 'https://connect.stripe.com/oauth/authorize?'. \http_build_query([
'response_type' => 'code', // The only option at the moment is "code."
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state)
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://connect.stripe.com/oauth/token',
[],
\http_build_query([
'grant_type' => $this->grantType['authorize'],
'code' => $code
])
), true);
$this->stripeAccountId = $this->tokens['stripe_user_id'];
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://connect.stripe.com/oauth/token',
[],
\http_build_query([
'grant_type' => $this->grantType['refresh'],
'refresh_token' => $refreshToken,
])
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
$this->stripeAccountId = $this->tokens['stripe_user_id'];
return $this->tokens;
}
/**
* @param $accessToken
*
* @return string
*/
public function getUserID(string $accessToken):string
{
$user = $this->getUser($accessToken);
if (isset($user['id'])) {
return $user['id'];
}
return '';
}
/**
* @param $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken):string
{
$user = $this->getUser($accessToken);
if(empty($user)) {
return '';
}
return $user['email'] ?? '';
}
/**
* @param $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)
{
if (empty($this->user) && !empty($this->stripeAccountId)) {
$this->user = \json_decode(
$this->request(
'GET',
'https://api.stripe.com/v1/accounts/' . $this->stripeAccountId,
['Authorization: Bearer '.\urlencode($accessToken)]
),
true
);
}
return $this->user;
}
}

View file

@ -33,6 +33,11 @@ class Tradeshift extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
protected $scopes = [
@ -69,23 +74,47 @@ class Tradeshift extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
$response = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint[$this->environment] . 'auth/token',
['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint[$this->environment] . 'auth/token',
['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)],
\http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
])
);
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
$accessToken = \json_decode($response, true);
return $accessToken['access_token'] ?? '';
return $this->tokens;
}
/**

View file

@ -31,6 +31,11 @@ class Twitch extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -59,26 +64,48 @@ class Twitch extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$result = \json_decode($this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'token?' . \http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'token?'. \http_build_query([
$this->endpoint . 'token?' . \http_build_query([
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
"refresh_token" => $refreshToken,
"grant_type" => "refresh_token",
])
), true);
if (isset($result['access_token'])) {
return $result['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Exception;
// Reference Material
// https://vk.com/dev/first_guide
@ -16,6 +17,11 @@ class Vk extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -57,36 +63,59 @@ class Vk extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8'];
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth.vk.com/access_token?',
$headers,
\http_build_query([
'code' => $code,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback
])
), true);
$this->user['email'] = $this->tokens['email'];
$this->user['user_id'] = $this->tokens['user_id'];
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth.vk.com/access_token?',
$headers,
\http_build_query([
'code' => $code,
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback
'grant_type' => 'refresh_token'
])
);
$accessToken = \json_decode($accessToken, true);
), true);
if (isset($accessToken['email'])) {
$this->user['email'] = $accessToken['email'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
if (isset($accessToken['user_id'])) {
$this->user['user_id'] = $accessToken['user_id'];
}
$this->user['email'] = $this->tokens['email'];
$this->user['user_id'] = $this->tokens['user_id'];
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
}
return '';
return $this->tokens;
}
/**

View file

@ -13,6 +13,11 @@ class WordPress extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -46,30 +51,52 @@ class WordPress extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$accessToken = $this->request(
if(empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
'https://public-api.wordpress.com/oauth2/token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'grant_type' => 'authorization_code',
'code' => $code
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$this->tokens = \json_decode($this->request(
'POST',
'https://public-api.wordpress.com/oauth2/token',
[],
\http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'client_secret' => $this->appSecret,
'grant_type' => 'authorization_code',
'code' => $code
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -32,6 +32,11 @@ class Yahoo extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -70,31 +75,58 @@ class Yahoo extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code):string
protected function getTokens(string $code): array
{
$header = [
"Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret),
"Content-Type: application/x-www-form-urlencoded",
if(empty($this->tokens)) {
$headers = [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'get_token',
$headers,
\http_build_query([
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$result = \json_decode($this->request(
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'get_token',
$header,
$headers,
\http_build_query([
"code" => $code,
"grant_type" => "authorization_code",
"redirect_uri" => $this->callback
"refresh_token" => $refreshToken,
"grant_type" => "refresh_token",
])
), true);
if (isset($result['access_token'])) {
return $result['access_token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -18,6 +18,11 @@ class Yammer extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @return string
@ -44,31 +49,53 @@ class Yammer extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
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->endpoint . 'access_token?',
$headers,
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'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'];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . 'access_token?',
$headers,
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'code' => $code,
'grant_type' => 'authorization_code'
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
])
);
), true);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token']['token'])) {
return $accessToken['access_token']['token'];
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -15,6 +15,11 @@ class Yandex extends OAuth2
* @var array
*/
protected $user = [];
/**
* @var array
*/
protected $tokens = [];
/**
* @var array
@ -56,31 +61,55 @@ class Yandex extends OAuth2
/**
* @param string $code
*
* @return string
* @return array
*/
public function getAccessToken(string $code): string
protected function getTokens(string $code): array
{
if(empty($this->tokens)) {
$headers = [
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth.yandex.com/token',
$headers,
\http_build_query([
'code' => $code,
'grant_type' => 'authorization_code'
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken):array
{
$headers = [
"Authorization: Basic " . \base64_encode($this->appID . ":" . $this->appSecret),
"Content-Type: application/x-www-form-urlencoded",
'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret),
'Content-Type: application/x-www-form-urlencoded',
];
$accessToken = $this->request(
$this->tokens = \json_decode($this->request(
'POST',
'https://oauth.yandex.com/token',
$headers,
\http_build_query([
'code' => $code,
'refresh_token' => $refreshToken,
'grant_type' => 'authorization_code'
])
);
$accessToken = \json_decode($accessToken, true);
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
), true);
if(empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return '';
return $this->tokens;
}
/**

View file

@ -1,161 +0,0 @@
<?php
namespace Appwrite\Database;
use Exception;
abstract class Adapter
{
/**
* @var string
*/
protected $namespace = '';
/**
* Set Namespace.
*
* Set namespace to divide different scope of data sets
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function setNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Missing namespace');
}
$this->namespace = $namespace;
return true;
}
/**
* Get Namespace.
*
* Get namespace of current set scope
*
* @throws Exception
*
* @return string
*/
public function getNamespace()
{
if (empty($this->namespace)) {
throw new Exception('Missing namespace');
}
return $this->namespace;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*/
abstract public function getDocument($id);
/**
* Create Document
**.
*
* @param array $data
*
* @return array
*/
abstract public function createDocument(array $data = [], array $unique = []);
/**
* Update Document.
*
* @param array $data
*
* @return array
*/
abstract public function updateDocument(array $data = []);
/**
* Delete Node.
*
* @param string $id
*
* @return array
*/
abstract public function deleteDocument(string $id);
/**
* Delete Unique Key.
*
* @param string $key
*
* @return array
*/
abstract public function deleteUniqueKey($key);
/**
* Add Unique Key.
*
* @param string $key
*
* @return array
*/
abstract public function addUniqueKey($key);
/**
* Create Namespace.
*
* @param string $namespace
*
* @return bool
*/
abstract public function createNamespace($namespace);
/**
* Delete Namespace.
*
* @param string $namespace
*
* @return bool
*/
abstract public function deleteNamespace($namespace);
/**
* Filter.
*
* Filter data sets using chosen queries
*
* @param array $options
* @param array $filterTypes
*
* @return array
*/
abstract public function getCollection(array $options, array $filterTypes = []);
/**
* @param array $options
*
* @return int
*/
abstract public function getCount(array $options);
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in corrent session has been changed
*
* @return int
*/
abstract public function lastModified();
/**
* Get Debug Data.
*
* @return array
*/
abstract public function getDebug();
}

View file

@ -1,986 +0,0 @@
<?php
namespace Appwrite\Database\Adapter;
use Appwrite\Database\Adapter;
use Appwrite\Database\Exception\Duplicate;
use Appwrite\Database\Validator\Authorization;
use Exception;
use PDO;
use Redis;
class MySQL extends Adapter
{
const DATA_TYPE_STRING = 'string';
const DATA_TYPE_INTEGER = 'integer';
const DATA_TYPE_FLOAT = 'float';
const DATA_TYPE_BOOLEAN = 'boolean';
const DATA_TYPE_OBJECT = 'object';
const DATA_TYPE_DICTIONARY = 'dictionary';
const DATA_TYPE_ARRAY = 'array';
const DATA_TYPE_NULL = 'null';
const OPTIONS_LIMIT_ATTRIBUTES = 1000;
/**
* Last modified.
*
* Read node with most recent changes
*
* @var int
*/
protected $lastModified = -1;
/**
* @var array
*/
protected $debug = [];
/**
* @var PDO
*/
protected $pdo;
/**
* @var Redis
*/
protected $redis;
/**
* Constructor.
*
* Set connection and settings
*
* @param PDO $pdo
* @param Redis $redis
*/
public function __construct($pdo, Redis $redis)
{
$this->pdo = $pdo;
$this->redis = $redis;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function getDocument($id)
{
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.documents` a
WHERE a.uid = :uid AND a.status = 0
ORDER BY a.updatedAt DESC LIMIT 10;
');
$st->bindValue(':uid', $id, PDO::PARAM_STR);
$st->execute();
$document = $st->fetch();
if (empty($document)) { // Not Found
return [];
}
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.properties` a
WHERE a.documentUid = :documentUid AND a.documentRevision = :documentRevision
ORDER BY `order`
');
$st->bindParam(':documentUid', $document['uid'], PDO::PARAM_STR, 32);
$st->bindParam(':documentRevision', $document['revision'], PDO::PARAM_STR, 32);
$st->execute();
$properties = $st->fetchAll();
$output = [
'$id' => null,
'$collection' => null,
'$permissions' => (!empty($document['permissions'])) ? \json_decode($document['permissions'], true) : [],
];
foreach ($properties as &$property) {
\settype($property['value'], $property['primitive']);
if ($property['array']) {
$output[$property['key']][] = $property['value'];
} else {
$output[$property['key']] = $property['value'];
}
}
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.relationships` a
WHERE a.start = :start AND revision = :revision
ORDER BY `order`
');
$st->bindParam(':start', $document['uid'], PDO::PARAM_STR, 32);
$st->bindParam(':revision', $document['revision'], PDO::PARAM_STR, 32);
$st->execute();
$output['temp-relations'] = $st->fetchAll();
return $output;
}
/**
* Create Document.
*
* @param array $data
*
* @throws \Exception
*
* @return array
*/
public function createDocument(array $data = [], array $unique = [])
{
$order = 0;
$data = \array_merge(['$id' => null, '$permissions' => []], $data); // Merge data with default params
$signature = \md5(\json_encode($data));
$revision = \uniqid('', true);
$data['$id'] = (empty($data['$id'])) ? null : $data['$id'];
/*
* When updating node, check if there are any changes to update
* by comparing data md5 signatures
*/
if (null !== $data['$id']) {
$st = $this->getPDO()->prepare('SELECT signature FROM `'.$this->getNamespace().'.database.documents` a
WHERE a.uid = :uid AND a.status = 0
ORDER BY a.updatedAt DESC LIMIT 1;
');
$st->bindValue(':uid', $data['$id'], PDO::PARAM_STR);
$st->execute();
$result = $st->fetch();
if ($result && isset($result['signature'])) {
$oldSignature = $result['signature'];
if ($signature === $oldSignature) {
return $data;
}
}
}
/**
* Check Unique Keys
*/
foreach ($unique as $key => $value) {
$st = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.unique`
SET `key` = :key;
');
$st->bindValue(':key', \md5($data['$collection'].':'.$key.'='.$value), PDO::PARAM_STR);
if (!$st->execute()) {
throw new Duplicate('Duplicated Property: '.$key.'='.$value);
}
}
// Add or update fields abstraction level
$st1 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.documents`
SET uid = :uid, createdAt = :createdAt, updatedAt = :updatedAt, signature = :signature, revision = :revision, permissions = :permissions, status = 0
ON DUPLICATE KEY UPDATE uid = :uid, updatedAt = :updatedAt, signature = :signature, revision = :revision, permissions = :permissions;
');
// Adding fields properties
if (null === $data['$id'] || !isset($data['$id'])) { // Get new fields UID
$data['$id'] = $this->getId();
}
$st1->bindValue(':uid', $data['$id'], PDO::PARAM_STR);
$st1->bindValue(':revision', $revision, PDO::PARAM_STR);
$st1->bindValue(':signature', $signature, PDO::PARAM_STR);
$st1->bindValue(':createdAt', \date('Y-m-d H:i:s', \time()), PDO::PARAM_STR);
$st1->bindValue(':updatedAt', \date('Y-m-d H:i:s', \time()), PDO::PARAM_STR);
$st1->bindValue(':permissions', \json_encode($data['$permissions']), PDO::PARAM_STR);
$st1->execute();
// Delete old properties
$rms1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.properties` WHERE documentUid = :documentUid AND documentRevision != :documentRevision');
$rms1->bindValue(':documentUid', $data['$id'], PDO::PARAM_STR);
$rms1->bindValue(':documentRevision', $revision, PDO::PARAM_STR);
$rms1->execute();
// Delete old relationships
$rms2 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.relationships` WHERE start = :start AND revision != :revision');
$rms2->bindValue(':start', $data['$id'], PDO::PARAM_STR);
$rms2->bindValue(':revision', $revision, PDO::PARAM_STR);
$rms2->execute();
// Create new properties
$st2 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.properties`
(`documentUid`, `documentRevision`, `key`, `value`, `primitive`, `array`, `order`)
VALUES (:documentUid, :documentRevision, :key, :value, :primitive, :array, :order)');
$props = [];
foreach ($data as $key => $value) { // Prepare properties data
if (\in_array($key, ['$permissions'])) {
continue;
}
$type = $this->getDataType($value);
// Handle array of relations
if (self::DATA_TYPE_ARRAY === $type) {
if (!is_array($value)) { // Property should be of type array, if not = skip
continue;
}
foreach ($value as $i => $child) {
if (self::DATA_TYPE_DICTIONARY !== $this->getDataType($child)) { // not dictionary
$props[] = [
'type' => $this->getDataType($child),
'key' => $key,
'value' => $child,
'array' => true,
'order' => $order++,
];
continue;
}
$data[$key][$i] = $this->createDocument($child);
$this->createRelationship($revision, $data['$id'], $data[$key][$i]['$id'], $key, true, $i);
}
continue;
}
// Handle relation
if (self::DATA_TYPE_DICTIONARY === $type) {
$value = $this->createDocument($value);
$this->createRelationship($revision, $data['$id'], $value['$id'], $key); //xxx
continue;
}
// Handle empty values
if (self::DATA_TYPE_NULL === $type) {
continue;
}
$props[] = [
'type' => $type,
'key' => $key,
'value' => $value,
'array' => false,
'order' => $order++,
];
}
foreach ($props as $prop) {
if (\is_array($prop['value'])) {
throw new Exception('Value can\'t be an array: '.\json_encode($prop['value']));
}
if (\is_bool($prop['value'])) {
$prop['value'] = (int) $prop['value'];
}
$st2->bindValue(':documentUid', $data['$id'], PDO::PARAM_STR);
$st2->bindValue(':documentRevision', $revision, PDO::PARAM_STR);
$st2->bindValue(':key', $prop['key'], PDO::PARAM_STR);
$st2->bindValue(':value', $prop['value'], PDO::PARAM_STR);
$st2->bindValue(':primitive', $prop['type'], PDO::PARAM_STR);
$st2->bindValue(':array', $prop['array'], PDO::PARAM_BOOL);
$st2->bindValue(':order', $prop['order'], PDO::PARAM_STR);
$st2->execute();
}
// TODO remove this dependency (check if related to nested documents)
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Update Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function updateDocument(array $data = [])
{
return $this->createDocument($data);
}
/**
* Delete Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function deleteDocument(string $id)
{
$st1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.documents`
WHERE uid = :id
');
$st1->bindValue(':id', $id, PDO::PARAM_STR);
$st1->execute();
$st2 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.properties`
WHERE documentUid = :id
');
$st2->bindValue(':id', $id, PDO::PARAM_STR);
$st2->execute();
$st3 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.relationships`
WHERE start = :id OR end = :id
');
$st3->bindValue(':id', $id, PDO::PARAM_STR);
$st3->execute();
return [];
}
/**
* Delete Unique Key.
*
* @param string $key
*
* @return array
*
* @throws Exception
*/
public function deleteUniqueKey($key)
{
$st1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.unique` WHERE `key` = :key');
$st1->bindValue(':key', $key, PDO::PARAM_STR);
$st1->execute();
return [];
}
/**
* Add Unique Key.
*
* @param string $key
*
* @return array
*
* @throws Exception
*/
public function addUniqueKey($key)
{
$st = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.unique`
SET `key` = :key;
');
$st->bindValue(':key', $key, PDO::PARAM_STR);
if (!$st->execute()) {
throw new Duplicate('Duplicated Property: '.$key);
}
return [];
}
/**
* Create Relation.
*
* Adds a new relationship between different nodes
*
* @param string $revision
* @param int $start
* @param int $end
* @param string $key
* @param bool $isArray
* @param int $order
*
* @return array
*
* @throws Exception
*/
protected function createRelationship($revision, $start, $end, $key, $isArray = false, $order = 0)
{
$st2 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.relationships`
(`revision`, `start`, `end`, `key`, `array`, `order`)
VALUES (:revision, :start, :end, :key, :array, :order)');
$st2->bindValue(':revision', $revision, PDO::PARAM_STR);
$st2->bindValue(':start', $start, PDO::PARAM_STR);
$st2->bindValue(':end', $end, PDO::PARAM_STR);
$st2->bindValue(':key', $key, PDO::PARAM_STR);
$st2->bindValue(':array', $isArray, PDO::PARAM_INT);
$st2->bindValue(':order', $order, PDO::PARAM_INT);
$st2->execute();
return [];
}
/**
* Create Namespace.
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function createNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Empty namespace');
}
$documents = 'app_'.$namespace.'.database.documents';
$properties = 'app_'.$namespace.'.database.properties';
$relationships = 'app_'.$namespace.'.database.relationships';
$unique = 'app_'.$namespace.'.database.unique';
$audit = 'app_'.$namespace.'.audit.audit';
$abuse = 'app_'.$namespace.'.abuse.abuse';
try {
$this->getPDO()->prepare('CREATE TABLE `'.$documents.'` LIKE `template.database.documents`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$properties.'` LIKE `template.database.properties`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$relationships.'` LIKE `template.database.relationships`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$unique.'` LIKE `template.database.unique`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$audit.'` LIKE `template.audit.audit`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$abuse.'` LIKE `template.abuse.abuse`;')->execute();
} catch (Exception $e) {
throw $e;
}
return true;
}
/**
* Delete Namespace.
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function deleteNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Empty namespace');
}
$unique = 'app_'.$namespace.'.database.unique';
$documents = 'app_'.$namespace.'.database.documents';
$properties = 'app_'.$namespace.'.database.properties';
$relationships = 'app_'.$namespace.'.database.relationships';
$audit = 'app_'.$namespace.'.audit.audit';
$abuse = 'app_'.$namespace.'.abuse.abuse';
try {
$this->getPDO()->prepare('DROP TABLE `'.$unique.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$documents.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$properties.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$relationships.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$audit.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$abuse.'`;')->execute();
} catch (Exception $e) {
throw $e;
}
return true;
}
/**
* Get Collection.
*
* @param array $options
* @param array $filterTypes
*
* @throws Exception
*
* @return array
*/
public function getCollection(array $options, array $filterTypes = [])
{
$start = \microtime(true);
$orderCastMap = [
'int' => 'UNSIGNED',
'string' => 'CHAR',
'date' => 'DATE',
'time' => 'TIME',
'datetime' => 'DATETIME',
];
$orderTypeMap = ['DESC', 'ASC'];
$options['orderField'] = (empty($options['orderField'])) ? '' : $options['orderField']; // Set default order field
$options['orderCast'] = (empty($options['orderCast'])) ? 'string' : $options['orderCast']; // Set default order field
if (!\array_key_exists($options['orderCast'], $orderCastMap)) {
throw new Exception('Invalid order cast');
}
if (!\in_array($options['orderType'], $orderTypeMap)) {
throw new Exception('Invalid order type');
}
$where = [];
$join = [];
$sorts = [];
$search = '';
// Filters
foreach ($options['filters'] as $i => $filter) {
$filter = $this->parseFilter($filter);
$key = $filter['key'];
$value = $filter['value'];
$operator = $filter['operator'];
$path = \explode('.', $key);
$original = $path;
if (1 < \count($path)) {
$key = \array_pop($path);
} else {
$path = [];
}
//$path = implode('.', $path);
if (array_key_exists($key, $filterTypes) && $filterTypes[$key] === 'numeric') {
$value = (float) $value;
} else {
$value = $this->getPDO()->quote($value, PDO::PARAM_STR);
}
$key = $this->getPDO()->quote($key, PDO::PARAM_STR);
//$path = $this->getPDO()->quote($path, PDO::PARAM_STR);
$options['offset'] = (int) $options['offset'];
$options['limit'] = (int) $options['limit'];
if (empty($path)) {
//if($path == "''") { // Handle direct attributes queries
$where[] = 'JOIN `'.$this->getNamespace().".database.properties` b{$i} ON a.uid IS NOT NULL AND b{$i}.documentUid = a.uid AND (b{$i}.key = {$key} AND b{$i}.value {$operator} {$value})";
} else { // Handle direct child attributes queries
$len = \count($original);
$prev = 'c'.$i;
foreach ($original as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
if (0 === $y) { // First key
$join[$i] = 'JOIN `'.$this->getNamespace().".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.properties` e{$i} ON e{$i}.documentUid = {$prev}.end AND e{$i}.key = {$part} AND e{$i}.value {$operator} {$value}";
} else {
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.relationships` d{$i}{$y} ON d{$i}{$y}.start = {$prev}.end AND d{$i}{$y}.key = {$part}";
$prev = 'd'.$i.$y;
}
}
//$join[] = "JOIN `" . $this->getNamespace() . ".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$path}
// JOIN `" . $this->getNamespace() . ".database.properties` d{$i} ON d{$i}.documentUid = c{$i}.end AND d{$i}.key = {$key} AND d{$i}.value {$operator} {$value}";
}
}
// Sorting
if (!empty($options['orderField'])) {
$orderPath = \explode('.', $options['orderField']);
$len = \count($orderPath);
$orderKey = 'order_b';
$part = $this->getPDO()->quote(\implode('', $orderPath), PDO::PARAM_STR);
$orderSelect = "CASE WHEN {$orderKey}.key = {$part} THEN CAST({$orderKey}.value AS {$orderCastMap[$options['orderCast']]}) END AS sort_ff";
if (1 === $len) {
//if($path == "''") { // Handle direct attributes queries
$sorts[] = 'LEFT JOIN `'.$this->getNamespace().".database.properties` order_b ON a.uid IS NOT NULL AND order_b.documentUid = a.uid AND (order_b.key = {$part})";
} else { // Handle direct child attributes queries
$prev = 'c';
$orderKey = 'order_e';
foreach ($orderPath as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
$x = $y - 1;
if (0 === $y) { // First key
$sorts[] = 'JOIN `'.$this->getNamespace().".database.relationships` order_c{$y} ON a.uid IS NOT NULL AND order_c{$y}.start = a.uid AND order_c{$y}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$sorts[] .= 'JOIN `'.$this->getNamespace().".database.properties` order_e ON order_e.documentUid = order_{$prev}{$x}.end AND order_e.key = {$part}";
} else {
$sorts[] .= 'JOIN `'.$this->getNamespace().".database.relationships` order_d{$y} ON order_d{$y}.start = order_{$prev}{$x}.end AND order_d{$y}.key = {$part}";
$prev = 'd';
}
}
}
} else {
$orderSelect = 'a.uid AS sort_ff';
}
/*
* Workaround for a MySQL bug as reported here:
* https://bugs.mysql.com/bug.php?id=78485
*/
$options['search'] = ($options['search'] === '*') ? '' : $options['search'];
// Search
if (!empty($options['search'])) { // Handle free search
$where[] = 'LEFT JOIN `'.$this->getNamespace().".database.properties` b_search ON a.uid IS NOT NULL AND b_search.documentUid = a.uid AND b_search.primitive = 'string'
LEFT JOIN
`".$this->getNamespace().'.database.relationships` c_search ON c_search.start = b_search.documentUid
LEFT JOIN
`'.$this->getNamespace().".database.properties` d_search ON d_search.documentUid = c_search.end AND d_search.primitive = 'string'
\n";
$search = "AND (MATCH (b_search.value) AGAINST ({$this->getPDO()->quote($options['search'], PDO::PARAM_STR)} IN BOOLEAN MODE)
OR MATCH (d_search.value) AGAINST ({$this->getPDO()->quote($options['search'], PDO::PARAM_STR)} IN BOOLEAN MODE)
)";
}
$select = 'DISTINCT a.uid';
$where = \implode("\n", $where);
$join = \implode("\n", $join);
$sorts = \implode("\n", $sorts);
$range = "LIMIT {$options['offset']}, {$options['limit']}";
$roles = [];
foreach (Authorization::getRoles() as $role) {
$roles[] = 'JSON_CONTAINS(REPLACE(a.permissions, \'{self}\', a.uid), \'"'.$role.'"\', \'$.read\')';
}
if (false === Authorization::$status) { // FIXME temporary solution (hopefully)
$roles = ['1=1'];
}
$query = "SELECT %s, {$orderSelect}
FROM `".$this->getNamespace().".database.documents` a {$where}{$join}{$sorts}
WHERE status = 0
{$search}
AND (".\implode('||', $roles).")
ORDER BY sort_ff {$options['orderType']} %s";
$st = $this->getPDO()->prepare(\sprintf($query, $select, $range));
$st->execute();
$results = ['data' => []];
// Get entire fields data for each id
foreach ($st->fetchAll() as $node) {
$results['data'][] = $node['uid'];
}
$count = $this->getPDO()->prepare(\sprintf($query, 'count(DISTINCT a.uid) as sum', ''));
$count->execute();
$count = $count->fetch();
$this->resetDebug();
$this
->setDebug('query', \preg_replace('/\s+/', ' ', \sprintf($query, $select, $range)))
->setDebug('time', \microtime(true) - $start)
->setDebug('filters', \count($options['filters']))
->setDebug('joins', \substr_count($query, 'JOIN'))
->setDebug('count', \count($results['data']))
->setDebug('sum', (int) $count['sum'])
;
return $results['data'];
}
/**
* Get Collection.
*
* @param array $options
*
* @throws Exception
*
* @return int
*/
public function getCount(array $options)
{
$start = \microtime(true);
$where = [];
$join = [];
$options = array_merge([
'attribute' => '',
'filters' => [],
], $options);
// Filters
foreach ($options['filters'] as $i => $filter) {
$filter = $this->parseFilter($filter);
$key = $filter['key'];
$value = $filter['value'];
$operator = $filter['operator'];
$path = \explode('.', $key);
$original = $path;
if (1 < \count($path)) {
$key = \array_pop($path);
} else {
$path = [];
}
$key = $this->getPDO()->quote($key, PDO::PARAM_STR);
$value = $this->getPDO()->quote($value, PDO::PARAM_STR);
if (empty($path)) {
//if($path == "''") { // Handle direct attributes queries
$where[] = 'JOIN `'.$this->getNamespace().".database.properties` b{$i} ON a.uid IS NOT NULL AND b{$i}.documentUid = a.uid AND (b{$i}.key = {$key} AND b{$i}.value {$operator} {$value})";
} else { // Handle direct child attributes queries
$len = \count($original);
$prev = 'c'.$i;
foreach ($original as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
if (0 === $y) { // First key
$join[$i] = 'JOIN `'.$this->getNamespace().".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.properties` e{$i} ON e{$i}.documentUid = {$prev}.end AND e{$i}.key = {$part} AND e{$i}.value {$operator} {$value}";
} else {
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.relationships` d{$i}{$y} ON d{$i}{$y}.start = {$prev}.end AND d{$i}{$y}.key = {$part}";
$prev = 'd'.$i.$y;
}
}
}
}
$where = \implode("\n", $where);
$join = \implode("\n", $join);
$attribute = $this->getPDO()->quote($options['attribute'], PDO::PARAM_STR);
$func = 'JOIN `'.$this->getNamespace().".database.properties` b_func ON a.uid IS NOT NULL
AND a.uid = b_func.documentUid
AND (b_func.key = {$attribute})";
$roles = [];
foreach (Authorization::getRoles() as $role) {
$roles[] = 'JSON_CONTAINS(REPLACE(a.permissions, \'{self}\', a.uid), \'"'.$role.'"\', \'$.read\')';
}
if (false === Authorization::$status) { // FIXME temporary solution (hopefully)
$roles = ['1=1'];
}
$query = "SELECT SUM(b_func.value) as result
FROM `".$this->getNamespace().".database.documents` a {$where}{$join}{$func}
WHERE status = 0
AND (".\implode('||', $roles).')';
$st = $this->getPDO()->prepare(\sprintf($query));
$st->execute();
$result = $st->fetch();
$this->resetDebug();
$this
->setDebug('query', \preg_replace('/\s+/', ' ', \sprintf($query)))
->setDebug('time', \microtime(true) - $start)
->setDebug('filters', \count($options['filters']))
->setDebug('joins', \substr_count($query, 'JOIN'))
;
return (isset($result['result'])) ? (int)$result['result'] : 0;
}
/**
* Get Unique Document ID.
*
* @return string
*/
public function getId(): string
{
$unique = \uniqid();
$attempts = 5;
for ($i = 1; $i <= $attempts; ++$i) {
$document = $this->getDocument($unique);
if (empty($document) || $document['$id'] !== $unique) {
return $unique;
}
}
throw new Exception('Failed to create a unique ID ('.$attempts.' attempts)');
}
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in corrent session has been changed
*
* @return int
*/
public function lastModified()
{
return $this->lastModified;
}
/**
* Parse Filter.
*
* @param string $filter
*
* @return array
*
* @throws Exception
*/
protected function parseFilter($filter)
{
$operatorsMap = ['!=', '>=', '<=', '=', '>', '<']; // Do not edit order of this array
//FIXME bug with >= <= operators
$operator = null;
foreach ($operatorsMap as $node) {
if (\strpos($filter, $node) !== false) {
$operator = $node;
break;
}
}
if (empty($operator)) {
throw new Exception('Invalid operator');
}
$filter = \explode($operator, $filter);
if (\count($filter) != 2) {
throw new Exception('Invalid filter expression');
}
return [
'key' => $filter[0],
'value' => $filter[1],
'operator' => $operator,
];
}
/**
* Get Data Type.
*
* Check value data type. return value can be on of the following:
* string, integer, float, boolean, object, list or null
*
* @param $value
*
* @return string
*
* @throws \Exception
*/
protected function getDataType($value)
{
switch (\gettype($value)) {
case 'string':
return self::DATA_TYPE_STRING;
break;
case 'integer':
return self::DATA_TYPE_INTEGER;
break;
case 'double':
return self::DATA_TYPE_FLOAT;
break;
case 'boolean':
return self::DATA_TYPE_BOOLEAN;
break;
case 'array':
if ((bool) \count(\array_filter(\array_keys($value), 'is_string'))) {
return self::DATA_TYPE_DICTIONARY;
}
return self::DATA_TYPE_ARRAY;
break;
case 'NULL':
return self::DATA_TYPE_NULL;
break;
}
throw new Exception('Unknown data type: '.$value.' ('.\gettype($value).')');
}
/**
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function setDebug(string $key, $value): self
{
$this->debug[$key] = $value;
return $this;
}
/**
* @return array
*/
public function getDebug(): array
{
return $this->debug;
}
/**
* return $this;.
*
* @return void
*/
public function resetDebug(): void
{
$this->debug = [];
}
/**
* @return PDO
*
* @throws Exception
*/
protected function getPDO()
{
return $this->pdo;
}
/**
* @throws Exception
*
* @return Redis
*/
protected function getRedis(): Redis
{
return $this->redis;
}
}

View file

@ -1,300 +0,0 @@
<?php
namespace Appwrite\Database\Adapter;
use Appwrite\Database\Adapter;
use Exception;
use Redis as Client;
class Redis extends Adapter
{
/**
* @var Client
*/
protected $redis;
/**
* @var Adapter
*/
protected $adapter;
/**
* Redis constructor.
*
* @param Adapter $adapter
* @param Client $redis
*/
public function __construct(Adapter $adapter, Client $redis)
{
$this->redis = $redis;
$this->adapter = $adapter;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function getDocument($id)
{
$output = \json_decode($this->getRedis()->get($this->getNamespace().':document-'.$id), true);
if (!$output) {
$output = $this->adapter->getDocument($id);
$this->getRedis()->set($this->getNamespace().':document-'.$id, \json_encode($output, JSON_UNESCAPED_UNICODE));
}
$output = $this->parseRelations($output);
return $output;
}
/**
* @param $output
*
* @return mixed
*
* @throws Exception
*/
protected function parseRelations($output)
{
$keys = [];
if (empty($output) || !isset($output['temp-relations'])) {
return $output;
}
foreach ($output['temp-relations'] as $relationship) {
$keys[] = $this->getNamespace().':document-'.$relationship['end'];
}
$nodes = (!empty($keys)) ? $this->getRedis()->mget($keys) : [];
foreach ($output['temp-relations'] as $i => $relationship) {
$node = $relationship['end'];
$node = (!empty($nodes[$i])) ? $this->parseRelations(\json_decode($nodes[$i], true)) : $this->getDocument($node);
if (empty($node)) {
continue;
}
if ($relationship['array']) {
$output[$relationship['key']][] = $node;
} else {
$output[$relationship['key']] = $node;
}
}
unset($output['temp-relations']);
return $output;
}
/**
* Create Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function createDocument(array $data = [], array $unique = [])
{
$data = $this->adapter->createDocument($data, $unique);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Update Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function updateDocument(array $data = [])
{
$data = $this->adapter->updateDocument($data);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Delete Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function deleteDocument(string $id)
{
$data = $this->adapter->deleteDocument($id);
$this->getRedis()->expire($this->getNamespace().':document-'.$id, 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$id, 0);
return $data;
}
/**
* Delete Unique Key.
*
* @param $key
*
* @return array
*
* @throws Exception
*/
public function deleteUniqueKey($key)
{
$data = $this->adapter->deleteUniqueKey($key);
return $data;
}
/**
* Add Unique Key.
*
* @param $key
*
* @return array
*
* @throws Exception
*/
public function addUniqueKey($key)
{
$data = $this->adapter->addUniqueKey($key);
return $data;
}
/**
* Create Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function createNamespace($namespace)
{
return $this->adapter->createNamespace($namespace);
}
/**
* Delete Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function deleteNamespace($namespace)
{
return $this->adapter->deleteNamespace($namespace);
}
/**
* @param array $options
* @param array $filterTypes
*
* @return array
*
* @throws Exception
*/
public function getCollection(array $options, array $filterTypes = [])
{
$data = $this->adapter->getCollection($options, $filterTypes);
$keys = [];
foreach ($data as $node) {
$keys[] = $this->getNamespace().':document-'.$node;
}
$nodes = (!empty($keys)) ? $this->getRedis()->mget($keys) : [];
foreach ($data as $i => &$node) {
$temp = (!empty($nodes[$i])) ? $this->parseRelations(\json_decode($nodes[$i], true)) : $this->getDocument($node);
if (!empty($temp)) {
$node = $temp;
}
}
return $data;
}
/**
* @param array $options
*
* @return int
*
* @throws Exception
*/
public function getCount(array $options)
{
return $this->adapter->getCount($options);
}
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in current session has been changed
*
* @return int
*/
public function lastModified()
{
return 0;
}
/**
* @return array
*/
public function getDebug()
{
return $this->adapter->getDebug();
}
/**
* @throws Exception
*
* @return Client
*/
protected function getRedis(): Client
{
return $this->redis;
}
/**
* Set Namespace.
*
* Set namespace to divide different scope of data sets
*
* @param $namespace
*
* @return bool
*
* @throws Exception
*/
public function setNamespace($namespace)
{
$this->adapter->setNamespace($namespace);
return parent::setNamespace($namespace);
}
}

View file

@ -1,274 +0,0 @@
<?php
namespace Appwrite\Database;
use ArrayObject;
class Document extends ArrayObject
{
const SET_TYPE_ASSIGN = 'assign';
const SET_TYPE_PREPEND = 'prepend';
const SET_TYPE_APPEND = 'append';
/**
* Construct.
*
* Construct a new fields object
*
* @see ArrayObject::__construct
*
* @param array $input
* @param int $flags
* @param string $iterator_class
*/
public function __construct($input = [], $flags = 0, $iterator_class = 'ArrayIterator')
{
foreach ($input as $key => &$value) {
if (\is_array($value)) {
if ((isset($value['$id']) || isset($value['$collection'])) && (!$value instanceof self)) {
$input[$key] = new self($value);
} else {
foreach ($value as $childKey => $child) {
if ((isset($child['$id']) || isset($child['$collection'])) && (!$child instanceof self)) {
$value[$childKey] = new self($child);
}
}
}
}
}
parent::__construct($input, $flags, $iterator_class);
}
/**
* @return string|null
*/
public function getId()
{
return $this->getAttribute('$id', null);
}
/**
* @return string
*/
public function getCollection()
{
return $this->getAttribute('$collection', null);
}
/**
* @return array
*/
public function getPermissions()
{
return $this->getAttribute('$permissions', []);
}
/**
* Get Attribute.
*
* Method for getting a specific fields attribute. If $name is not found $default value will be returned.
*
* @param string $name
* @param mixed $default
*
* @return mixed
*/
public function getAttribute($name, $default = null)
{
$name = \explode('.', $name);
$temp = &$this;
foreach ($name as $key) {
if (!isset($temp[$key])) {
return $default;
}
$temp = &$temp[$key];
}
return $temp;
}
/**
* Get Document Attributes
*
* @return array
*/
public function getAttributes(): array
{
$attributes = [];
foreach ($this as $attribute => $value) {
if(array_key_exists($attribute, ['$id' => true, '$permissions' => true, '$collection' => true, '$execute' => []])) {
continue;
}
$attributes[$attribute] = $value;
}
return $attributes;
}
/**
* Set Attribute.
*
* Method for setting a specific field attribute
*
* @param string $key
* @param mixed $value
* @param string $type
*
* @return mixed
*/
public function setAttribute($key, $value, $type = self::SET_TYPE_ASSIGN)
{
switch ($type) {
case self::SET_TYPE_ASSIGN:
$this[$key] = $value;
break;
case self::SET_TYPE_APPEND:
$this[$key] = (!isset($this[$key]) || !\is_array($this[$key])) ? [] : $this[$key];
\array_push($this[$key], $value);
break;
case self::SET_TYPE_PREPEND:
$this[$key] = (!isset($this[$key]) || !\is_array($this[$key])) ? [] : $this[$key];
\array_unshift($this[$key], $value);
break;
}
return $this;
}
/**
* Remove Attribute.
*
* Method for removing a specific field attribute
*
* @param string $key
* @param mixed $value
* @param string $type
*
* @return mixed
*/
public function removeAttribute($key)
{
if (isset($this[$key])) {
unset($this[$key]);
}
return $this;
}
/**
* Search.
*
* Get array child by key and value match
*
* @param $key
* @param $value
* @param array|null $scope
*
* @return Document|Document[]|mixed|null|array
*/
public function search($key, $value, $scope = null)
{
$array = (!\is_null($scope)) ? $scope : $this;
if (\is_array($array) || $array instanceof self) {
if (isset($array[$key]) && $array[$key] == $value) {
return $array;
}
foreach ($array as $k => $v) {
if ((\is_array($v) || $v instanceof self) && (!empty($v))) {
$result = $this->search($key, $value, $v);
if (!empty($result)) {
return $result;
}
} else {
if ($k === $key && $v === $value) {
return $array;
}
}
}
}
if ($array === $value) {
return $array;
}
return;
}
/**
* Checks if document has data.
*
* @return bool
*/
public function isEmpty()
{
return empty($this->getId());
}
/**
* Checks if a document key is set.
*
* @param string $key
*
* @return bool
*/
public function isSet($key)
{
return isset($this[$key]);
}
/**
* Get Array Copy.
*
* Outputs entity as a PHP array
*
* @param array $whitelist
* @param array $blacklist
*
* @return array
*/
public function getArrayCopy(array $whitelist = [], array $blacklist = []): array
{
$array = parent::getArrayCopy();
$output = [];
foreach ($array as $key => &$value) {
if (!empty($whitelist) && !\in_array($key, $whitelist)) { // Export only whitelisted fields
continue;
}
if (!empty($blacklist) && \in_array($key, $blacklist)) { // Don't export blacklisted fields
continue;
}
if ($value instanceof self) {
$output[$key] = $value->getArrayCopy($whitelist, $blacklist);
} elseif (\is_array($value)) {
foreach ($value as $childKey => &$child) {
if ($child instanceof self) {
$output[$key][$childKey] = $child->getArrayCopy($whitelist, $blacklist);
} else {
$output[$key][$childKey] = $child;
}
}
if (empty($value)) {
$output[$key] = $value;
}
} else {
$output[$key] = $value;
}
}
return $output;
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Authorization extends \Exception
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Duplicate extends \Exception
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Structure extends \Exception
{
}

View file

@ -1,215 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Document;
use Utopia\Validator;
class Authorization extends Validator
{
/**
* @var array
*/
public static $roles = ['*' => true];
/**
* @var Document
*/
protected $document;
/**
* @var string
*/
protected $action = '';
/**
* @var string
*/
protected $message = 'Authorization Error';
/**
* Structure constructor.
*
* @param Document $document
* @param string $action
*/
public function __construct(Document $document, $action)
{
$this->document = $document;
$this->action = $action;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $permissions
*
* @return bool
*/
public function isValid($permissions): bool
{
if (!self::$status) {
return true;
}
if (!isset($permissions[$this->action])) {
$this->message = 'Missing action key: "'.$this->action.'"';
return false;
}
$permission = null;
foreach ($permissions[$this->action] as $permission) {
$permission = \str_replace(':{self}', ':'.$this->document->getId(), $permission);
if (\array_key_exists($permission, self::$roles)) {
return true;
}
}
$this->message = 'Missing "'.$this->action.'" permission for role "'.$permission.'". Only this scopes "'.\json_encode(self::getRoles()).'" are given and only this are allowed "'.\json_encode($permissions[$this->action]).'".';
return false;
}
/**
* @param string $role
*
* @return void
*/
public static function setRole(string $role): void
{
self::$roles[$role] = true;
}
/**
* @param string $role
*
* @return void
*/
public static function unsetRole(string $role): void
{
unset(self::$roles[$role]);
}
/**
* @return array
*/
public static function getRoles(): array
{
return \array_keys(self::$roles);
}
/**
* @return void
*/
public static function cleanRoles(): void
{
self::$roles = [];
}
/**
* @param string $role
*
* @return bool
*/
public static function isRole(string $role): bool
{
return (\array_key_exists($role, self::$roles));
}
/**
* @var bool
*/
public static $status = true;
/**
* Default value in case we need
* to reset Authorization status
*
* @var bool
*/
public static $statusDefault = true;
/**
* Change default status.
* This will be used for the
* value set on the self::reset() method
*
* @return void
*/
public static function setDefaultStatus($status): void
{
self::$statusDefault = $status;
self::$status = $status;
}
/**
* Enable Authorization checks
*
* @return void
*/
public static function enable(): void
{
self::$status = true;
}
/**
* Disable Authorization checks
*
* @return void
*/
public static function disable(): void
{
self::$status = false;
}
/**
* Disable Authorization checks
*
* @return void
*/
public static function reset(): void
{
self::$status = self::$statusDefault;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_ARRAY;
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class Collection extends Structure
{
/**
* @var array
*/
protected $collections = [];
/**
* @var array
*/
protected $merge = [];
/**
* @param Database $database
* @param array $collections
* @param array $merge
*/
public function __construct(Database $database, array $collections, array $merge = [])
{
$this->collections = $collections;
$this->merge = $merge;
return parent::__construct($database);
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $document
*
* @return bool
*/
public function isValid($document): bool
{
$document = new Document(
\array_merge($this->merge, ($document instanceof Document) ? $document->getArrayCopy() : $document)
);
if (\is_null($document->getCollection())) {
$this->message = 'Missing collection attribute $collection';
return false;
}
if (!\in_array($document->getCollection(), $this->collections)) {
$this->message = 'Collection is not allowed';
return false;
}
return parent::isValid($document);
}
}

View file

@ -1,105 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Utopia\Validator;
class DocumentId extends Validator
{
/**
* @var string
*/
protected $message = 'Document not found.';
/**
* @var Database
*/
protected $database;
/**
* @var string
*/
protected $collection = '';
/**
* Structure constructor.
*
* @param Database $database
* @param string $collection
*/
public function __construct(Database $database, string $collection = '')
{
$this->database = $database;
$this->collection = $collection;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param $value
*
* @return bool
*/
public function isValid($id): bool
{
$document = $this->database->getDocument($id);
if (!$document) {
return false;
}
if (!$document instanceof Document) {
return false;
}
if (!$document->getId()) {
return false;
}
if ($document->getCollection() !== $this->collection) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Utopia\Validator;
class Key extends Validator
{
/**
* @var string
*/
protected $message = 'Parameter must contain only letters with no spaces or special chars and be shorter than 32 chars';
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param $value
*
* @return bool
*/
public function isValid($value): bool
{
if (!\is_string($value)) {
return false;
}
if (\preg_match('/[^A-Za-z0-9\-\_]/', $value)) {
return false;
}
if (\mb_strlen($value) > 32) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -1,100 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Document;
use Utopia\Validator;
class Permissions extends Validator
{
/**
* @var string
*/
protected $message = 'Permissions Error';
/**
* @var Document
*/
protected $document;
/**
* Structure constructor.
*
* @param Document $document
*/
public function __construct(Document $document)
{
$this->document = $document;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if (!\is_array($value) && !empty($value)) {
$this->message = 'Invalid permissions data structure';
return false;
}
foreach ($value as $action => $roles) {
if (!\in_array($action, ['read', 'write', 'execute'])) {
$this->message = 'Unknown action ("'.$action.'")';
return false;
}
foreach ($roles as $role) {
if (!\is_string($role)) {
$this->message = 'Permissions role must be of type string.';
return false;
}
}
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_ARRAY;
}
}

View file

@ -1,313 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Network\Validator as NetworkValidator;
use Utopia\Validator;
class Structure extends Validator
{
const RULE_TYPE_ID = 'id';
const RULE_TYPE_PERMISSIONS = 'permissions';
const RULE_TYPE_KEY = 'key';
const RULE_TYPE_TEXT = 'text';
const RULE_TYPE_MARKDOWN = 'markdown';
const RULE_TYPE_NUMERIC = 'numeric';
const RULE_TYPE_BOOLEAN = 'boolean';
const RULE_TYPE_EMAIL = 'email';
const RULE_TYPE_URL = 'url';
const RULE_TYPE_IP = 'ip';
const RULE_TYPE_WILDCARD = 'wildcard';
const RULE_TYPE_DOCUMENT = 'document';
const RULE_TYPE_DOCUMENTID = 'documentId';
const RULE_TYPE_FILEID = 'fileId';
/**
* @var Database
*/
protected $database;
/**
* @var string
*/
protected $id = '';
/**
* Basic rules to apply on all documents.
*
* @var array
*/
protected $rules = [
[
'label' => '$id',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$id',
'type' => 'id',
'default' => null,
'required' => false,
'array' => false,
],
[
'label' => '$collection',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$collection',
'type' => 'id',
'default' => null,
'required' => true,
'array' => false,
],
[
'label' => '$permissions',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$permissions',
'type' => 'permissions',
'default' => null,
'required' => true,
'array' => false,
],
[
'label' => '$createdAt',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$createdAt',
'type' => 'numeric',
'default' => null,
'required' => false,
'array' => false,
],
[
'label' => '$updatedAt',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$updatedAt',
'type' => 'numeric',
'default' => null,
'required' => false,
'array' => false,
],
];
/**
* @var string
*/
protected $message = 'General Error';
/**
* Structure constructor.
*
* @param Database $database
*/
public function __construct(Database $database)
{
$this->database = $database;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Invalid document structure: '.$this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $document
*
* @return bool
*/
public function isValid($document): bool
{
$document = (\is_array($document)) ? new Document($document) : $document;
$this->id = $document->getId();
if (\is_null($document->getCollection())) {
$this->message = 'Missing collection attribute $collection';
return false;
}
$collection = $this->getCollection($document->getCollection());
if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
$this->message = 'Collection not found';
return false;
}
$array = $document->getArrayCopy();
$rules = \array_merge($this->rules, $collection->getAttribute('rules', []));
foreach ($rules as $rule) { // Check all required keys are set
if (isset($rule['key']) && !isset($array[$rule['key']])
&& isset($rule['required']) && true == $rule['required']) {
$this->message = 'Missing required key "'.$rule['key'].'"';
return false;
}
}
foreach ($array as $key => $value) {
$rule = $collection->search('key', $key, $rules);
if (!$rule) {
continue;
}
$ruleType = $rule['type'] ?? '';
$ruleRequired = $rule['required'] ?? true;
$ruleArray = $rule['array'] ?? false;
$validator = null;
switch ($ruleType) {
case self::RULE_TYPE_ID:
$validator = new UID();
break;
case self::RULE_TYPE_PERMISSIONS:
$validator = new Permissions($document); //$validator = ($this->forcePermissions) ? new Authorization($original, 'write') : new Validator\Mock();
break;
case self::RULE_TYPE_KEY:
$validator = new Key();
break;
case self::RULE_TYPE_TEXT:
case self::RULE_TYPE_MARKDOWN:
$validator = new Validator\Text(0);
break;
case self::RULE_TYPE_NUMERIC:
$validator = new Validator\Numeric();
break;
case self::RULE_TYPE_BOOLEAN:
$validator = new Validator\Boolean();
break;
case self::RULE_TYPE_EMAIL:
$validator = new NetworkValidator\Email();
break;
case self::RULE_TYPE_URL:
$validator = new NetworkValidator\URL();
break;
case self::RULE_TYPE_IP:
$validator = new NetworkValidator\IP();
break;
case self::RULE_TYPE_WILDCARD:
$validator = new Validator\Wildcard();
break;
case self::RULE_TYPE_DOCUMENT:
$validator = new Collection($this->database, (isset($rule['list'])) ? $rule['list'] : []);
$value = $document->getAttribute($key);
break;
case self::RULE_TYPE_DOCUMENTID:
$validator = new DocumentId($this->database, (isset($rule['list']) && isset($rule['list'][0])) ? $rule['list'][0] : '');
$value = $document->getAttribute($key);
break;
case self::RULE_TYPE_FILEID:
$validator = new DocumentId($this->database, Database::SYSTEM_COLLECTION_FILES);
$value = $document->getAttribute($key);
break;
}
if (empty($validator)) { // Error creating validator for property
$this->message = 'Unknown rule type "'.$ruleType.'" for property "'.\htmlspecialchars($key, ENT_QUOTES, 'UTF-8').'"';
if (empty($ruleType)) {
$this->message = 'Unknown property "'.$key.'" type'.
'. Make sure to follow '.\strtolower($collection->getAttribute('name', 'unknown')).' collection structure';
}
return false;
}
if ($ruleRequired && ('' === $value || null === $value)) {
$this->message = 'Required property "'.$key.'" has no value';
return false;
}
if (!$ruleRequired && empty($value)) {
unset($array[$key]);
unset($rule);
continue;
}
if ($ruleArray) { // Array of values validation
if (!\is_array($value)) {
$this->message = 'Property "'.$key.'" must be an array';
return false;
}
// TODO add is required check here
foreach ($value as $node) {
if (!$validator->isValid($node)) { // Check if property is valid, if not required can also be empty
$this->message = 'Property "'.$key.'" has invalid input. '.$validator->getDescription();
return false;
}
}
} else { // Single value validation
if ((!$validator->isValid($value)) && !('' === $value && !$ruleRequired)) { // Error when value is not valid, and is not optional and empty
$this->message = 'Property "'.$key.'" has invalid input. '.$validator->getDescription();
return false;
}
}
unset($array[$key]);
unset($rule);
}
if (!empty($array)) { // No fields should be left unvalidated
$this->message = 'Unknown properties are not allowed ('.\implode(', ', \array_keys($array)).') for this collection'.
'. Make sure to follow '.\strtolower($collection->getAttribute('name', 'unknown')).' collection structure';
return false;
}
return true;
}
/**
* Get Collection
*
* Get Collection by unique ID
*
* @return Document
*/
protected function getCollection($id): Document
{
return $this->database->getDocument($id);
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_OBJECT;
}
}

View file

@ -1,70 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Utopia\Validator;
class UID extends Validator
{
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Invalid UID format';
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if ($value === 0) { // TODO Deprecate confition when we get the chance.
return true;
}
if (!is_string($value)) {
return false;
}
if (mb_strlen($value) > 32) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -290,7 +290,7 @@ class Realtime extends Adapter
$channels[] = '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();

View file

@ -2,107 +2,58 @@
namespace Appwrite\Migration;
use Appwrite\Database\Document as OldDocument;
use Appwrite\Database\Database as OldDatabase;
use PDO;
use Redis;
use Swoole\Runtime;
use Utopia\Database\Document;
use Utopia\Database\Database;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Exception;
abstract class Migration
{
/**
* @var array
*/
protected array $options;
/**
* @var PDO
*/
protected PDO $db;
/**
* @var Redis
*/
protected Redis $cache;
/**
* @var int
*/
protected int $limit = 500;
protected int $limit = 100;
/**
* @var OldDocument
* @var Document
*/
protected OldDocument $project;
protected Document $project;
/**
* @var OldDatabase
* @var Database
*/
protected OldDatabase $oldProjectDB;
protected Database $projectDB;
/**
* @var OldDatabase
* @var Database
*/
protected OldDatabase $oldConsoleDB;
protected Database $consoleDB;
/**
* @var array
*/
public static array $versions = [
'0.6.0' => 'V05',
'0.7.0' => 'V06',
'0.8.0' => 'V07',
'0.9.0' => 'V08',
'0.9.1' => 'V08',
'0.9.2' => 'V08',
'0.9.3' => 'V08',
'0.9.4' => 'V08',
'0.10.0' => 'V09',
'0.10.1' => 'V09',
'0.10.2' => 'V09',
'0.10.3' => 'V09',
'0.10.4' => 'V09',
'0.11.0' => 'V10',
'0.12.0' => 'V11',
'0.12.1' => 'V11',
'0.13.0' => 'V12',
];
/**
* Migration constructor.
*
* @param PDO $db
* @param Redis|null $cache
* @param array $options
* @return void
*/
public function __construct(PDO $db, Redis $cache = null, array $options = [])
{
$this->options = $options;
$this->db = $db;
if (!is_null($cache)) {
$this->cache = $cache;
}
}
/**
* Set project for migration.
*
* @param OldDocument $project
* @param OldDatabase $projectDB
* @param OldDatabase $oldConsoleDB
* @param Document $project
* @param Database $projectDB
* @param Database $oldConsoleDB
*
* @return self
*/
public function setProject(OldDocument $project, OldDatabase $projectDB, OldDatabase $oldConsoleDB): self
public function setProject(Document $project, Database $projectDB, Database $consoleDB): self
{
$this->project = $project;
$this->projectDB = $projectDB;
$this->projectDB->setNamespace('_project_' . $this->project->getId());
$this->oldProjectDB = $projectDB;
$this->oldProjectDB->setNamespace('app_' . $project->getId());
$this->oldConsoleDB = $oldConsoleDB;
$this->consoleDB = $consoleDB;
return $this;
}
@ -114,52 +65,72 @@ abstract class Migration
*/
public function forEachDocument(callable $callback): void
{
$sum = $this->limit;
$offset = 0;
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
while ($sum >= $this->limit) {
$all = $this->projectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
]);
/** @var array $collections */
$collections = Config::getParam('collections', []);
$sum = \count($all);
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
foreach ($collections as $collection) {
$sum = 0;
$nextDocument = null;
$collectionCount = $this->projectDB->count($collection['$id']);
Console::log('Migrating Collection ' . $collection['$id'] . ':');
Console::log('Migrating: ' . $offset . ' / ' . $this->projectDB->getSum());
\Co\run(function () use ($all, $callback) {
foreach ($all as $document) {
go(function () use ($document, $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
if ($document->getCollection() !== 0) {
Console::warning('Skipped Document due to missing ID or Collection.');
do {
$documents = $this->projectDB->find($collection['$id'], limit: $this->limit, cursor: $nextDocument);
$count = count($documents);
$sum += $count;
Console::log($sum . ' / ' . $collectionCount);
\Co\run(function (array $documents, callable $callback) {
foreach ($documents as $document) {
go(function (Document $document, callable $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
return;
}
return;
}
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
if (!$this->check_diff_multi($new->getArrayCopy(), $old)) {
return;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = call_user_func($callback, $attr);
}
try {
$new = $this->projectDB->overwriteDocument($new->getArrayCopy());
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
if ($document && $new->getId() !== $document->getId()) {
throw new Exception('Duplication Error');
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = call_user_func($callback, $child);
}
}
}
}
}
});
if (!$this->check_diff_multi($new->getArrayCopy(), $old)) {
return;
}
try {
$new = $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
if ($document && $new->getId() !== $document->getId()) {
throw new Exception('Duplication Error');
}
}
}, $document, $callback);
}
}, $documents, $callback);
if ($count !== $this->limit) {
$nextDocument = null;
} else {
$nextDocument = end($documents);
}
});
$offset += $this->limit;
} while (!is_null($nextDocument));
}
}

Some files were not shown because too many files have changed in this diff Show more