Merge branch 'blocking' into 4.14

This commit is contained in:
ansuz 2022-03-22 12:15:13 +05:30
commit b6c9c3cabe
14 changed files with 341 additions and 150 deletions

View file

@ -19,6 +19,16 @@ server {
set $main_domain "your-main-domain.com";
set $sandbox_domain "your-sandbox-domain.com";
# By default CryptPad allows remote domains to embed CryptPad documents in iframes.
# This behaviour can be blocked by changing $allowed_origins from "*" to the
# sandbox domain, which must be permitted to load content from the main domain
# in order for CryptPad to work as expected.
#
# An example is given below which can be uncommented if you want to block
# remote sites from including content from your server
set $allowed_origins "*";
# set $allowed_origins "https://${sandbox_domain}";
# CryptPad's dynamic content (websocket traffic and encrypted blobs)
# can be served over separate domains. Using dedicated domains (or subdomains)
# for these purposes allows you to move them to a separate machine at a later date
@ -58,7 +68,7 @@ server {
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Origin "${allowed_origins}";
# add_header X-Frame-Options "SAMEORIGIN";
# Opt out of Google's FLoC Network
@ -115,6 +125,9 @@ server {
# script-src specifies valid sources for javascript, including inline handlers
set $scriptSrc "'self' resource: https://${main_domain}";
# XXX frame-ancestors defines where your cryptpad instance can be embedded...
set $frameAncestors "https://${main_domain} $https://${sandbox_domain}";
set $unsafe 0;
# the following assets are loaded via the sandbox domain
# they unfortunately still require exceptions to the sandboxing to work correctly.
@ -135,7 +148,7 @@ server {
}
# Finally, set all the rules you composed above.
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc;";
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc; frame-ancestors $frameAncestors";
# The nodejs process can handle all traffic whether accessed over websocket or as static assets
# We prefer to serve static content from nginx directly and to leave the API server to handle
@ -183,7 +196,7 @@ server {
# encrypted blobs are immutable and are thus cached for a year
location ^~ /blob/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Origin' "${allowed_origins}";
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Max-Age' 1728000;
@ -192,7 +205,7 @@ server {
return 204;
}
add_header Cache-Control max-age=31536000;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Origin' "${allowed_origins}";
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Content-Length';

View file

@ -324,6 +324,7 @@ var setLastEviction = function (Env, Server, cb, data, unsafeKey) {
var instanceStatus = function (Env, Server, cb) {
cb(void 0, {
restrictRegistration: Env.restrictRegistration,
disableEmbedding: Env.disableEmbedding,
launchTime: Env.launchTime,
currentTime: +new Date(),

View file

@ -53,6 +53,8 @@ ADD_INVITE
REVOKE_INVITE
REDEEM_INVITE
DISABLE_EMBEDDING
// 2.0
Env.DEV_MODE || Env.FRESH_MODE,
@ -92,6 +94,9 @@ var makeBooleanSetter = function (attr) {
};
};
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_EMBEDDING', [true]]], console.log)
commands.DISABLE_EMBEDDING = makeBooleanSetter('disableEmbedding');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
commands.RESTRICT_REGISTRATION = makeBooleanSetter('restrictRegistration');

View file

@ -1,7 +1,8 @@
var Default = module.exports;
Default.commonCSP = function (domain, sandbox) {
domain = ' ' + domain;
Default.commonCSP = function (Env) {
var domain = ' ' + Env.httpUnsafeOrigin;
var sandbox = Env.httpSafeOrigin;
sandbox = (sandbox && sandbox !== domain? ' ' + sandbox: '');
// Content-Security-Policy
@ -31,25 +32,25 @@ Default.commonCSP = function (domain, sandbox) {
"media-src blob:",
// for accounts.cryptpad.fr authentication and cross-domain iframe sandbox
"frame-ancestors *",
Env.disableEmbedding? `frame-ancestors ${domain}${sandbox}`: "frame-ancestors *",
"worker-src 'self'",
""
];
};
Default.contentSecurity = function (domain, sandbox) {
return (Default.commonCSP(domain, sandbox).join('; ') + "script-src 'self' resource: " + domain).replace(/\s+/g, ' ');
Default.contentSecurity = function (Env) {
return (Default.commonCSP(Env).join('; ') + "script-src 'self' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
};
Default.padContentSecurity = function (domain, sandbox) {
return (Default.commonCSP(domain, sandbox).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + domain).replace(/\s+/g, ' ');
Default.padContentSecurity = function (Env) {
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
};
Default.httpHeaders = function () {
Default.httpHeaders = function (Env) {
return {
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Origin": Env.disableEmbedding? Env.permittedEmbedders: "*",
"Permissions-policy":"interest-cohort=()"
};
};

View file

@ -21,13 +21,45 @@ var isValidPort = function (p) {
return typeof(p) === 'number' && p < 65535;
};
var deriveSandboxOrigin = function (unsafe, port) {
var url = new URL(unsafe);
url.port = port;
return url.origin;
};
module.exports.create = function (config) {
var httpUnsafeOrigin = canonicalizeOrigin(config.httpUnsafeOrigin);
var httpSafeOrigin;
var NO_SANDBOX = false;
var httpSafePort;
var httpPort = isValidPort(config.httpPort)? config.httpPort: 3000;
if (typeof(config.httpSafeOrigin) !== 'string') {
NO_SANDBOX = true;
if (typeof(config.httpSafePort) !== 'number') { httpSafePort = httpPort + 1; }
httpSafeOrigin = deriveSandboxOrigin(httpUnsafeOrigin, httpSafePort);
} else {
httpSafeOrigin = canonicalizeOrigin(config.httpSafeOrigin);
}
var permittedEmbedders = config.permittedEmbedders;
if (typeof(permittedEmbedders) === 'string') {
permittedEmbedders = permittedEmbedders.trim();
}
const Env = {
fileHost: config.fileHost, // XXX
NO_SANDBOX: NO_SANDBOX,
httpSafePort: httpSafePort,
version: Package.version,
installMethod: config.installMethod || undefined,
httpUnsafeOrigin: canonicalizeOrigin(config.httpUnsafeOrigin),
httpSafeOrigin: canonicalizeOrigin(config.httpSafeOrigin),
httpUnsafeOrigin: httpUnsafeOrigin,
httpSafeOrigin: httpSafeOrigin,
permittedEmbedders: typeof(permittedEmbedders) === 'string' && permittedEmbedders? permittedEmbedders: httpSafeOrigin,
removeDonateButton: config.removeDonateButton,
httpPort: isValidPort(config.httpPort)? config.httpPort: 3000,
httpAddress: typeof(config.httpAddress) === 'string'? config.httpAddress: '127.0.0.1',
@ -39,9 +71,19 @@ module.exports.create = function (config) {
DEV_MODE: false,
configCache: {},
broadcastCache: {},
officeHeadersCache: undefined,
standardHeadersCache: undefined,
apiHeadersCache: undefined,
flushCache: function () {
Env.configCache = {};
Env.broadcastCache = {};
Env.officeHeadersCache = undefined;
Env.standardHeadersCache = undefined;
Env.apiHeadersCache = undefined;
Env.FRESH_KEY = +new Date();
if (!(Env.DEV_MODE || Env.FRESH_MODE)) { Env.FRESH_MODE = true; }
if (!Env.Log) { return; }
@ -62,7 +104,6 @@ module.exports.create = function (config) {
archiveRetentionTime: config.archiveRetentionTime,
accountRetentionTime: config.accountRetentionTime,
// TODO implement mutability
adminEmail: config.adminEmail,
supportMailbox: config.supportMailboxPublicKey,
@ -112,6 +153,9 @@ module.exports.create = function (config) {
}
},
// as of 4.14.0 you need to opt-in to remote embedding.
disableEmbedding: true,
/* FIXME restrictRegistration is initialized as false and then overridden by admin decree
There is a narrow window in which someone could register before the server updates this value.
See also the cached 'restrictRegistration' value in server.js#serveConfig

115
server.js
View file

@ -23,34 +23,35 @@ var fancyURL = function (domain, path) {
return false;
};
var deriveSandboxOrigin = function (unsafe, port) {
var url = new URL(unsafe);
url.port = port;
return url.origin;
};
(function () {
// you absolutely must provide an 'httpUnsafeOrigin' (a truthy string)
if (!Env.httpUnsafeOrigin || typeof(Env.httpUnsafeOrigin) !== 'string') {
if (typeof(Env.httpUnsafeOrigin) !== 'string' || !Env.httpUnsafeOrigin.trim()) {
throw new Error("No 'httpUnsafeOrigin' provided");
}
if (typeof(Env.httpSafeOrigin) !== 'string') {
Env.NO_SANDBOX = true;
if (typeof(Env.httpSafePort) !== 'number') {
Env.httpSafePort = Env.httpPort + 1;
}
Env.httpSafeOrigin = deriveSandboxOrigin(Env.httpUnsafeOrigin, Env.httpSafePort);
}
}());
var applyHeaderMap = function (res, map) {
for (let header in map) { res.setHeader(header, map[header]); }
for (let header in map) {
if (typeof(map[header]) === 'string') { res.setHeader(header, map[header]); }
}
};
var setHeaders = (function () {
// load the default http headers unless the admin has provided their own via the config file
var headers;
var EXEMPT = [
/^\/common\/onlyoffice\/.*\.html.*/,
/^\/(sheet|presentation|doc)\/inner\.html.*/,
/^\/unsafeiframe\/inner\.html.*$/,
];
var cacheHeaders = function (Env, key, headers) {
if (Env.DEV_MODE) { return; }
Env[key] = headers;
};
var getHeaders = function (Env, type) {
var key = type + 'HeadersCache';
if (Env[key]) { return Env[key]; }
var headers = {};
var custom = config.httpHeaders;
// if the admin provided valid http headers then use them
@ -58,62 +59,46 @@ var setHeaders = (function () {
headers = Util.clone(custom);
} else {
// otherwise use the default
headers = Default.httpHeaders();
headers = Default.httpHeaders(Env);
}
// next define the base Content Security Policy (CSP) headers
if (typeof(config.contentSecurity) === 'string') {
headers['Content-Security-Policy'] = config.contentSecurity;
if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' }
if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) {
// backward compat for those who do not merge the new version of the config
// when updating. This prevents endless spinner if someone clicks donate.
// It also fixes the cross-domain iframe.
headers['Content-Security-Policy'] += "frame-ancestors *;";
}
} else {
// use the default CSP headers constructed with your domain
headers['Content-Security-Policy'] = Default.contentSecurity(Env.httpUnsafeOrigin, Env.httpSafeOrigin);
}
headers['Content-Security-Policy'] = type === 'office'?
Default.padContentSecurity(Env):
Default.contentSecurity(Env);
const padHeaders = Util.clone(headers);
if (typeof(config.padContentSecurity) === 'string') {
padHeaders['Content-Security-Policy'] = config.padContentSecurity;
} else {
padHeaders['Content-Security-Policy'] = Default.padContentSecurity(Env.httpUnsafeOrigin, Env.httpSafeOrigin);
}
if (Object.keys(headers).length) {
return function (req, res) {
if (Env.NO_SANDBOX) { // handles correct configuration for local development
// https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs
applyHeaderMap(res, {
"Cross-Origin-Resource-Policy": 'cross-origin',
"Cross-Origin-Embedder-Policy": 'require-corp',
});
headers["Cross-Origin-Resource-Policy"] = 'cross-origin';
headers["Cross-Origin-Embedder-Policy"] = 'require-corp';
}
// Don't set CSP headers on /api/ endpoints
// because they aren't necessary and they cause problems
// when duplicated by NGINX in production environments
if (/^\/api\/(broadcast|config)/.test(req.url)) { return; }
if (type === 'api') {
cacheHeaders(Env, key, headers);
return headers;
}
applyHeaderMap(res, {
"Cross-Origin-Resource-Policy": 'cross-origin',
});
headers["Cross-Origin-Resource-Policy"] = 'cross-origin';
cacheHeaders(Env, key, headers);
return headers;
};
// targeted CSP, generic policies, maybe custom headers
const h = [
/^\/common\/onlyoffice\/.*\.html.*/,
/^\/(sheet|presentation|doc)\/inner\.html.*/,
/^\/unsafeiframe\/inner\.html.*$/,
].some((regex) => {
return regex.test(req.url);
}) ? padHeaders : headers;
var setHeaders = function (req, res) {
var type;
if (EXEMPT.some(regex => regex.test(req.url))) {
type = 'office';
} else if (/^\/api\/(broadcast|config)/.test(req.url)) {
type = 'api';
} else {
type = 'standard';
}
var h = getHeaders(Env, type);
//console.log('PEWPEW', type, h);
applyHeaderMap(res, h);
};
}
return function () {};
}());
(function () {
if (!config.logFeedback) { return; }
@ -135,7 +120,7 @@ app.use('/blob', function (req, res, next) {
if (req.method === 'HEAD') {
Express.static(Path.join(__dirname, Env.paths.blob), {
setHeaders: function (res, path, stat) {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Origin', Env.disableEmbedding? Env.permittedEmbedders: '*');
res.set('Access-Control-Allow-Headers', 'Content-Length');
res.set('Access-Control-Expose-Headers', 'Content-Length');
}
@ -147,9 +132,9 @@ app.use('/blob', function (req, res, next) {
app.use(function (req, res, next) {
if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Origin', Env.disableEmbedding? Env.permittedEmbedders: '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range');
res.setHeader('Access-Control-Allow-Headers', 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Access-Control-Allow-Origin');
res.setHeader('Access-Control-Max-Age', 1728000);
res.setHeader('Content-Type', 'application/octet-stream; charset=utf-8');
res.setHeader('Content-Length', 0);
@ -249,6 +234,8 @@ var serveConfig = makeRouteCache(function (host) {
premiumUploadSize: Env.premiumUploadSize,
restrictRegistration: Env.restrictRegistration,
httpSafeOrigin: Env.httpSafeOrigin,
disableEmbedding: Env.disableEmbedding,
fileHost: Env.fileHost,
}, null, '\t'),
'});'
].join(';\n')

View file

@ -54,6 +54,7 @@ define([
'cp-admin-flush-cache',
'cp-admin-update-limit',
'cp-admin-registration',
'cp-admin-disableembeds',
'cp-admin-email',
'cp-admin-instance-info-notice',
@ -293,40 +294,6 @@ define([
return $div;
};
create['registration'] = function () {
var key = 'registration';
var $div = makeBlock(key); // Msg.admin_registrationHint, .admin_registrationTitle, .admin_registrationButton
var state = APP.instanceStatus.restrictRegistration;
var $cbox = $(UI.createCheckbox('cp-settings-' + key,
Messages.admin_registrationTitle,
state, { label: { class: 'noTitle' } }));
var spinner = UI.makeSpinner($cbox);
var $checkbox = $cbox.find('input').on('change', function() {
spinner.spin();
var val = $checkbox.is(':checked') || false;
$checkbox.attr('disabled', 'disabled');
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['RESTRICT_REGISTRATION', [val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
spinner.done();
state = APP.instanceStatus.restrictRegistration;
$checkbox[0].checked = state;
$checkbox.removeAttr('disabled');
});
});
});
$cbox.appendTo($div);
return $div;
};
var makeAdminCheckbox = function (data) {
return function () {
var state = data.getState();
@ -354,7 +321,16 @@ define([
};
};
// Msg.admin_registrationHint, .admin_registrationTitle, .admin_registrationButton
Messages.admin_cacheEvictionRequired = "Your server's internal state has been updated, but you may need to use the 'flush cache' button for clients to experience the intended effect."; // XXX
Messages.admin_reviewCheckupNotice = "It is also recommended that you review this instance's checkup page to confirm that it is configured correctly."; // XXX
var flushCacheNotice = function () {
UI.alert(h('span', [
h('p', Messages.admin_cacheEvictionRequired),
h('p', Messages.admin_reviewCheckupNotice),
]));
};
// Msg.admin_registrationHint, .admin_registrationTitle
create['registration'] = makeAdminCheckbox({
key: 'registration',
getState: function () {
@ -371,6 +347,32 @@ define([
}
APP.updateStatus(function () {
setState(APP.instanceStatus.restrictRegistration);
flushCacheNotice();
});
});
},
});
Messages.admin_disableembedsTitle = "Disable remote embedding"; // XXX
Messages.admin_disableembedsHint = "Remove options to embed pads and media-tags hosted on third party websites from sharing menus."; // XXX
// Msg.admin_disableembedsHint, .admin_disableembedsTitle
create['disableembeds'] = makeAdminCheckbox({
key: 'disableembeds',
getState: function () {
return APP.instanceStatus.disableEmbedding;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['DISABLE_EMBEDDING', [val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(APP.instanceStatus.disableEmbedding);
flushCacheNotice();
});
});
},
@ -1957,7 +1959,6 @@ define([
Messages.admin_bytesWrittenTitle = "Disk performance measurement window"; // XXX
Messages.admin_bytesWrittenHint = "If you have enabled disk performance measurements then the duration of the window can be configured below."; // XXX
Messages.admin_bytesWrittenDuration = "Duration of the window in milliseconds: {0}"; // XXX
//Messages.admin_defaultDuration = "admin_defaultDuration"; // XXX
Messages.admin_setDuration = "Set duration"; // XXX
var isPositiveInteger = function (n) {

View file

@ -977,8 +977,8 @@ define([
'img-src': ["'self'", 'data:', 'blob:', $outer],
'media-src': ['blob:'],
//'frame-ancestors': ['*'], // XXX IFF you want to support remote embedding
'worker-src': ["'self'"], // , $outer, $sandbox],
'frame-ancestors': ApiConfig.disableEmbedding? [$outer, $sandbox]: ['*'],
'worker-src': ["'self'"],
});
cb(result);
});
@ -1015,7 +1015,7 @@ define([
],
'img-src': ["'self'", 'data:', 'blob:', $outer],
'media-src': ['blob:'],
//'frame-ancestors': ['*'], // XXX IFF you want to support remote embedding
'frame-ancestors': ApiConfig.disableEmbedding? [$outer, $sandbox]: ['*'],
'worker-src': ["'self'"],//, $outer, $sandbox],
});
@ -1023,18 +1023,53 @@ define([
});
});
assert(function (cb, msg) {
/* Only two use-cases are currently supported:
1. remote embedding is enabled, and fully permissive
2. remote embedding is disabled, so media-tags can only be loaded on your instance
Support for selectively enabling embedding on remote sites is far more complicated
and will need funding.
*/
var checkAllowedOrigins = function (raw, url, msg, cb) {
var header = 'Access-Control-Allow-Origin';
var expected;
if (ApiConfig.disableEmbedding) {
expected = trimmedSafe;
msg.appendChild(h('span', [
'This instance has been configured to disable support for embedding assets and documents in third-party websites. ',
'In order for this setting to be effective while still permitting encrypted media to load locally ',
'the ',
code(header),
' should only match trusted domains.',
' Under most circumstances it is sufficient to permit only the sandbox domain to load assets.',
" Remote embedding can be enabled via the admin panel.",
]));
} else {
expected = '*';
msg.appendChild(h('span', [
"This instance has been configured to permit embedding assets and documents in third-party websites.",
'Assets must be served with an ',
code(header),
' header with a value of ',
code("'*'"),
' if you wish to support embedding of encrypted media on third party websites.',
'.',
' Remote embedding can be disabled via the admin panel.',
]));
Tools.common_xhr('/', function (xhr) {
}
if (raw === expected) { return void cb(true); }
cb({
url: url,
response: raw,
disableEmbedding: ApiConfig.disableEmbedding,
});
};
assert(function (cb, msg) {
var header = 'Access-Control-Allow-Origin';
var url = new URL('/', trimmedUnsafe).href;
Tools.common_xhr(url, function (xhr) {
var raw = xhr.getResponseHeader(header);
cb(raw === "*" || raw);
checkAllowedOrigins(raw, url, msg, cb);
});
});
@ -1248,6 +1283,33 @@ define([
});
});
assert(function (cb, msg) {
var url;
try {
url = new URL('/', trimmedUnsafe);
} catch (err) {
// if your configuration is bad enough that this throws
// then other tests should detect it. Let's just bail out
return void cb(true);
}
// xhr.getResponseHeader and similar APIs don't behave as expected in insecure cross-origin contexts
// which prevents us from inspecting headers in a development context. We bail out early
// and assume it passed. The proper test will run as normal in production
if (url.protocol !== 'https') { return void cb(true); }
var header = 'Access-Control-Allow-Origin';
deferredPostMessage({
command: 'GET_HEADER',
content: {
url: url.href,
header: header,
},
}, function (raw) {
checkAllowedOrigins(raw, url.href, msg, cb);
});
});
var serverToken;
Tools.common_xhr('/', function (xhr) {
serverToken = xhr.getResponseHeader('server');

View file

@ -523,6 +523,15 @@ define([
UI.openCustomModal(modal);
};
UIElements.openDirectlyConfirmation = function (common, cb) {
cb = cb || Util.noop;
UI.confirm(h('p', Messages.ui_openDirectly), yes => {
if (!yes) { return void cb(yes); }
common.openDirectly();
cb(yes);
});
};
UIElements.createButton = function (common, type, rightside, data, callback) {
var AppConfig = common.getAppConfig();
var button;
@ -879,6 +888,14 @@ define([
.text(Messages.propertiesButton))
.click(common.prepareFeedback(type))
.click(function () {
var isTop;
try {
isTop = common.getMetadataMgr().getPrivateData().isTop;
} catch (err) { console.error(err); }
if (!isTop) {
return void UIElements.openDirectlyConfirmation(common);
}
sframeChan.event('EV_PROPERTIES_OPEN');
});
break;

View file

@ -1,5 +1,6 @@
define([
'jquery',
'/api/config',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-interface.js',
@ -11,10 +12,19 @@ define([
'/customize/messages.js',
'/bower_components/nthen/index.js',
'/customize/pages.js',
], function ($, Util, Hash, UI, UIElements, Feedback, Modal, h, Clipboard,
], function ($, ApiConfig, Util, Hash, UI, UIElements, Feedback, Modal, h, Clipboard,
Messages, nThen, Pages) {
var Share = {};
var embeddableApps = [
'code',
'form',
'kanban',
'pad',
'slide',
'whiteboard',
].map(app => `/${app}/`);
var createShareWithFriends = function (config, onShare, linkGetter) {
var common = config.common;
var sframeChan = common.getSframeChannel();
@ -771,7 +781,7 @@ define([
icon: "fa fa-link",
active: !contactsActive,
}];
if (!opts.static) {
if (!opts.static && !ApiConfig.disableEmbedding && embeddableApps.includes(pathname)) {
tabs.push({
getTab: getEmbedTab,
title: Messages.share_embedCategory,
@ -965,11 +975,16 @@ define([
title: Messages.share_linkCategory,
icon: "fa fa-link",
active: !hasFriends,
}, {
}];
if (!ApiConfig.disableEmbedding) {
tabs.push({
getTab: getFileEmbedTab,
title: Messages.share_embedCategory,
icon: "fa fa-code",
}];
});
}
Modal.getModal(common, opts, tabs, cb);
};

View file

@ -734,6 +734,10 @@ define([
var createFilePicker = function () {
if (!common.isLoggedIn()) { return; }
$embedButton = common.createButton('mediatag', true).click(function () {
if (!cpNfInner.metadataMgr.getPrivateData().isTop) {
return void UIElements.openDirectlyConfirmation(common);
}
var cfg = {
types: ['file', 'link'],
where: ['root']

View file

@ -8,7 +8,33 @@ define([
], function (nThen, ApiConfig, RequireConfig, Messages, $) {
var common = {};
var embeddableApps = [
'code',
'form',
'kanban',
'pad',
'slide',
'whiteboard',
].map(function (x) {
return `/${x}/`;
});
common.initIframe = function (waitFor, isRt, pathname) {
if (window.top !== window) {
if (ApiConfig.disableEmbedding) {
return void window.alert(`This CryptPad instance's administrators have disabled remote embedding of its editors.`);
}
// even where embedding is not forbidden it should still be limited
// to apps that are explicitly permitted
if (!embeddableApps.includes(window.location.pathname)) {
return void window.alert(`Embedding this CryptPad editor in remote pages is not supported.`);
}
}
if (window.location.origin !== ApiConfig.httpUnsafeOrigin) {
return void window.alert(`This page is configured to only be accessed via ${ApiConfig.httpUnsafeOrigin}.`);
}
var requireConfig = RequireConfig();
var lang = Messages._languageUsed;
var themeKey = 'CRYPTPAD_STORE|colortheme';
@ -641,6 +667,7 @@ define([
prefersDriveRedirect: Utils.LocalStore.getDriveRedirectPreference(),
isPresent: parsed.hashData && parsed.hashData.present,
isEmbed: parsed.hashData && parsed.hashData.embed,
isTop: window.top === window,
canEdit: hashes && hashes.editHash,
oldVersionHash: parsed.hashData && parsed.hashData.version < 2, // password
isHistoryVersion: parsed.hashData && parsed.hashData.versionHash,
@ -838,14 +865,19 @@ define([
}
});
sframeChan.on('EV_OPEN_URL', function (url) {
if (url) {
var openURL = function (url) {
if (!url) { return; }
var a = window.open(url);
if (!a) {
sframeChan.event('EV_POPUP_BLOCKED');
}
}
};
sframeChan.on('EV_OPEN_URL_DIRECTLY', function () {
var url = currentPad.href;
openURL(url);
});
sframeChan.on('EV_OPEN_URL', openURL);
sframeChan.on('EV_OPEN_UNSAFE_URL', function (url) {
if (url) {

View file

@ -662,6 +662,9 @@ define([
});
};
funcs.openDirectly = function () {
ctx.sframeChan.event('EV_OPEN_URL_DIRECTLY');
};
funcs.gotoURL = function (url) { ctx.sframeChan.event('EV_GOTO_URL', url); };
funcs.openURL = function (url) { ctx.sframeChan.event('EV_OPEN_URL', url); };
funcs.getBounceURL = function (url) {

View file

@ -582,6 +582,9 @@ MessengerUI, Messages, Pages) {
hidden: true
});
$shareBlock.click(function () {
if (!config.metadataMgr.getPrivateData().isTop) {
return void UIElements.openDirectlyConfirmation(Common);
}
if (toolbar.isDeleted) {
return void UI.warn(Messages.deletedFromServer);
}
@ -610,6 +613,9 @@ MessengerUI, Messages, Pages) {
h('span.cp-button-name', Messages.accessButton)
]));
$accessBlock.click(function () {
if (!config.metadataMgr.getPrivateData().isTop) {
return void UIElements.openDirectlyConfirmation(Common);
}
if (toolbar.isDeleted) {
return void UI.warn(Messages.deletedFromServer);
}