mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-21 00:59:11 +00:00
* docs(spec): Open Graph metadata for pad pages (issue #7599) Spec for adding og:* and twitter:card meta tags to /p/:pad, the timeslider, and the homepage so shared links unfurl with a useful preview in chat apps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(spec): expand OG spec — i18n (locale map + og:locale) and a11y (image:alt) Address review feedback: socialDescription accepts a per-language map, og:locale is emitted from the negotiated render language, and image:alt attributes are emitted for screen readers in chat clients. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: emit Open Graph & Twitter Card metadata for pad/timeslider/home Closes #7599. Pad URLs shared in chat apps (WhatsApp, Signal, Slack, etc.) previously unfurled with no preview because the rendered HTML carried no OG or Twitter Card metadata. This change emits og:title, og:description, og:image, og:url, og:site_name, og:type, og:locale, og:image:alt and the equivalent twitter:* tags on the pad page, the timeslider, and the homepage. A new settings.json key `socialDescription` controls the description. It accepts either a plain string applied to every locale or a per-language map keyed by BCP-47 tag with an optional `default` fallback. og:locale is emitted from the language already negotiated via req.acceptsLanguages and og:image:alt provides screen-reader text for chat-client previews. Pad names from the URL are HTML-escaped before being interpolated into og:title to prevent reflected XSS via crafted pad IDs. Tests: src/tests/backend/specs/socialMeta.ts covers the default, per-locale override, locale fallback, URL decoding, XSS escape, and the timeslider/homepage variants. Semver: minor (new setting; templates emit additional tags but no existing behavior changes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): use valid pad-name char in URL-decode test Spaces aren't allowed in pad names — Etherpad redirected /p/Has%20Space* to a sanitized name (302), so the og:title assertion failed. Use %2D ("-") instead, which is a valid pad-name character and still exercises the URL-decode path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(socialMeta): don't double-decode pad name from req.params.pad Express has already URL-decoded :pad route params before they reach the handler. Calling decodeURIComponent on the result throws URIError for pad names containing a literal "%" — e.g. the URL /p/100%25 yields req.params.pad === "100%", and decodeURIComponent("100%") throws. This would have prevented the page from rendering for some valid pad IDs. Drop the redundant decode and add a regression test for the "%" case. Reported by Qodo on PR #7635. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(socialMeta): source description from i18n catalog, drop settings key Per review: the OG description is a translatable string and belongs in Etherpad's locale files alongside the rest of the UI strings, not in settings.json. Operators who want to override it per-language continue to use the standard customLocaleStrings mechanism — no new config surface. Changes: - Add "pad.social.description" to src/locales/en.json (default English). - Export i18n.locales so server-side renderers can look up translations. - socialMeta.renderSocialMeta now takes a `locales` map and resolves renderLang → primary subtag → en, instead of taking a per-locale map from settings. - Remove `socialDescription` from Settings.ts, settings.json.template, settings.json.docker (the key never shipped). - Update tests and spec doc to reflect i18n-sourced description. Reported by Qodo on PR #7635 (also confirmed feature is fine to land default-on; no flag needed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(socialMeta): add unit tests for pure helpers 21 cases exercising buildSocialMetaHtml and renderSocialMeta directly, without HTTP/DB. Covers tag enumeration, HTML escaping, og:locale region formatting, title composition (pad/timeslider/home), description i18n resolution (exact/primary/en fallback, missing catalog), image URL (default favicon vs absolute settings.favicon vs alt text), canonical URL building with query-string stripping, the literal "%" no-throw regression, and attribute-breakout escape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(socialMeta): defend og:url/og:image against host-header poisoning Previously og:url and og:image were built from req.protocol + req.get('host'), both of which can be client-controlled (Host header directly, or X-Forwarded-* under trust proxy). A crafted Host could make the server emit OG tags pointing at an attacker's origin — harmful if any cache fronts the response or if a vulnerable proxy forwards the headers unsanitized. Two-layer defense: 1. New optional setting `publicURL` lets operators pin the canonical origin used for shared link previews ("https://pad.example"). When set, og:url and og:image use it unconditionally. Sanitized at use time: must be http(s)://host[:port] with no path, no userinfo, no trailing slash; malformed values fall back to the request. 2. When `publicURL` is unset, the request-derived fallback now strictly validates the Host header against /^[a-z0-9]([a-z0-9.-]{0,253}[a-z0-9])?(:\d{1,5})?$/i and caps the scheme to "http"/"https". A crafted Host (CRLF injection, userinfo, "<script>") is replaced with "localhost" instead of being echoed into og:url. Reported by Qodo on PR #7635. Tests: 5 new unit cases covering publicURL preference, trailing-slash strip, malformed-publicURL fallback, Host validation, scheme cap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(socialMeta): tighten types, drop `any` - `req: any` -> express `Request` (covers acceptsLanguages/protocol/get/originalUrl). - `settings: any` -> local `SocialMetaSettings` interface narrowed to the three fields we actually read (title/favicon/publicURL); avoids coupling to the full Settings module surface. - `availableLangs: {[k: string]: any}` -> `{[lang: string]: unknown}`; only keys are read, so values stay deliberately unconstrained. No runtime change. All 26 socialMeta unit tests still pass. Per Sam's review on #7635. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
538 lines
26 KiB
HTML
538 lines
26 KiB
HTML
<%
|
|
var langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
|
|
, pluginUtils = require('ep_etherpad-lite/static/js/pluginfw/shared')
|
|
, skinColors = require('ep_etherpad-lite/node/utils/SkinColors')
|
|
;
|
|
var renderLang = (req && typeof req.acceptsLanguages === 'function'
|
|
&& req.acceptsLanguages(Object.keys(langs))) || 'en';
|
|
var renderDir = (langs[renderLang] && langs[renderLang].direction === 'rtl') ? 'rtl' : 'ltr';
|
|
// theme-color matches the configured toolbar so mobile address bars don't
|
|
// paint a mismatched system color above the toolbar on first paint. We do
|
|
// not emit a prefers-color-scheme: dark variant: the client-side dark-mode
|
|
// auto-switch is gated on enableDarkMode, matchMedia, and a localStorage
|
|
// white-mode override, none of which a media query can express.
|
|
var configuredColor = skinColors.configuredToolbarColor(settings.skinName, settings.skinVariants);
|
|
%>
|
|
<!doctype html>
|
|
<html lang="<%=renderLang%>" dir="<%=renderDir%>" translate="no" class="pad <%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
|
|
<head>
|
|
<% e.begin_block("htmlHead"); %>
|
|
<% e.end_block(); %>
|
|
<title><%=settings.title%></title>
|
|
<%- typeof socialMetaHtml !== 'undefined' ? socialMetaHtml : '' %>
|
|
<link rel="manifest" href="../../manifest.json" />
|
|
<script>
|
|
/*
|
|
|@licstart The following is the entire license notice for the
|
|
JavaScript code in this page.|
|
|
|
|
Copyright 2011 Peter Martischka, Primary Technology.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
|@licend The above is the entire license notice
|
|
for the JavaScript code in this page.|
|
|
*/
|
|
</script>
|
|
|
|
<meta charset="utf-8">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<meta name="referrer" content="no-referrer">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
|
|
<% if (configuredColor) { %><meta name="theme-color" content="<%=configuredColor%>"><% } %>
|
|
<link rel="shortcut icon" href="../favicon.ico">
|
|
|
|
<% e.begin_block("styles"); %>
|
|
<link href="../static/css/pad.css?v=<%=settings.randomVersionString%>" rel="stylesheet">
|
|
|
|
<% e.begin_block("customStyles"); %>
|
|
<link href="../static/skins/<%=encodeURI(settings.skinName)%>/pad.css?v=<%=settings.randomVersionString%>" rel="stylesheet">
|
|
<% e.end_block(); %>
|
|
|
|
<style title="dynamicsyntax"></style>
|
|
<% e.end_block(); %>
|
|
|
|
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
|
|
</head>
|
|
<body>
|
|
<% e.begin_block("body"); %>
|
|
|
|
<!----------------------------->
|
|
<!--------- TOOLBAR ----------->
|
|
<!----------------------------->
|
|
<div id="editbar" class="toolbar">
|
|
<div id="toolbar-overlay"></div>
|
|
|
|
<ul class="menu_left" role="toolbar">
|
|
<% e.begin_block("editbarMenuLeft"); %>
|
|
<%- toolbar.menu(settings.toolbar.left, isReadOnly, 'left', 'pad') %>
|
|
<% e.end_block(); %>
|
|
</ul>
|
|
<ul class="menu_right" role="toolbar">
|
|
<% e.begin_block("editbarMenuRight"); %>
|
|
<%- toolbar.menu(settings.toolbar.right, isReadOnly, 'right', 'pad') %>
|
|
<% e.end_block(); %>
|
|
</ul>
|
|
<button type="button" class="show-more-icon-btn" aria-label="Show more toolbar buttons" aria-expanded="false"></button> <!-- use on small screen to display hidden toolbar buttons -->
|
|
</div>
|
|
|
|
<% e.begin_block("afterEditbar"); %><% e.end_block(); %>
|
|
|
|
<div id="editorcontainerbox" class="flex-layout">
|
|
|
|
<% e.begin_block("editorContainerBox"); %>
|
|
|
|
<!----------------------------->
|
|
<!--- PAD EDITOR (in iframe) -->
|
|
<!----------------------------->
|
|
|
|
<div id="editorcontainer" class="editorcontainer" aria-label="Document editor"></div>
|
|
|
|
<div id="editorloadingbox">
|
|
<% e.begin_block("permissionDenied"); %>
|
|
<div id="permissionDenied">
|
|
<p data-l10n-id="pad.permissionDenied" class="editorloadingbox-message">
|
|
You do not have permission to access this pad
|
|
</p>
|
|
</div>
|
|
<% e.end_block(); %>
|
|
<% e.begin_block("loading"); %>
|
|
<p data-l10n-id="pad.loading" id="loading" class="editorloadingbox-message">
|
|
<img src='../static/img/brand.svg' class='etherpadBrand'><br/>
|
|
Loading...
|
|
</p>
|
|
<% e.end_block(); %>
|
|
<noscript>
|
|
<p class="editorloadingbox-message">
|
|
<strong>
|
|
Sorry, you have to enable Javascript in order to use this.
|
|
</strong>
|
|
</p>
|
|
</noscript>
|
|
</div>
|
|
|
|
|
|
<!------------------------------------------------------------->
|
|
<!-- SETTINGS POPUP (change font, language, chat parameters) -->
|
|
<!------------------------------------------------------------->
|
|
|
|
<div id="settings" class="popup" role="dialog" aria-modal="true" aria-labelledby="settings-title"><div class="popup-content">
|
|
<% if (settings.enablePadWideSettings) { %>
|
|
<h1 id="settings-title" data-l10n-id="pad.settings.title">Settings</h1>
|
|
<% } else { %>
|
|
<h1 id="settings-title" data-l10n-id="pad.settings.padSettings">Pad Settings</h1>
|
|
<% } %>
|
|
<div class="settings-sections">
|
|
<div id="user-settings-section" class="settings-section">
|
|
<% e.begin_block("mySettings"); %>
|
|
<% if (settings.enablePadWideSettings) { %>
|
|
<h2 data-l10n-id="pad.settings.userSettings"></h2>
|
|
<% } else { %>
|
|
<h2 data-l10n-id="pad.settings.myView"></h2>
|
|
<% } %>
|
|
<p class="hide-for-mobile">
|
|
<input type="checkbox" id="options-disablechat">
|
|
<label for="options-disablechat" data-l10n-id="pad.settings.disablechat">Disable Chat</label>
|
|
</p>
|
|
<p class="hide-for-mobile">
|
|
<input type="checkbox" id="options-stickychat">
|
|
<label for="options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
|
|
</p>
|
|
<p class="hide-for-mobile">
|
|
<input type="checkbox" id="options-chatandusers">
|
|
<label for="options-chatandusers" data-l10n-id="pad.settings.chatandusers"></label>
|
|
</p>
|
|
<p>
|
|
<input type="checkbox" id="options-colorscheck">
|
|
<label for="options-colorscheck" data-l10n-id="pad.settings.colorcheck"></label>
|
|
</p>
|
|
<p>
|
|
<input type="checkbox" id="options-linenoscheck" checked>
|
|
<label for="options-linenoscheck" data-l10n-id="pad.settings.linenocheck"></label>
|
|
</p>
|
|
<p>
|
|
<input type="checkbox" id="options-rtlcheck">
|
|
<label for="options-rtlcheck" data-l10n-id="pad.settings.rtlcheck"></label>
|
|
</p>
|
|
<p id="theme-toggle-row" hidden>
|
|
<input type="checkbox" id="options-darkmode">
|
|
<label for="options-darkmode" data-l10n-id="pad.settings.darkMode">Dark mode</label>
|
|
</p>
|
|
<% if (settings.enablePadWideSettings) { %><p id="enforce-settings-notice" class="settings-notice" hidden
|
|
data-l10n-id="pad.settings.enforcedNotice">
|
|
These settings are locked for you by this pad's creator. Ask the pad creator if you need them changed.
|
|
</p><% } %>
|
|
<% e.end_block(); %>
|
|
|
|
<div class="dropdowns-container">
|
|
<% e.begin_block("mySettings.dropdowns"); %>
|
|
<p class="dropdown-line">
|
|
<label for="viewfontmenu" data-l10n-id="pad.settings.fontType">Font type:</label>
|
|
<select id="viewfontmenu">
|
|
<option value="" data-l10n-id="pad.settings.fontType.normal">Normal</option>
|
|
<%= fonts = ["Quicksand", "Roboto", "Alegreya", "PlayfairDisplay", "Montserrat", "OpenDyslexic", "RobotoMono"] %>
|
|
<% for(var i=0; i < fonts.length; i++) { %>
|
|
<option value="<%=fonts[i]%>"><%=fonts[i]%></option>
|
|
<% } %>
|
|
</select>
|
|
</p>
|
|
|
|
<p class="dropdown-line">
|
|
<label for="languagemenu" data-l10n-id="pad.settings.language">Language:</label>
|
|
<select id="languagemenu">
|
|
<% for (lang in langs) { %>
|
|
<option value="<%=lang%>"><%=langs[lang].nativeName%></option>
|
|
<% } %>
|
|
</select>
|
|
</p>
|
|
<% e.end_block(); %>
|
|
</div>
|
|
</div>
|
|
<% if (settings.enablePadWideSettings) { %><div id="pad-settings-section" class="settings-section" hidden>
|
|
<% e.begin_block("padSettings"); %>
|
|
<h2 data-l10n-id="pad.settings.padSettings"></h2>
|
|
<p class="hide-for-mobile">
|
|
<input type="checkbox" id="padsettings-options-disablechat">
|
|
<label for="padsettings-options-disablechat" data-l10n-id="pad.settings.disablechat">Disable Chat</label>
|
|
</p>
|
|
<p class="hide-for-mobile">
|
|
<input type="checkbox" id="padsettings-options-stickychat">
|
|
<label for="padsettings-options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
|
|
</p>
|
|
<p class="hide-for-mobile">
|
|
<input type="checkbox" id="padsettings-options-chatandusers">
|
|
<label for="padsettings-options-chatandusers" data-l10n-id="pad.settings.chatandusers"></label>
|
|
</p>
|
|
<p>
|
|
<input type="checkbox" id="padsettings-options-colorscheck">
|
|
<label for="padsettings-options-colorscheck" data-l10n-id="pad.settings.colorcheck"></label>
|
|
</p>
|
|
<p>
|
|
<input type="checkbox" id="padsettings-options-linenoscheck" checked>
|
|
<label for="padsettings-options-linenoscheck" data-l10n-id="pad.settings.linenocheck"></label>
|
|
</p>
|
|
<p>
|
|
<input type="checkbox" id="padsettings-options-rtlcheck">
|
|
<label for="padsettings-options-rtlcheck" data-l10n-id="pad.settings.rtlcheck"></label>
|
|
</p>
|
|
<p>
|
|
<input type="checkbox" id="padsettings-enforcecheck">
|
|
<label for="padsettings-enforcecheck" data-l10n-id="pad.settings.enforceSettings">Enforce settings for other users</label>
|
|
</p>
|
|
<% e.end_block(); %>
|
|
<div class="dropdowns-container">
|
|
<% e.begin_block("padSettings.dropdowns"); %>
|
|
<p class="dropdown-line">
|
|
<label for="padsettings-viewfontmenu" data-l10n-id="pad.settings.fontType">Font type:</label>
|
|
<select id="padsettings-viewfontmenu">
|
|
<option value="" data-l10n-id="pad.settings.fontType.normal">Normal</option>
|
|
<% for(var i=0; i < fonts.length; i++) { %>
|
|
<option value="<%=fonts[i]%>"><%=fonts[i]%></option>
|
|
<% } %>
|
|
</select>
|
|
</p>
|
|
<p class="dropdown-line">
|
|
<label for="padsettings-languagemenu" data-l10n-id="pad.settings.language">Language:</label>
|
|
<select id="padsettings-languagemenu">
|
|
<% for (lang in langs) { %>
|
|
<option value="<%=lang%>"><%=langs[lang].nativeName%></option>
|
|
<% } %>
|
|
</select>
|
|
</p>
|
|
<% e.end_block(); %>
|
|
</div>
|
|
<button class="btn btn-danger" data-l10n-id="pad.settings.deletePad" id="delete-pad">Delete pad</button>
|
|
</div><% } %>
|
|
</div>
|
|
<h2 data-l10n-id="pad.settings.about">About</h2>
|
|
<span data-l10n-id="pad.settings.poweredBy">Powered by</span>
|
|
<a href="https://etherpad.org" target="_blank" referrerpolicy="no-referrer" rel="noopener">Etherpad</a>
|
|
<% if (settings.exposeVersion) { %>(commit <%= settings.gitVersion %>)<% } %> </div></div>
|
|
|
|
|
|
<!------------------------->
|
|
<!-- IMPORT EXPORT POPUP -->
|
|
<!------------------------->
|
|
|
|
<div id="import_export" class="popup" role="dialog" aria-modal="true" aria-labelledby="importexport-title"><div class="popup-content">
|
|
<h1 id="importexport-title" data-l10n-id="pad.importExport.import_export"></h1>
|
|
<div class="acl-write">
|
|
<% e.begin_block("importColumn"); %>
|
|
<h2 data-l10n-id="pad.importExport.import"></h2>
|
|
<div class="importmessage" id="importmessagenoconverter" data-l10n-id="pad.importExport.noConverter.innerHTML" hidden></div>
|
|
<form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data">
|
|
<div class="importformdiv" id="importformfilediv">
|
|
<input type="file" name="file" size="10" id="importfileinput">
|
|
<div class="importmessage" id="importmessagefail"></div>
|
|
</div>
|
|
<div id="import"></div>
|
|
<div class="importmessage" id="importmessagesuccess" data-l10n-id="pad.importExport.importSuccessful"></div>
|
|
<div class="importformdiv" id="importformsubmitdiv">
|
|
<span class="nowrap">
|
|
<input type="submit" class="btn btn-primary" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput">
|
|
<div alt="" id="importstatusball" class="loadingAnimation" align="top"></div>
|
|
</span>
|
|
</div>
|
|
</form>
|
|
<% e.end_block(); %>
|
|
</div>
|
|
<div id="exportColumn">
|
|
<h2 data-l10n-id="pad.importExport.export"></h2>
|
|
<% e.begin_block("exportColumn"); %>
|
|
<a id="exportetherpada" target="_blank" class="exportlink">
|
|
<span class="exporttype buttonicon buttonicon-file-powerpoint" id="exportetherpad" data-l10n-id="pad.importExport.exportetherpad"></span>
|
|
</a>
|
|
<a id="exporthtmla" target="_blank" class="exportlink">
|
|
<span class="exporttype buttonicon buttonicon-file-code" id="exporthtml" data-l10n-id="pad.importExport.exporthtml"></span>
|
|
</a>
|
|
<a id="exportplaina" target="_blank" class="exportlink">
|
|
<span class="exporttype buttonicon buttonicon-file" id="exportplain" data-l10n-id="pad.importExport.exportplain"></span>
|
|
</a>
|
|
<a id="exportworda" target="_blank" class="exportlink">
|
|
<span class="exporttype buttonicon buttonicon-file-word" id="exportword" data-l10n-id="pad.importExport.exportword"></span>
|
|
</a>
|
|
<a id="exportpdfa" target="_blank" class="exportlink">
|
|
<span class="exporttype buttonicon buttonicon-file-pdf" id="exportpdf" data-l10n-id="pad.importExport.exportpdf"></span>
|
|
</a>
|
|
<a id="exportopena" target="_blank" class="exportlink">
|
|
<span class="exporttype buttonicon buttonicon-file-alt" id="exportopen" data-l10n-id="pad.importExport.exportopen"></span>
|
|
</a>
|
|
<% e.end_block(); %>
|
|
</div>
|
|
</div></div>
|
|
|
|
|
|
<!---------------------------------------------------->
|
|
<!-- CONNECTIVITY POPUP (when you get disconnected) -->
|
|
<!---------------------------------------------------->
|
|
|
|
<div id="connectivity" class="popup" role="dialog" aria-modal="true" aria-label="Connection status"><div class="popup-content">
|
|
<% e.begin_block("modals"); %>
|
|
<div class="connected visible">
|
|
<h2 data-l10n-id="pad.modals.connected"></h2>
|
|
</div>
|
|
<div class="reconnecting">
|
|
<h1 data-l10n-id="pad.modals.reconnecting"></h1>
|
|
<i class='buttonicon buttonicon-spin5 icon-spin'>
|
|
<img src='../static/img/brand.svg' class='etherpadBrand'><br/>
|
|
</i>
|
|
</div>
|
|
<div class="userdup">
|
|
<h1 data-l10n-id="pad.modals.userdup"></h1>
|
|
<h2 data-l10n-id="pad.modals.userdup.explanation"></h2>
|
|
<p id="defaulttext" data-l10n-id="pad.modals.userdup.advice"></p>
|
|
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
|
|
</div>
|
|
<div class="unauth">
|
|
<h1 data-l10n-id="pad.modals.unauth"></h1>
|
|
<p id="defaulttext" data-l10n-id="pad.modals.unauth.explanation"></p>
|
|
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
|
|
</div>
|
|
<div class="looping">
|
|
<h1 data-l10n-id="pad.modals.disconnected"></h1>
|
|
<h2 data-l10n-id="pad.modals.looping.explanation"></h2>
|
|
<p data-l10n-id="pad.modals.looping.cause"></p>
|
|
</div>
|
|
<div class="initsocketfail">
|
|
<h1 data-l10n-id="pad.modals.initsocketfail"></h1>
|
|
<h2 data-l10n-id="pad.modals.initsocketfail.explanation"></h2>
|
|
<p data-l10n-id="pad.modals.initsocketfail.cause"></p>
|
|
</div>
|
|
<div class="slowcommit with_reconnect_timer">
|
|
<h1 data-l10n-id="pad.modals.disconnected"></h1>
|
|
<h2 data-l10n-id="pad.modals.slowcommit.explanation"></h2>
|
|
<p id="defaulttext" data-l10n-id="pad.modals.slowcommit.cause"></p>
|
|
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
|
|
</div>
|
|
<div class="badChangeset with_reconnect_timer">
|
|
<h1 data-l10n-id="pad.modals.disconnected"></h1>
|
|
<h2 data-l10n-id="pad.modals.badChangeset.explanation"></h2>
|
|
<p id="defaulttext" data-l10n-id="pad.modals.badChangeset.cause"></p>
|
|
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
|
|
</div>
|
|
<div class="corruptPad">
|
|
<h1 data-l10n-id="pad.modals.disconnected"></h1>
|
|
<h2 data-l10n-id="pad.modals.corruptPad.explanation"></h2>
|
|
<p data-l10n-id="pad.modals.corruptPad.cause"></p>
|
|
</div>
|
|
<div class="deleted">
|
|
<h1 data-l10n-id="pad.modals.deleted"></h1>
|
|
<p data-l10n-id="pad.modals.deleted.explanation"></p>
|
|
</div>
|
|
<div class="rateLimited">
|
|
<h1 data-l10n-id="pad.modals.rateLimited"></h1>
|
|
<p data-l10n-id="pad.modals.rateLimited.explanation"></p>
|
|
</div>
|
|
<div class="rejected">
|
|
<h1 data-l10n-id="pad.modals.disconnected"></h1>
|
|
<h2 data-l10n-id="pad.modals.rejected.explanation"></h2>
|
|
<p data-l10n-id="pad.modals.rejected.cause"></p>
|
|
</div>
|
|
<div class="disconnected with_reconnect_timer">
|
|
<% e.begin_block("disconnected"); %>
|
|
<h1 data-l10n-id="pad.modals.disconnected"></h1>
|
|
<h2 data-l10n-id="pad.modals.disconnected.explanation"></h2>
|
|
<p id="defaulttext" data-l10n-id="pad.modals.disconnected.cause"></p>
|
|
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
|
|
<% e.end_block(); %>
|
|
</div>
|
|
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
|
|
<input type="hidden" class="padId" name="padId">
|
|
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
|
|
<input type="hidden" class="missedChanges" name="missedChanges">
|
|
</form>
|
|
<% e.end_block(); %>
|
|
</div></div>
|
|
|
|
|
|
<!-------------------------------->
|
|
<!-- EMBED POPUP (Share, embed) -->
|
|
<!-------------------------------->
|
|
|
|
<div id="embed" class="popup" role="dialog" aria-modal="true" aria-labelledby="embed-title"><div class="popup-content">
|
|
<% e.begin_block("embedPopup"); %>
|
|
<h1 id="embed-title" data-l10n-id="pad.share"></h1>
|
|
<div id="embedreadonly" class="acl-write">
|
|
<input type="checkbox" id="readonlyinput">
|
|
<label for="readonlyinput" data-l10n-id="pad.share.readonly"></label>
|
|
</div>
|
|
<div id="linkcode">
|
|
<h2 data-l10n-id="pad.share.link"></h2>
|
|
<input id="linkinput" type="text" value="" onclick="this.select()">
|
|
</div>
|
|
<div id="embedcode">
|
|
<h2 data-l10n-id="pad.share.emebdcode"></h2>
|
|
<input id="embedinput" type="text" value="" onclick="this.select()">
|
|
</div>
|
|
<% e.end_block(); %>
|
|
</div></div>
|
|
|
|
<div class="sticky-container">
|
|
|
|
<!---------------------------------------------------------------------->
|
|
<!-- USERS POPUP (set username, color, see other users names & color) -->
|
|
<!---------------------------------------------------------------------->
|
|
|
|
<div id="users" class="popup" role="dialog" aria-modal="true" aria-label="Users on this pad"><div class="popup-content">
|
|
<% e.begin_block("userlist"); %>
|
|
<div id="connectionstatus"></div>
|
|
<div id="myuser">
|
|
<div id="mycolorpicker" class="popup" role="dialog" aria-modal="true" aria-label="Choose your author color"><div class="popup-content">
|
|
<div id="colorpicker"></div>
|
|
<div class="btn-container">
|
|
<button id="mycolorpickersave" data-l10n-id="pad.colorpicker.save" class="btn btn-primary"></button>
|
|
<button id="mycolorpickercancel" data-l10n-id="pad.colorpicker.cancel" class="btn btn-default"></button>
|
|
<span id="mycolorpickerpreview" class="myswatchboxhoverable"></span>
|
|
</div>
|
|
</div></div>
|
|
<div id="myswatchbox"><div id="myswatch"></div></div>
|
|
<div id="myusernameform">
|
|
<input type="text" id="myusernameedit" disabled="disabled" data-l10n-id="pad.userlist.entername">
|
|
</div>
|
|
</div>
|
|
<div id="otherusers" role="region" aria-live="polite" aria-label="Active users on this pad">
|
|
<table id="otheruserstable" cellspacing="0" cellpadding="0" border="0">
|
|
<tr><td></td></tr>
|
|
</table>
|
|
</div>
|
|
<div id="userlistbuttonarea"></div>
|
|
<% e.end_block(); %>
|
|
</div></div>
|
|
|
|
|
|
<!----------------------------->
|
|
<!----------- CHAT ------------>
|
|
<!----------------------------->
|
|
|
|
<button type="button" id="chaticon" class="visible" title="Chat (Alt C)" data-l10n-id="pad.chat.title">
|
|
<span id="chatlabel" data-l10n-id="pad.chat"></span>
|
|
<span class="buttonicon buttonicon-chat" aria-hidden="true"></span>
|
|
<span id="chatcounter" aria-label="Unread messages">0</span>
|
|
</button>
|
|
|
|
<div id="chatbox">
|
|
<div class="chat-content">
|
|
<div id="titlebar">
|
|
<h1 id ="titlelabel" data-l10n-id="pad.chat"></h1>
|
|
<button type="button" id="titlecross" class="hide-reduce-btn" aria-label="Close chat">_</button>
|
|
<button type="button" id="titlesticky" class="stick-to-screen-btn" data-l10n-id="pad.chat.stick.title">█</button>
|
|
</div>
|
|
<div id="chattext" class="thin-scrollbar" aria-live="polite" aria-relevant="additions removals text" role="log" aria-atomic="false">
|
|
<div alt="loading.." id="chatloadmessagesball" class="chatloadmessages loadingAnimation" align="top"></div>
|
|
<button id="chatloadmessagesbutton" class="chatloadmessages" data-l10n-id="pad.chat.loadmessages"></button>
|
|
</div>
|
|
<div id="chatinputbox">
|
|
<form>
|
|
<textarea id="chatinput" maxlength="999" data-l10n-id="pad.chat.writeMessage.placeholder"></textarea>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!------------------------------------------------------------------>
|
|
<!-- SKIN VARIANTS BUILDER (Customize rendering, only for admins) -->
|
|
<!------------------------------------------------------------------>
|
|
<% if (settings.skinName == 'colibris') { %>
|
|
<div id="skin-variants" class="popup" role="dialog" aria-modal="true" aria-labelledby="skin-variants-title"><div class="popup-content">
|
|
<h1 id="skin-variants-title">Skin Builder</h1>
|
|
|
|
<div class="dropdowns-container">
|
|
<% containers = [ "toolbar", "background", "editor" ]; %>
|
|
<% for(var i=0; i < containers.length; i++) { %>
|
|
<p class="dropdown-line">
|
|
<label class="skin-variant-container"><%=containers[i]%></label>
|
|
<select class="skin-variant skin-variant-color" data-container="<%=containers[i]%>">
|
|
<option value="super-light">Super Light</option>
|
|
<option value="light">Light</option>
|
|
<option value="dark">Dark</option>
|
|
<option value="super-dark">Super Dark</option>
|
|
</select>
|
|
</p>
|
|
<% } %>
|
|
</div>
|
|
|
|
<p>
|
|
<input type="checkbox" id="skin-variant-full-width" class="skin-variant"/>
|
|
<label for="skin-variant-full-width">Full Width Editor</label>
|
|
</p>
|
|
|
|
<p>
|
|
<label>Result to copy in settings.json</label>
|
|
<input id="skin-variants-result" type="text" readonly class="disabled" />
|
|
</p>
|
|
</div></div>
|
|
<% } %>
|
|
|
|
<% e.end_block(); %>
|
|
|
|
</div> <!-- End of #editorcontainerbox -->
|
|
|
|
<% e.end_block(); %>
|
|
|
|
|
|
<!----------------------------->
|
|
<!-------- JAVASCRIPT --------->
|
|
<!----------------------------->
|
|
|
|
<% e.begin_block("scripts"); %>
|
|
|
|
<script src="<%=entrypoint%>"></script>
|
|
|
|
<% e.begin_block("customScripts"); %>
|
|
<script type="text/javascript" src="../static/skins/<%=encodeURI(settings.skinName)%>/pad.js?v=<%=settings.randomVersionString%>"></script>
|
|
<% e.end_block(); %>
|
|
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
|
|
<% e.end_block(); %>
|
|
</body>
|
|
</html>
|