Server app prototype to display listed servers from accounts.cryptpad.fr

This commit is contained in:
Ludovic Dubost 2020-11-29 21:09:07 +01:00
parent 825820f6bf
commit ba0a39f00c
8 changed files with 1027 additions and 1 deletions

View File

@ -50,7 +50,8 @@
"jszip": "Stuk/jszip#^3.1.5",
"requirejs-plugins": "^1.0.3",
"dragula.js": "3.7.2",
"MathJax": "3.0.5"
"MathJax": "3.0.5",
"datatables": "^1.10.21"
},
"resolutions": {
"bootstrap": "^v4.0.0",

View File

@ -0,0 +1,306 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
@import (reference) '../../customize/src/less2/include/avatar.less';
@import (reference) "../../customize/src/less2/include/limit-bar.less";
&.cp-app-accounts {
.limit-bar_main();
.framework_min_main(
@bg-color: @colortheme_admin-bg,
@warn-color: @colortheme_admin-warn,
@color: @colortheme_admin-color
);
.sidebar-layout_main();
.cp-hidden {
display: none !important;
}
display: flex;
flex-flow: column;
@plan_basic: #DDEFFF;
@plan_pro: #E4FFDD;
@plan_power: #F6DDFF;
@alert-neutral: #EFEFEF;
@active-color: #AFFDC2;
@inactive-color: #FFD4D4;
#cp-sidebarlayout-rightside {
color: @cryptpad_text_col !important;
.alert {
font-size: 14px;
}
}
.cp-accounts-subscribe-anon, .cp-accounts-subscribe-form, .cp-accounts-subscribe-admin {
max-width: 940px;
margin: 10px auto 20px !important;
}
.cp-accounts-donate {
max-width: 650px;
}
.cp-limit-container {
.cp-limit-buttons {
display: none;
}
}
.cp-accounts-mysubs-addplan {
max-width: 830px;
#gift label {
margin-bottom: 0;
margin-top: 10px;
font-weight: unset;
}
}
.cp-accounts-subscribe-form {
ul {
list-style-position: outside;
padding-left: 10px;
margin-left: 10px;
}
.subscription-info {
font-size: 14px;
}
.subscription-terms {
font-size: 14px;
margin-bottom: 20px;
}
.active-plan {
margin: 20px 0;
}
.choose-plan {
display: flex;
justify-content: space-between;
@media (max-width: @browser_media-medium-screen) {
flex-flow: column;
align-items: center;
}
}
.plan {
display: flex;
flex-flow: column;
width: 300px;
min-height: 350px;
&:not(:last-child) {
margin-right: 20px;
@media (max-width: @browser_media-medium-screen) {
margin-right: 0;
margin-bottom: 20px;
}
}
.plan-header-text {
text-transform: capitalize;
font-size: 30px;
}
.plan-price-area {
display: flex;
flex-flow: column;
align-items: center;
.plan-price-text {
font-size: 60px;
font-weight: bold;
}
}
ul.plan-details {
flex: 1;
padding-right: 10px;
margin-left: 15px;
}
.buttons {
margin: 10px;
button {
width: 100%;
font-weight: bold;
}
}
.alert {
margin: 0 10px;
background: white;
border: none;
color: inherit;
}
}
.plan-basic {
background-color: @plan_basic;
}
.plan-pro {
background-color: @plan_pro;
}
.plan-power {
background-color: @plan_power;
}
.link-to-stripe {
margin-top: 30px;
margin-bottom: 10px;
display: inline-flex;
align-items: center;
}
.info-bottom {
font-size: 14px;
p {
margin-bottom: 0;
}
}
}
.cp-accounts-mysubs-data {
.active-subs {
.subscription {
min-height: 300px;
width: 500px;
}
.alert {
background: white;
display: flex;
flex-flow: row;
color: inherit;
border: none;
align-items: center;
font-size: 14px;
button {
flex-shrink: 0;
margin-top: 0px !important;
}
}
.users {
display: flex;
width: 100%;
& > span {
width: 180px;
margin-right: 10px;
}
}
}
.subscription-container {
.subscription {
min-height: 175px;
width: 300px;
&:not(.details) {
.creation, .notes, .expired, .cancel, .close-d, .storage {
display: none !important;
}
}
&.details {
.open-d {
display: none !important;
}
}
}
}
.subscription-container, .active-subs {
display: flex;
flex-wrap: wrap;
align-items: baseline;
.subscription {
display: flex;
flex-flow: column;
align-items: baseline;
justify-content: space-between;
margin-bottom: 20px;
margin-right: 20px;
padding: 10px;
background-color: #eee;
position: relative;
& > span {
margin-bottom: 10px;
}
button.open-details {
width: 100%;
}
.open-d, .close-d {
display: flex;
align-items: center;
justify-content: center;
i {
margin-right: 5px;
}
}
.billing, .shared, .expired {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
button {
margin-right: 0 !important;
}
}
.billing {
.text {
display: flex;
flex-flow: column;
}
}
.plan {
font-size: 20px;
}
.active, .inactive {
position: absolute;
padding: 0 5px;
right: 5px;
top: 5px;
}
.inactive {
background-color: @inactive-color;
}
.active {
background-color: @active-color;
}
.benificiary-data {
display: flex;
align-items: center;
background: white;
padding: 5px;
.cp-avatar {
margin-right: 10px;
flex-shrink: 0;
}
.name {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
.notes {
flex: 1;
max-height: 60px;
}
.avatar_main(30px);
}
}
}
.cp-accounts-mysubs-addplan {
.avatar_main(30px);
}
.cp-accounts-subscribe-admin {
div.admin {
margin-bottom: 150px;
}
}
.cp-accounts-admin {
input[type="text"] {
width: auto;
}
}
}

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/servers/app/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
</style>
</head>
<body>
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-sidebarlayout-container" style="display: none;">
<div id="cp-sidebarlayout-rightside">
<h1 id="cp-servers-title">CryptPad Server List</h1>
<p id="cp-servers-desc">
Lorem ipso sum. Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.Lorem ipso sum.
</p>
<table id="servers" class="display" style="width:100%">
<thead>
<tr>
<th>URL</th>
<th>Name</th>
<th>Description</th>
<th>Version</th>
<th>Registered Users</th>
<th>Max connections</th>
<th>First Connection</th>
<th>Last Connection</th>
</tr>
</thead>
</div>
</div>
</body>
</html>

56
www/servers/app/inner.js Normal file
View File

@ -0,0 +1,56 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'jquery',
'/bower_components/nthen/index.js',
'/customize/application_config.js',
'/common/dom-ready.js',
'/common/common-interface.js',
'/common/sframe-common.js',
'/common/toolbar.js',
'/bower_components/datatables/media/js/jquery.dataTables.min.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/datatables/media/css/dataTables.bootstrap4.min.css',
'css!/bower_components/datatables/media/css/jquery.dataTables.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'css!/servers/app/servers.css',
'less!/servers/app/app-servers.less',
], function ($, nThen, ApiConfig, DomReady, UI, SFCommon, Toolbar, DataTable) {
var APP = {}
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
$(waitFor(UI.addLoadingScreen));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (/*waitFor*/) {
APP.$container = $('#cp-sidebarlayout-container');
APP.$toolbar = $('#cp-toolbar');
var displayed = ['pageTitle'];
var configTb = {
displayed: displayed,
$container: APP.$toolbar,
sfCommon: common,
pageTitle: "CryptPad Servers",
metadataMgr: common.getMetadataMgr(),
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.hide();
APP.$container.show();
UI.removeLoadingScreen();
$('#servers').DataTable( {
"ajax": ApiConfig.accounts_api + "/api/servers",
"columns" : [
{ "data" : "url" },
{ "data" : "name" },
{ "data" : "desc" },
{ "data" : "version" },
{ "data" : "registeredUsers" },
{ "data" : "maxOpenUniqueWebSockets" },
{ "data" : "firstConnection" },
{ "data" : "lastConnection" }
]
});
});
});

