etherpad-lite/src/templates/pad.html
John McLear 85f9a5f2f5
feat: Open Graph & Twitter Card metadata for pad/timeslider/home (closes #7599) (#7635)
* 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>
2026-05-01 10:43:29 +01:00

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">&#9608;</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>