2022-07-06 19:15:59 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
|
|
|
|
list,
|
2022-07-08 15:14:48 +02:00
|
|
|
listByUser,
|
2022-07-06 19:15:59 +02:00
|
|
|
add,
|
|
|
|
|
get,
|
|
|
|
|
update,
|
2022-07-07 14:11:14 +02:00
|
|
|
remove,
|
|
|
|
|
getIcon
|
2022-07-06 19:15:59 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const assert = require('assert'),
|
2022-07-08 15:14:48 +02:00
|
|
|
apps = require('./apps.js'),
|
2022-07-06 19:15:59 +02:00
|
|
|
BoxError = require('./boxerror.js'),
|
2022-10-06 11:22:45 +02:00
|
|
|
database = require('./database.js'),
|
|
|
|
|
debug = require('debug')('box:applinks'),
|
|
|
|
|
jsdom = require('jsdom'),
|
2022-07-06 19:15:59 +02:00
|
|
|
safe = require('safetydance'),
|
2022-07-07 12:36:53 +02:00
|
|
|
superagent = require('superagent'),
|
2022-10-06 11:22:45 +02:00
|
|
|
uuid = require('uuid'),
|
|
|
|
|
validator = require('validator');
|
2022-07-06 19:15:59 +02:00
|
|
|
|
|
|
|
|
const APPLINKS_FIELDS= [ 'id', 'accessRestrictionJson', 'creationTime', 'updateTime', 'ts', 'label', 'tagsJson', 'icon', 'upstreamUri' ].join(',');
|
|
|
|
|
|
|
|
|
|
function postProcess(result) {
|
|
|
|
|
assert.strictEqual(typeof result, 'object');
|
|
|
|
|
|
|
|
|
|
assert(result.tagsJson === null || typeof result.tagsJson === 'string');
|
|
|
|
|
result.tags = safe.JSON.parse(result.tagsJson) || [];
|
|
|
|
|
delete result.tagsJson;
|
|
|
|
|
|
|
|
|
|
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
|
|
|
|
|
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
|
|
|
|
|
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
|
|
|
|
|
delete result.accessRestrictionJson;
|
|
|
|
|
|
2022-09-28 14:59:30 +02:00
|
|
|
result.ts = new Date(result.ts).getTime();
|
|
|
|
|
|
2022-07-07 14:11:14 +02:00
|
|
|
result.icon = result.icon ? result.icon : null;
|
2022-07-06 19:15:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function list() {
|
|
|
|
|
const results = await database.query(`SELECT ${APPLINKS_FIELDS} FROM applinks ORDER BY upstreamUri`);
|
|
|
|
|
|
|
|
|
|
results.forEach(postProcess);
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-08 15:14:48 +02:00
|
|
|
async function listByUser(user) {
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
|
|
|
|
|
|
|
|
|
const result = await list();
|
|
|
|
|
return result.filter((app) => apps.canAccess(app, user));
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-28 14:23:45 +02:00
|
|
|
async function detectMetaInfo(applink) {
|
2022-07-06 19:15:59 +02:00
|
|
|
assert.strictEqual(typeof applink, 'object');
|
|
|
|
|
|
2022-07-07 16:41:25 +02:00
|
|
|
const [error, response] = await safe(superagent.get(applink.upstreamUri));
|
2022-09-19 20:59:48 +02:00
|
|
|
if (error || !response.text) throw new BoxError(BoxError.BAD_FIELD, 'cannot fetch upstream uri for favicon and label');
|
2022-07-06 19:15:59 +02:00
|
|
|
|
2022-09-28 14:23:45 +02:00
|
|
|
// fixup upstreamUri to match the redirect
|
|
|
|
|
if (response.redirects && response.redirects.length) {
|
|
|
|
|
debug(`detectMetaInfo: found redirect from ${applink.upstreamUri} to ${response.redirects[0]}`);
|
|
|
|
|
applink.upstreamUri = response.redirects[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (applink.favicon && applink.label) return;
|
|
|
|
|
|
2022-07-07 16:41:25 +02:00
|
|
|
const dom = new jsdom.JSDOM(response.text);
|
2022-07-07 12:36:53 +02:00
|
|
|
if (!applink.icon) {
|
2022-07-07 18:19:53 +02:00
|
|
|
let favicon = '';
|
|
|
|
|
if (dom.window.document.querySelector('link[rel="apple-touch-icon"]')) favicon = dom.window.document.querySelector('link[rel="apple-touch-icon"]').href ;
|
2022-09-28 14:23:45 +02:00
|
|
|
if (!favicon.endsWith('.png') && dom.window.document.querySelector('meta[name="msapplication-TileImage"]')) favicon = dom.window.document.querySelector('meta[name="msapplication-TileImage"]').content ;
|
|
|
|
|
if (!favicon.endsWith('.png') && dom.window.document.querySelector('link[rel="shortcut icon"]')) favicon = dom.window.document.querySelector('link[rel="shortcut icon"]').href ;
|
|
|
|
|
if (!favicon.endsWith('.png') && dom.window.document.querySelector('link[rel="icon"]')) favicon = dom.window.document.querySelector('link[rel="icon"]').href ;
|
|
|
|
|
if (!favicon.endsWith('.png') && dom.window.document.querySelector('meta[itemprop="image"]')) favicon = dom.window.document.querySelector('meta[itemprop="image"]').content;
|
2022-07-07 12:36:53 +02:00
|
|
|
|
2022-07-07 18:19:53 +02:00
|
|
|
if (favicon) {
|
|
|
|
|
if (favicon.startsWith('/')) favicon = applink.upstreamUri + favicon;
|
|
|
|
|
|
|
|
|
|
const [error, response] = await safe(superagent.get(favicon));
|
|
|
|
|
if (error) console.error(`Failed to fetch icon ${favicon}: `, error);
|
|
|
|
|
else if (response.ok && response.headers['content-type'] === 'image/png') applink.icon = response.body;
|
|
|
|
|
else console.error(`Failed to fetch icon ${favicon}: statusCode=${response.status}`);
|
2022-09-28 14:23:45 +02:00
|
|
|
} else {
|
|
|
|
|
console.error(`Unable to find a suitable icon for ${applink.upstreamUri}`);
|
2022-07-07 12:36:53 +02:00
|
|
|
}
|
2022-07-07 16:41:25 +02:00
|
|
|
}
|
2022-07-07 12:36:53 +02:00
|
|
|
|
2022-07-07 18:19:53 +02:00
|
|
|
if (!applink.label) {
|
|
|
|
|
if (dom.window.document.querySelector('meta[property="og:title"]')) applink.label = dom.window.document.querySelector('meta[property="og:title"]').content;
|
|
|
|
|
else if (dom.window.document.querySelector('meta[property="og:site_name"]')) applink.label = dom.window.document.querySelector('meta[property="og:site_name"]').content;
|
|
|
|
|
else if (dom.window.document.title) applink.label = dom.window.document.title;
|
2022-07-07 12:36:53 +02:00
|
|
|
}
|
2022-07-07 16:41:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function add(applink) {
|
|
|
|
|
assert.strictEqual(typeof applink, 'object');
|
|
|
|
|
assert.strictEqual(typeof applink.upstreamUri, 'string');
|
|
|
|
|
|
|
|
|
|
debug(`add: ${applink.upstreamUri}`, applink);
|
|
|
|
|
|
2022-09-19 20:59:48 +02:00
|
|
|
if (applink.icon) {
|
|
|
|
|
if (!validator.isBase64(applink.icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64');
|
|
|
|
|
applink.icon = Buffer.from(applink.icon, 'base64');
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-28 14:23:45 +02:00
|
|
|
await detectMetaInfo(applink);
|
2022-07-06 19:15:59 +02:00
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
id: uuid.v4(),
|
|
|
|
|
accessRestrictionJson: applink.accessRestriction ? JSON.stringify(applink.accessRestriction) : null,
|
|
|
|
|
label: applink.label || '',
|
|
|
|
|
tagsJson: applink.tags ? JSON.stringify(applink.tags) : null,
|
|
|
|
|
icon: applink.icon || null,
|
|
|
|
|
upstreamUri: applink.upstreamUri
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const query = 'INSERT INTO applinks (id, accessRestrictionJson, label, tagsJson, icon, upstreamUri) VALUES (?, ?, ?, ?, ?, ?)';
|
|
|
|
|
const args = [ data.id, data.accessRestrictionJson, data.label, data.tagsJson, data.icon, data.upstreamUri ];
|
|
|
|
|
|
|
|
|
|
const [error] = await safe(database.query(query, args));
|
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
|
|
|
|
return data.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function get(applinkId) {
|
|
|
|
|
assert.strictEqual(typeof applinkId, 'string');
|
|
|
|
|
|
|
|
|
|
const result = await database.query(`SELECT ${APPLINKS_FIELDS} FROM applinks WHERE id = ?`, [ applinkId ]);
|
|
|
|
|
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'Applink not found');
|
|
|
|
|
|
|
|
|
|
postProcess(result[0]);
|
|
|
|
|
|
|
|
|
|
return result[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function update(applinkId, applink) {
|
|
|
|
|
assert.strictEqual(typeof applinkId, 'string');
|
|
|
|
|
assert.strictEqual(typeof applink, 'object');
|
|
|
|
|
assert.strictEqual(typeof applink.upstreamUri, 'string');
|
|
|
|
|
|
2022-09-28 15:10:17 +02:00
|
|
|
if (applink.icon) {
|
|
|
|
|
if (!validator.isBase64(applink.icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64');
|
|
|
|
|
applink.icon = Buffer.from(applink.icon, 'base64');
|
2022-07-08 17:47:53 +02:00
|
|
|
}
|
|
|
|
|
|
2022-09-28 14:23:45 +02:00
|
|
|
await detectMetaInfo(applink);
|
2022-07-07 16:41:25 +02:00
|
|
|
|
2022-07-07 19:44:59 +02:00
|
|
|
const query = 'UPDATE applinks SET label=?, icon=?, upstreamUri=?, tagsJson=?, accessRestrictionJson=? WHERE id = ?';
|
2022-09-28 15:10:17 +02:00
|
|
|
const args = [ applink.label, applink.icon || null, applink.upstreamUri, applink.tags ? JSON.stringify(applink.tags) : null, applink.accessRestriction ? JSON.stringify(applink.accessRestriction) : null, applinkId ];
|
2022-07-07 16:41:25 +02:00
|
|
|
|
|
|
|
|
const result = await database.query(query, args);
|
|
|
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Applink not found');
|
2022-07-06 19:15:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function remove(applinkId) {
|
|
|
|
|
assert.strictEqual(typeof applinkId, 'string');
|
|
|
|
|
|
2022-10-06 11:22:45 +02:00
|
|
|
const result = await database.query('DELETE FROM applinks WHERE id = ?', [ applinkId ]);
|
2022-07-06 19:15:59 +02:00
|
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Applink not found');
|
|
|
|
|
}
|
2022-07-07 14:11:14 +02:00
|
|
|
|
|
|
|
|
async function getIcon(applinkId) {
|
|
|
|
|
assert.strictEqual(typeof applinkId, 'string');
|
|
|
|
|
|
|
|
|
|
const applink = await get(applinkId);
|
|
|
|
|
|
|
|
|
|
return applink.icon;
|
|
|
|
|
}
|