65
www/servers/app/main.js Normal file
View File

@ -0,0 +1,65 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'/common/dom-ready.js',
'/common/requireconfig.js',
'/common/sframe-common-outer.js',
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) {
var requireConfig = RequireConfig();
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
document.getElementById('sbox-iframe').setAttribute('src',
ApiConfig.httpSafeOrigin + '/servers/app/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
var addRpc = function (sframeChan, Cryptpad/*, Utils*/) {
sframeChan.on('ACCOUNTS_GET_KEYS', function (data, cb) {
Cryptpad.getUserObject(null, function (obj) {
cb(obj);
});
});
sframeChan.on('Q_UPDATE_LIMIT', function (data, cb) {
Cryptpad.updatePinLimit(function (e) {
cb({error: e});
});
});
};
var category;
if (window.location.hash) {
category = window.location.hash.slice(1);
window.location.hash = '';
}
var addData = function (obj) {
if (category) { obj.category = category; }
};
SFCommonO.start({
noRealtime: true,
addRpc: addRpc,
addData: addData
});
});
});

192
www/servers/app/messages.js Normal file
View File

@ -0,0 +1,192 @@
(function () {
var LS_LANG = "CRYPTPAD_LANG";
// add your module to this map so it gets used
var map = {
'de': 'Deutsch',
'es': 'Español',
'fr': 'Français',
//'it': 'Italiano',
};
var getStoredLanguage = function () { return localStorage.getItem(LS_LANG); };
var getBrowserLanguage = function () { return navigator.language || navigator.userLanguage; };
var getLanguage = function () {
if (window.cryptpadLanguage) { return window.cryptpadLanguage; }
if (getStoredLanguage()) { return getStoredLanguage(); }
var l = getBrowserLanguage() || '';
if (Object.keys(map).indexOf(l) !== -1) {
return l;
}
// Edge returns 'fr-FR' --> transform it to 'fr' and check again
return Object.keys(map).indexOf(l.split('-')[0]) !== -1 ? l.split('-')[0] : 'en';
};
var language = getLanguage();
var req = ['jquery', 'json!/accounts/resources/translations/messages.json'];
if (language && map[language]) { req.push('json!/accounts/resources/translations/messages.' + language + '.json'); }
define(req, function($, Default, Language) {
var externalMap = JSON.parse(JSON.stringify(map));
map.en = 'English';
var defaultLanguage = 'en';
var messages;
if (!Language || !language || language === defaultLanguage || language === 'default' || !map[language]) {
messages = Default;
}
else {
// Add the translated keys to the returned object
messages = $.extend(true, {}, Default, Language);
}
messages._languages = map;
// TODO
messages._checkTranslationState = function (cb) {
if (typeof(cb) !== "function") { return; }
var allMissing = [];
var reqs = [];
Object.keys(externalMap).forEach(function (code) {
reqs.push('/resources/translations/messages.' + code + '.js');
});
require(reqs, function () {
var langs = arguments;
Object.keys(externalMap).forEach(function (code, i) {
var translation = langs[i];
var missing = [];
var checkInObject = function (ref, translated, path) {
var updated = {};
Object.keys(ref).forEach(function (k) {
if (/^updated_[0-9]+_/.test(k) && !translated[k]) {
var key = k.split('_').slice(2).join('_');
// Make sure we don't already have an update for that key. It should not happen
// but if it does, keep the latest version
if (updated[key]) {
var ek = updated[key];
if (parseInt(ek.split('_')[1]) > parseInt(k.split('_')[1])) { return; }
}
updated[key] = k;
}
});
Object.keys(ref).forEach(function (k) {
if (/^_/.test(k) || k === 'driveReadme') { return; }
var nPath = path.slice();
nPath.push(k);
if (!translated[k] || updated[k]) {
if (updated[k]) {
var uPath = path.slice();
uPath.unshift('out');
missing.push([code, nPath, 2, uPath.join('.') + '.' + updated[k]]);
return;
}
return void missing.push([code, nPath, 1]);
}
if (typeof ref[k] !== typeof translated[k]) {
return void missing.push([code, nPath, 3]);
}
if (typeof ref[k] === "object" && !Array.isArray(ref[k])) {
checkInObject(ref[k], translated[k], nPath);
}
});
Object.keys(translated).forEach(function (k) {
if (/^_/.test(k) || k === 'driveReadme') { return; }
var nPath = path.slice();
nPath.push(k);
if (typeof ref[k] === "undefined") {
missing.push([code, nPath, 0]);
}
});
};
checkInObject(Default, translation, []);
// Push the removals at the end
missing.sort(function (a, b) {
if (a[2] === 0 && b[2] !== 0) { return 1; }
if (a[2] !== 0 && b[2] === 0) { return -1; }
return 0;
});
Array.prototype.push.apply(allMissing, missing); // Destructive concat
});
cb(allMissing);
});
};
// Get keys with parameters
messages._getKey = function (key, argArray) {
if (!messages[key]) { return '?'; }
var text = messages[key];
if (typeof(text) === 'string') {
return text.replace(/\{(\d+)\}/g, function (str, p1) {
if (typeof(argArray[p1]) === 'string' || typeof(argArray[p1]) === "number") {
return argArray[p1];
}
console.error("Only strings and numbers can be used in _getKey params!");
return '';
});
} else {
return text;
}
};
// Add handler to the language selector
var storeLanguage = function (l) {
localStorage.setItem(LS_LANG, l);
};
messages._initSelector = function ($select) {
var selector = $select || $('#language-selector');
if (!selector.length) { return; }
var $button = $(selector).find('button .buttonTitle');
// Select the current language in the list
var option = $(selector).find('[data-value="' + language + '"]');
selector.setValue(language || 'English');
// Listen for language change
$(selector).find('a.languageValue').on('click', function () {
var newLanguage = $(this).attr('data-value');
storeLanguage(newLanguage);
if (newLanguage !== language) {
setTimeout(function () { window.location.reload(); });
}
});
};
var translateText = function (i, e) {
var $el = $(e);
var key = $el.data('localization');
$el.html(messages[key]);
};
var translateAppend = function (i, e) {
var $el = $(e);
var key = $el.data('localization-append');
$el.append(messages[key]);
};
var translateTitle = function (i, e) {
var $el = $(this);
var key = $el.data('localization-title');
$el.attr('title', messages[key]);
};
var translatePlaceholder = function (i, e) {
var $el = $(this);
var key = $el.data('localization-placeholder');
$el.attr('placeholder', messages[key]);
};
messages._applyTranslation = function () {
$('[data-localization]').each(translateText);
$('[data-localization-append]').each(translateAppend);
$('#pad-iframe').contents().find('[data-localization]').each(translateText);
$('[data-localization-title]').each(translateTitle);
$('[data-localization-placeholder]').each(translatePlaceholder);
$('#pad-iframe').contents().find('[data-localization-title]').each(translateTitle);
};
messages._getLanguage = function () { return language; };
return messages;
});
}());

