123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- const APIError = require("../../api/APIError");
- const { Sequence } = require("../../codex/Sequence");
- const config = require("../../config");
- const { WorkList } = require("../../util/workutil");
- const validator = require('validator');
- const { get_user, get_app } = require("../../helpers");
- const { PermissionUtil } = require("../../services/auth/PermissionService");
- const FSNodeParam = require("../../api/filesystem/FSNodeParam");
- const { TYPE_DIRECTORY } = require("../../filesystem/FSNodeContext");
- const { UsernameNotifSelector } = require("../../services/NotificationService");
- const { quot } = require("../../util/strutil");
- /*
- This code is optimized for editors supporting folding.
- Fold at Level 2 to conveniently browse sequence steps.
- Fold at Level 3 after opening an inner-sequence.
-
- If you're using VSCode {
- typically "Ctrl+K, Ctrl+2" or "⌘K, ⌘2";
- to revert "Ctrl+K, Ctrl+J" or "⌘K, ⌘J";
- https://stackoverflow.com/questions/30067767
- }
- */
- module.exports = new Sequence([
- function validate_mode (a) {
- const req = a.get('req');
- const mode = req.body.mode;
-
- if ( mode === 'strict' ) {
- a.set('strict_mode', true);
- return;
- }
- if ( ! mode || mode === 'best-effort' ) {
- a.set('strict_mode', false);
- return;
- }
- throw APIError.create('field_invalid', null, {
- key: 'mode',
- expected: '`strict`, `best-effort`, or undefined',
- });
- },
- function validate_recipients (a) {
- const req = a.get('req');
- let recipients = req.body.recipients;
- // A string can be adapted to an array of one string
- if ( typeof recipients === 'string' ) {
- recipients = [recipients];
- }
- // Must be an array
- if ( ! Array.isArray(recipients) ) {
- throw APIError.create('field_invalid', null, {
- key: 'recipients',
- expected: 'array or string',
- got: typeof recipients,
- })
- }
- // At least one recipient
- if ( recipients.length < 1 ) {
- throw APIError.create('field_invalid', null, {
- key: 'recipients',
- expected: 'at least one',
- got: 'none',
- });
- }
- a.set('req_recipients', recipients);
- },
- function validate_shares (a) {
- const req = a.get('req');
- let shares = req.body.shares;
- if ( ! Array.isArray(shares) ) {
- shares = [shares];
- }
-
- // At least one share
- if ( shares.length < 1 ) {
- throw APIError.create('field_invalid', null, {
- key: 'shares',
- expected: 'at least one',
- got: 'none',
- });
- }
-
- a.set('req_shares', shares);
- },
- function initialize_result_object (a) {
- a.set('result', {
- $: 'api:share',
- $version: 'v0.0.0',
- status: null,
- recipients:
- Array(a.get('req_recipients').length).fill(null),
- shares:
- Array(a.get('req_shares').length).fill(null),
- serialize () {
- const result = this;
- for ( let i=0 ; i < result.recipients.length ; i++ ) {
- if ( ! result.recipients[i] ) continue;
- if ( result.recipients[i] instanceof APIError ) {
- result.status = 'mixed';
- result.recipients[i] = result.recipients[i].serialize();
- }
- }
- for ( let i=0 ; i < result.shares.length ; i++ ) {
- if ( ! result.shares[i] ) continue;
- if ( result.shares[i] instanceof APIError ) {
- result.status = 'mixed';
- result.shares[i] = result.shares[i].serialize();
- }
- }
- delete result.serialize;
- return result;
- }
- });
- },
- function initialize_worklists (a) {
- const recipients_work = new WorkList();
- const shares_work = new WorkList();
-
- const { req_recipients, req_shares } = a.values();
-
- // track: common operations on multiple items
-
- for ( let i=0 ; i < req_recipients.length ; i++ ) {
- const value = req_recipients[i];
- recipients_work.push({ i, value });
- }
-
- for ( let i=0 ; i < req_shares.length ; i++ ) {
- const value = req_shares[i];
- shares_work.push({ i, value });
- }
-
- recipients_work.lockin();
- shares_work.lockin();
-
- a.values({ recipients_work, shares_work });
- },
- new Sequence({ name: 'process recipients',
- after_each (a) {
- const { recipients_work } = a.values();
- recipients_work.clear_invalid();
- }
- }, [
- function valid_username_or_email (a) {
- const { result, recipients_work } = a.values();
- for ( const item of recipients_work.list() ) {
- const { value, i } = item;
-
- if ( typeof value !== 'string' ) {
- item.invalid = true;
- result.recipients[i] =
- APIError.create('invalid_username_or_email', null, {
- value,
- });
- continue;
- }
- if ( value.match(config.username_regex) ) {
- item.type = 'username';
- continue;
- }
- if ( validator.isEmail(value) ) {
- item.type = 'email';
- continue;
- }
-
- item.invalid = true;
- result.recipients[i] =
- APIError.create('invalid_username_or_email', null, {
- value,
- });
- }
- },
- async function check_existing_users_for_email_shares (a) {
- const { recipients_work } = a.values();
- for ( const recipient_item of recipients_work.list() ) {
- if ( recipient_item.type !== 'email' ) continue;
- const user = await get_user({
- email: recipient_item.value,
- });
- if ( ! user ) continue;
- recipient_item.type = 'username';
- recipient_item.value = user.username;
- }
- },
- async function check_username_specified_users_exist (a) {
- const { result, recipients_work } = a.values();
- for ( const item of recipients_work.list() ) {
- if ( item.type !== 'username' ) continue;
- const user = await get_user({ username: item.value });
- if ( ! user ) {
- item.invalid = true;
- result.recipients[item.i] =
- APIError.create('user_does_not_exist', null, {
- username: item.value,
- });
- continue;
- }
- item.user = user;
- }
- }
- ]),
- new Sequence({ name: 'process shares',
- beforeEach (a) {
- const { shares_work } = a.values();
- shares_work.clear_invalid();
- }
- }, [
- function validate_share_types (a) {
- const { result, shares_work } = a.values();
-
- const lib_typeTagged = a.iget('services').get('lib-type-tagged');
-
- for ( const item of shares_work.list() ) {
- const { i } = item;
- let { value } = item;
-
- const thing = lib_typeTagged.process(value);
- if ( thing.$ === 'error' ) {
- item.invalid = true;
- result.shares[i] =
- APIError.create('format_error', null, {
- message: thing.message
- });
- continue;
- }
-
- const allowed_things = ['fs-share', 'app-share'];
- if ( ! allowed_things.includes(thing.$) ) {
- item.invalid = true;
- result.shares[i] =
- APIError.create('disallowed_thing', null, {
- thing: thing.$,
- accepted: allowed_things,
- });
- continue;
- }
-
- item.thing = thing;
- }
- },
- function create_file_share_intents (a) {
- const { result, shares_work } = a.values();
- for ( const item of shares_work.list() ) {
- const { thing } = item;
- if ( thing.$ !== 'fs-share' ) continue;
- item.type = 'fs';
- const errors = [];
- if ( ! thing.path ) {
- errors.push('`path` is required');
- }
- let access = thing.access;
- if ( access ) {
- if ( ! ['read','write'].includes(access) ) {
- errors.push('`access` should be `read` or `write`');
- }
- } else access = 'read';
- if ( errors.length ) {
- item.invalid = true;
- result.shares[item.i] =
- APIError.create('field_errors', null, {
- key: `shares[${item.i}]`,
- errors
- });
- continue;
- }
-
- item.path = thing.path;
- item.share_intent = {
- $: 'share-intent:file',
- permissions: [PermissionUtil.join('fs', thing.path, access)],
- };
- }
- },
- function create_app_share_intents (a) {
- const { result, shares_work } = a.values();
- for ( const item of shares_work.list() ) {
- const { thing } = item;
- if ( thing.$ !== 'app-share' ) continue;
- item.type = 'app';
- const errors = [];
- if ( ! thing.uid && ! thing.name ) {
- errors.push('`uid` or `name` is required');
- }
- if ( errors.length ) {
- item.invalid = true;
- result.shares[item.i] =
- APIError.create('field_errors', null, {
- key: `shares[${item.i}]`,
- errors
- });
- continue;
- }
-
- const app_selector = thing.uid
- ? `uid#${thing.uid}` : thing.name;
-
- item.share_intent = {
- $: 'share-intent:app',
- permissions: [
- PermissionUtil.join('app', app_selector, 'access')
- ]
- }
- continue;
- }
- },
- async function fetch_nodes_for_file_shares (a) {
- const { req, result, shares_work } = a.values();
- for ( const item of shares_work.list() ) {
- if ( item.type !== 'fs' ) continue;
- const node = await (new FSNodeParam('path')).consolidate({
- req, getParam: () => item.path
- });
-
- if ( ! await node.exists() ) {
- item.invalid = true;
- result.shares[item.i] = APIError.create('subject_does_not_exist', {
- path: item.path,
- })
- continue;
- }
-
- item.node = node;
- let email_path = item.path;
- let is_dir = true;
- if ( await node.get('type') !== TYPE_DIRECTORY ) {
- is_dir = false;
- // remove last component
- email_path = email_path.slice(0, item.path.lastIndexOf('/')+1);
- }
- if ( email_path.startsWith('/') ) email_path = email_path.slice(1);
- const email_link = `${config.origin}/show/${email_path}`;
- item.is_dir = is_dir;
- item.email_link = email_link;
- }
- },
- async function fetch_apps_for_app_shares (a) {
- const { result, shares_work } = a.values();
- const db = a.iget('db');
-
- for ( const item of shares_work.list() ) {
- if ( item.type !== 'app' ) continue;
- const { thing } = item;
- const app = await get_app(thing.uid ?
- { uid: thing.uid } : { name: thing.name });
- if ( ! app ) {
- item.invalid = true;
- result.shares[item.i] =
- // note: since we're reporting `entity_not_found`
- // we will report the id as an entity-storage-compatible
- // identifier.
- APIError.create('entity_not_found', null, {
- identifier: thing.uid
- ? { uid: thing.uid }
- : { id: { name: thing.name } }
- });
- }
-
- app.metadata = db.case({
- mysql: () => app.metadata,
- otherwise: () => JSON.parse(app.metadata ?? '{}')
- })();
-
- item.app = app;
- }
- },
- async function add_subdomain_permissions (a) {
- const { shares_work } = a.values();
- const actor = a.get('actor');
- const db = a.iget('db');
- for ( const item of shares_work.list() ) {
- if ( item.type !== 'app' ) continue;
- const [subdomain] = await db.read(
- `SELECT * FROM subdomains WHERE associated_app_id = ? ` +
- `AND user_id = ? LIMIT 1`,
- [item.app.id, actor.type.user.id]
- );
- if ( ! subdomain ) continue;
-
- // The subdomain is also owned by this user, so we'll
- // add a permission for that as well
-
- const site_selector = `uid#${subdomain.uuid}`;
- item.share_intent.permissions.push(
- PermissionUtil.join('site', site_selector, 'access')
- )
- }
- },
- async function add_appdata_permissions (a) {
- const { result, shares_work } = a.values();
- for ( const item of shares_work.list() ) {
- if ( item.type !== 'app' ) continue;
- if ( ! item.app.metadata?.shared_appdata ) continue;
-
- const app_owner = await get_user({ id: item.app.owner_user_id });
-
- const appdatadir =
- `/${app_owner.username}/AppData/${item.app.uid}`;
- const appdatadir_perm =
- PermissionUtil.join('fs', appdatadir, 'write');
- item.share_intent.permissions.push(appdatadir_perm);
- }
- },
- function apply_success_status_to_shares (a) {
- const { result, shares_work } = a.values();
- for ( const item of shares_work.list() ) {
- result.shares[item.i] =
- {
- $: 'api:status-report',
- status: 'success',
- fields: {
- permission: item.permission,
- }
- };
- }
- },
- ]),
- function abort_on_error_if_mode_is_strict (a) {
- const strict_mode = a.get('strict_mode');
- if ( ! strict_mode ) return;
-
- const result = a.get('result');
- if (
- result.recipients.some(v => v !== null) ||
- result.shares.some(v => v !== null)
- ) {
- result.serialize();
- result.status = 'aborted';
- const res = a.get('res');
- res.status(218).send(result);
- a.stop();
- }
- },
- function early_return_on_dry_run (a) {
- if ( ! a.get('req').body.dry_run ) return;
-
- const { res, result, recipients_work } = a.values();
- for ( const item of recipients_work.list() ) {
- result.recipients[item.i] =
- { $: 'api:status-report', status: 'success' };
- }
-
- result.serialize();
- result.status = 'success';
- result.dry_run = true;
- res.send(result);
- a.stop();
- },
- async function grant_permissions_to_existing_users (a) {
- const {
- req, result, recipients_work, shares_work
- } = a.values();
-
- const svc_permission = a.iget('services').get('permission');
- const svc_notification = a.iget('services').get('notification');
-
- const actor = a.get('actor');
- for ( const recipient_item of recipients_work.list() ) {
- if ( recipient_item.type !== 'username' ) continue;
-
- const username = recipient_item.user.username;
- for ( const share_item of shares_work.list() ) {
- const permissions = share_item.share_intent.permissions;
- for ( const perm of permissions ) {
- await svc_permission.grant_user_user_permission(
- actor,
- username,
- perm,
- );
- }
- }
-
- const files = []; {
- for ( const item of shares_work.list() ) {
- if ( item.type !== 'file' ) continue;
- files.push(
- await item.node.getSafeEntry(),
- );
- }
- }
- const apps = []; {
- for ( const item of shares_work.list() ) {
- if ( item.type !== 'app' ) continue;
- // TODO: is there a general way to create a
- // client-safe app right now without
- // going through entity storage?
- // track: manual safe object
- apps.push(item.name
- ? item.name : await get_app({
- uid: item.uid,
- }));
- }
- }
-
- svc_notification.notify(UsernameNotifSelector(username), {
- source: 'sharing',
- icon: 'shared.svg',
- title: 'Files were shared with you!',
- template: 'file-shared-with-you',
- fields: {
- username: actor.type.user.username,
- files,
- },
- text: `The user ${quot(req.user.username)} shared ` +
- `${files.length} ` +
- (files.length === 1 ? 'file' : 'files') + ' ' +
- 'with you.',
- });
-
- result.recipients[recipient_item.i] =
- { $: 'api:status-report', status: 'success' };
- }
- },
- async function email_the_email_recipients (a) {
- const { actor, recipients_work, shares_work } = a.values();
-
- const svc_share = a.iget('services').get('share');
- const svc_token = a.iget('services').get('token');
- const svc_email = a.iget('services').get('token');
-
- for ( const recipient_item of recipients_work.list() ) {
- if ( recipient_item.type !== 'email' ) continue;
-
- const email = recipient_item.value;
-
- // data that gets stored in the `data` column of the share
- const data = {
- $: 'internal:share',
- $v: 'v0.0.0',
- permissions: [],
- };
-
- for ( const share_item of shares_work.list() ) {
- const permissions = share_item.share_intent.permissions;
- data.permissions.push(...permissions);
- }
-
- // track: scoping iife
- const share_token = await (async () => {
- const share_uid = await svc_share.create_share({
- issuer: actor,
- email,
- data,
- });
- return svc_token.sign('share', {
- $: 'token:share',
- $v: '0.0.0',
- uid: share_uid,
- }, {
- expiresIn: '14d'
- });
- })();
-
- const email_link =
- `${config.origin}?share_token=${share_token}`;
-
- await svc_email.send_email({ email }, 'share_by_email', {
- link: email_link,
- });
- }
- },
- function send_result (a) {
- const { res, result } = a.values();
- result.serialize();
- res.send(result);
- }
- ]);
|