360
www/servers/app/servers.css Normal file
View File

@ -0,0 +1,360 @@
.hide {
display:none;
}
div.disabled {
opacity: 0.4;
pointer-events: none;
}
div.plan, div.renewal {
border: 1px solid transparent;
}
div.plan-selected, div.renewal-selected {
border: 1px solid #dde;
background: #eef;
}
#info {
margin-bottom: 20px;
}
.bold {
font-weight: bold;
}
.choose-plan .plan img {
width: 100%;
}
.spinner-div {
margin: 20px auto;
text-align: center;
display: block;
}
.loading-message {
font-size: 2rem;
}
/* login form */
.login-form {
width: 300px;
}
.login-form-entry {
margin-bottom: 10px;
}
.support-disabled {
opacity: 0.4;
background: #eee;
}
.support-pro-noauth {
width: 90%;
margin-right: auto;
margin-left: auto;
}
.plan-details, .support-details, .donate-details {
margin-top: 20px;
text-align:left;
font-family: lato, Helvetica, sans-serif;
font-size: 1.02em;
}
/*End of Plan Style*/
.choose-renewal {
margin: 10px 10%;
padding: 20px;
flex-flow: column;
display: flex;
background: #efe;
justify-content: center;
align-items: center;
}
.renewal-options {
width: 100%;
display: flex;
justify-content: space-around;
justify-content: space-evenly;
}
.cp h3.renewal-description {
padding-top: 0;
}
.renewal-label {
display: inline-flex;
align-items: center;
}
.renewal-label input {
margin-right: 5px;
}
.cp button.btn-plan-basic, button.btn-donate5, button.btn-gift {
background-color: #00ADEE;
border-color: #00ADEE;
}
.cp button.btn-plan-pro {
background-color: #78b336;
border-color: #78b336;
}
.cp button.btn-plan-power, .cp button.btn-donate10 {
background-color: #F87217;
border-color: #F87217;
}
.cp button.paybutton:hover {
opacity: 0.7;
}
.center {
text-align: center;
}
table.subscription-table {
width: 100%;
}
table.subscription-table .cell-benificiary {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
table.subscription-table tr.personnal-subscription {
background-color: #eef;
}
table.subscription-table .cell-cancel {
text-align: center;
}
.active-subs div.buttons {
text-align: center;
}
.active-subs div.buttons button {
margin-left: 10px;
margin-right: 10px;
}
.active-subs div.buttons .show-active-only-box {
margin-left: 5px;
}
.cp-contacts-container {
display: flex;
flex-flow: row wrap;
}
.cp-contacts-container .cp-contact {
padding: 5px;
margin-right: 5px;
display: inline-flex;
align-items: center;
margin-right: 20px;
cursor: pointer;
}
.cp-contacts-container .cp-contact:hover {
background: rgba(0,0,0,0.2);
}
.cp-contacts-container .cp-avatar {
display: inline-flex;
align-items: center;
margin-right: 5px;
}
.cp-contacts-container .cp-name {
width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.subscription-form div.buttons {
text-align: center;
}
#info .button-container {
justify-content: flex-end;
}
#info div.row > div {
height: 40px;
line-height: 40px;
}
#info div.row {
margin-right: 0px;
margin-left: 0px;
}
#info #error {
padding: 0 10px 0 10px;
}
/* TODO(cjd) this is crap */
#info > span {
padding-left: 15px;
}
#info a.btn {
color: white;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: normal;
}
#heading {
text-align: center;
}
.pay-spinner {
position: fixed;
top:0;
left:0;
right:0;
bottom:0;
background-color:rgba(0, 0, 0, 0.6);
z-index: 999999;
color:white;
min-height: 100%;
min-height: 100vh;
height: 100%;
display: flex;
align-items: center;
}
.pay-spinner div {
width: 100%
}
#hallOfFameCheck {
margin-right: 5px;
}
.faq-container .faq-questions-q {
color: #3a84b6;
padding: 0;
margin-bottom: 0;
margin-top: 5px;
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.faq-container .faq-questions-q:hover {
color: #2e688f;
text-decoration: underline;
}
.faq-container .faq-questions-a {
display: none;
padding: 0;
}
.crypto input {
width: 100%;
background-color: rgba(0,0,0,0.03);
border: 0px;
}
.crypto table {
width: 100%;
}
.crypto .coin-address {
width: 100%;
padding-left: 5px;
}
.cp-admin-tabs-container {
height: 50px;
margin: 20px 0;
display: flex;
}
.cp-admin-tab {
height: 100%;
display: inline-flex;
align-items: center;
background: #EEF;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-top: 1px solid #AAA;
border-left: 1px solid #AAA;
border-right: 1px solid #AAA;
padding: 10px;
cursor: pointer;
}
.cp-admin-tab:hover {
background: #DDE;
}
.cp-admin-tab.active {
background: #DDE;
cursor: default;
}
.cp-admin-stats {
background-color: #EEE;
padding: 20px;
margin: 20px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.cp-admin-stats p {
margin-bottom: 0;
}
.cp div.cp-stats {
flex: 1;
}
.cp div.cp-stats * {
padding: 0;
margin: 0;
}
.cp-admin-stats .cp-stats-note {
font-style: italic;
font-size: 14px;
width: 100%;
}
.cp-admin-edit-getform input {
margin: 0 10px;
}
.cp-admin-edit-found th {
border: 1px solid #CCC;
}
.cp-admin-edit-found tbody tr:hover {
background: #EEE;
}
.cp-admin-edit-found td {
padding: 5px;
border: 1px solid #CCC;
cursor: pointer;
}
.cp-admin-edit-getform input {
margin: 0 10px;
}
.cp-admin-edit-form p {
background: #AA0000;
color: white;
font-weight: bold;
padding: 5px;
}
.cp-admin-edit-form-content .cp-edit-field {
font-weight: bold;
}
.cp-admin-edit-form-content .cp-edit-note {
font-size: 0.9em;
font-style: italic;
}
.cp-admin-edit-form-content input {
margin: 0 10px;
padding: 0 5px;
border: 1px solid #888;
}
.cp-admin-edit-form-content {
display: flex;
flex-flow: column;
}

12
www/servers/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="app/main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css?ver=1.1" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">