From d5c011e6ed26eb15e5d60e9f7a13b4132f7b7709 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Tue, 23 Jan 2024 22:16:00 +0100 Subject: [PATCH] Added script for checking the types. --- src/node/utils/{Stream.js => Stream.ts} | 15 +- .../utils/{UpdateCheck.js => UpdateCheck.ts} | 21 +- src/node/utils/randomstring.ts | 2 +- ...anitizePathname.js => sanitizePathname.ts} | 2 +- src/node/utils/toolbar.js | 270 ---------------- src/node/utils/toolbar.ts | 305 ++++++++++++++++++ src/package.json | 3 +- 7 files changed, 332 insertions(+), 286 deletions(-) rename src/node/utils/{Stream.js => Stream.ts} (93%) rename src/node/utils/{UpdateCheck.js => UpdateCheck.ts} (79%) rename src/node/utils/{sanitizePathname.js => sanitizePathname.ts} (96%) delete mode 100644 src/node/utils/toolbar.js create mode 100644 src/node/utils/toolbar.ts diff --git a/src/node/utils/Stream.js b/src/node/utils/Stream.ts similarity index 93% rename from src/node/utils/Stream.js rename to src/node/utils/Stream.ts index 611b83b33..36fde1ac7 100644 --- a/src/node/utils/Stream.js +++ b/src/node/utils/Stream.ts @@ -5,17 +5,19 @@ * objects lack. */ class Stream { + private _iter + private _next: any /** * @returns {Stream} A Stream that yields values in the half-open range [start, end). */ - static range(start, end) { + static range(start: number, end: number) { return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })()); } /** * @param {Iterable} values - Any iterable of values. */ - constructor(values) { + constructor(values: Iterable) { this._iter = values[Symbol.iterator](); this._next = null; } @@ -52,10 +54,11 @@ class Stream { * @param {number} size - The number of values to read at a time. * @returns {Stream} A new Stream that gets its values from this Stream. */ - batch(size) { + batch(size: number) { return new Stream((function* () { const b = []; try { + // @ts-ignore for (const v of this) { Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors. b.push(v); @@ -100,10 +103,11 @@ class Stream { * @param {number} capacity - The number of values to keep buffered. * @returns {Stream} A new Stream that gets its values from this Stream. */ - buffer(capacity) { + buffer(capacity: number) { return new Stream((function* () { const b = []; try { + // @ts-ignore for (const v of this) { Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors. // Note: V8 has good Array push+shift optimization. @@ -123,7 +127,8 @@ class Stream { * @param {(v: any) => any} fn - Value transformation function. * @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`. */ - map(fn) { return new Stream((function* () { for (const v of this) yield fn(v); }).call(this)); } + map(fn:Function) { return new Stream((function* () { // @ts-ignore + for (const v of this) yield fn(v); }).call(this)); } /** * Implements the JavaScript iterable protocol. diff --git a/src/node/utils/UpdateCheck.js b/src/node/utils/UpdateCheck.ts similarity index 79% rename from src/node/utils/UpdateCheck.js rename to src/node/utils/UpdateCheck.ts index 9290380d8..193a40a98 100644 --- a/src/node/utils/UpdateCheck.js +++ b/src/node/utils/UpdateCheck.ts @@ -6,9 +6,14 @@ const headers = { 'User-Agent': 'Etherpad/' + settings.getEpVersion(), } +type Infos = { + latestVersion: string +} + + const updateInterval = 60 * 60 * 1000; // 1 hour -let infos; -let lastLoadingTime = null; +let infos: Infos; +let lastLoadingTime: number | null = null; const loadEtherpadInformations = () => { if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) { @@ -16,7 +21,7 @@ const loadEtherpadInformations = () => { } return axios.get('https://static.etherpad.org/info.json', {headers: headers}) - .then(async resp => { + .then(async (resp: any) => { infos = await resp.data; if (infos === undefined || infos === null) { await Promise.reject("Could not retrieve current version") @@ -26,7 +31,7 @@ const loadEtherpadInformations = () => { lastLoadingTime = Date.now(); return await Promise.resolve(infos); }) - .catch(async err => { + .catch(async (err: Error) => { return await Promise.reject(err); }); } @@ -37,20 +42,20 @@ exports.getLatestVersion = () => { return infos?.latestVersion; }; -exports.needsUpdate = async (cb) => { +exports.needsUpdate = async (cb: Function) => { await loadEtherpadInformations() - .then((info) => { + .then((info:Infos) => { if (semver.gt(info.latestVersion, settings.getEpVersion())) { if (cb) return cb(true); } - }).catch((err) => { + }).catch((err: Error) => { console.error(`Can not perform Etherpad update check: ${err}`); if (cb) return cb(false); }); }; exports.check = () => { - exports.needsUpdate((needsUpdate) => { + exports.needsUpdate((needsUpdate: boolean) => { if (needsUpdate) { console.warn(`Update available: Download the actual version ${infos.latestVersion}`); } diff --git a/src/node/utils/randomstring.ts b/src/node/utils/randomstring.ts index f60fae1f3..a86d28566 100644 --- a/src/node/utils/randomstring.ts +++ b/src/node/utils/randomstring.ts @@ -5,6 +5,6 @@ */ const cryptoMod = require('crypto'); -const randomString = (len: string) => cryptoMod.randomBytes(len).toString('hex'); +const randomString = (len: number) => cryptoMod.randomBytes(len).toString('hex'); module.exports = randomString; diff --git a/src/node/utils/sanitizePathname.js b/src/node/utils/sanitizePathname.ts similarity index 96% rename from src/node/utils/sanitizePathname.js rename to src/node/utils/sanitizePathname.ts index 61b611166..2932b913d 100644 --- a/src/node/utils/sanitizePathname.js +++ b/src/node/utils/sanitizePathname.ts @@ -4,7 +4,7 @@ const path = require('path'); // Normalizes p and ensures that it is a relative path that does not reach outside. See // https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. -module.exports = (p, pathApi = path) => { +module.exports = (p: string, pathApi = path) => { // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.js deleted file mode 100644 index 40a476878..000000000 --- a/src/node/utils/toolbar.js +++ /dev/null @@ -1,270 +0,0 @@ -'use strict'; -/** - * The Toolbar Module creates and renders the toolbars and buttons - */ -const _ = require('underscore'); - -const removeItem = (array, what) => { - let ax; - while ((ax = array.indexOf(what)) !== -1) { - array.splice(ax, 1); - } - return array; -}; - -const defaultButtonAttributes = (name, overrides) => ({ - command: name, - localizationId: `pad.toolbar.${name}.title`, - class: `buttonicon buttonicon-${name}`, -}); - -const tag = (name, attributes, contents) => { - const aStr = tagAttributes(attributes); - - if (_.isString(contents) && contents.length > 0) { - return `<${name}${aStr}>${contents}`; - } else { - return `<${name}${aStr}>`; - } -}; - -const tagAttributes = (attributes) => { - attributes = _.reduce(attributes || {}, (o, val, name) => { - if (!_.isUndefined(val)) { - o[name] = val; - } - return o; - }, {}); - - return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`; -}; - -const ButtonsGroup = function () { - this.buttons = []; -}; - -ButtonsGroup.fromArray = function (array) { - const btnGroup = new this(); - _.each(array, (btnName) => { - btnGroup.addButton(Button.load(btnName)); - }); - return btnGroup; -}; - -ButtonsGroup.prototype.addButton = function (button) { - this.buttons.push(button); - return this; -}; - -ButtonsGroup.prototype.render = function () { - if (this.buttons && this.buttons.length === 1) { - this.buttons[0].grouping = ''; - } else if (this.buttons && this.buttons.length > 1) { - _.first(this.buttons).grouping = 'grouped-left'; - _.last(this.buttons).grouping = 'grouped-right'; - _.each(this.buttons.slice(1, -1), (btn) => { - btn.grouping = 'grouped-middle'; - }); - } - - return _.map(this.buttons, (btn) => { - if (btn) return btn.render(); - }).join('\n'); -}; - -const Button = function (attributes) { - this.attributes = attributes; -}; - -Button.load = (btnName) => { - const button = module.exports.availableButtons[btnName]; - try { - if (button.constructor === Button || button.constructor === SelectButton) { - return button; - } else { - return new Button(button); - } - } catch (e) { - console.warn('Error loading button', btnName); - return false; - } -}; - -_.extend(Button.prototype, { - grouping: '', - - render() { - const liAttributes = { - 'data-type': 'button', - 'data-key': this.attributes.command, - }; - return tag('li', liAttributes, - tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId}, - tag('button', { - 'class': ` ${this.attributes.class}`, - 'data-l10n-id': this.attributes.localizationId, - }))); - }, -}); - - -const SelectButton = function (attributes) { - this.attributes = attributes; - this.options = []; -}; - -_.extend(SelectButton.prototype, Button.prototype, { - addOption(value, text, attributes) { - this.options.push({ - value, - text, - attributes, - }); - return this; - }, - - select(attributes) { - const options = []; - - _.each(this.options, (opt) => { - const a = _.extend({ - value: opt.value, - }, opt.attributes); - - options.push(tag('option', a, opt.text)); - }); - return tag('select', attributes, options.join('')); - }, - - render() { - const attributes = { - 'id': this.attributes.id, - 'data-key': this.attributes.command, - 'data-type': 'select', - }; - return tag('li', attributes, this.select({id: this.attributes.selectId})); - }, -}); - -const Separator = function () {}; -Separator.prototype.render = function () { - return tag('li', {class: 'separator'}); -}; - -module.exports = { - availableButtons: { - bold: defaultButtonAttributes('bold'), - italic: defaultButtonAttributes('italic'), - underline: defaultButtonAttributes('underline'), - strikethrough: defaultButtonAttributes('strikethrough'), - - orderedlist: { - command: 'insertorderedlist', - localizationId: 'pad.toolbar.ol.title', - class: 'buttonicon buttonicon-insertorderedlist', - }, - - unorderedlist: { - command: 'insertunorderedlist', - localizationId: 'pad.toolbar.ul.title', - class: 'buttonicon buttonicon-insertunorderedlist', - }, - - indent: defaultButtonAttributes('indent'), - outdent: { - command: 'outdent', - localizationId: 'pad.toolbar.unindent.title', - class: 'buttonicon buttonicon-outdent', - }, - - undo: defaultButtonAttributes('undo'), - redo: defaultButtonAttributes('redo'), - - clearauthorship: { - command: 'clearauthorship', - localizationId: 'pad.toolbar.clearAuthorship.title', - class: 'buttonicon buttonicon-clearauthorship', - }, - - importexport: { - command: 'import_export', - localizationId: 'pad.toolbar.import_export.title', - class: 'buttonicon buttonicon-import_export', - }, - - timeslider: { - command: 'showTimeSlider', - localizationId: 'pad.toolbar.timeslider.title', - class: 'buttonicon buttonicon-history', - }, - - savedrevision: defaultButtonAttributes('savedRevision'), - settings: defaultButtonAttributes('settings'), - embed: defaultButtonAttributes('embed'), - showusers: defaultButtonAttributes('showusers'), - - timeslider_export: { - command: 'import_export', - localizationId: 'timeslider.toolbar.exportlink.title', - class: 'buttonicon buttonicon-import_export', - }, - - timeslider_settings: { - command: 'settings', - localizationId: 'pad.toolbar.settings.title', - class: 'buttonicon buttonicon-settings', - }, - - timeslider_returnToPad: { - command: 'timeslider_returnToPad', - localizationId: 'timeslider.toolbar.returnbutton', - class: 'buttontext', - }, - }, - - registerButton(buttonName, buttonInfo) { - this.availableButtons[buttonName] = buttonInfo; - }, - - button: (attributes) => new Button(attributes), - - separator: () => (new Separator()).render(), - - selectButton: (attributes) => new SelectButton(attributes), - - /* - * Valid values for whichMenu: 'left' | 'right' | 'timeslider-right' - * Valid values for page: 'pad' | 'timeslider' - */ - menu(buttons, isReadOnly, whichMenu, page) { - if (isReadOnly) { - // The best way to detect if it's the left editbar is to check for a bold button - if (buttons[0].indexOf('bold') !== -1) { - // Clear all formatting buttons - buttons = []; - } else { - // Remove Save Revision from the right menu - removeItem(buttons[0], 'savedrevision'); - } - } else { - /* - * This pad is not read only - * - * Add back the savedrevision button (the "star") if is not already there, - * but only on the right toolbar, and only if we are showing a pad (dont't - * do it in the timeslider). - * - * This is a quick fix for #3702 (and subsequent issue #3767): it was - * sufficient to visit a single read only pad to cause the disappearence - * of the star button from all the pads. - */ - if ((buttons[0].indexOf('savedrevision') === -1) && - (whichMenu === 'right') && (page === 'pad')) { - buttons[0].push('savedrevision'); - } - } - - const groups = _.map(buttons, (group) => ButtonsGroup.fromArray(group).render()); - return groups.join(this.separator()); - }, -}; diff --git a/src/node/utils/toolbar.ts b/src/node/utils/toolbar.ts new file mode 100644 index 000000000..aac3fb3d3 --- /dev/null +++ b/src/node/utils/toolbar.ts @@ -0,0 +1,305 @@ +'use strict'; +/** + * The Toolbar Module creates and renders the toolbars and buttons + */ +const _ = require('underscore'); + +const removeItem = (array: string[], what: string) => { + let ax; + while ((ax = array.indexOf(what)) !== -1) { + array.splice(ax, 1); + } + return array; +}; + +const defaultButtonAttributes = (name: string, overrides?: boolean) => ({ + command: name, + localizationId: `pad.toolbar.${name}.title`, + class: `buttonicon buttonicon-${name}`, +}); + +const tag = (name: string, attributes: AttributeObj, contents?: string) => { + const aStr = tagAttributes(attributes); + + if (_.isString(contents) && contents!.length > 0) { + return `<${name}${aStr}>${contents}`; + } else { + return `<${name}${aStr}>`; + } +}; + + +type AttributeObj = { + [id: string]: string +} + +const tagAttributes = (attributes: AttributeObj) => { + attributes = _.reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => { + if (!_.isUndefined(val)) { + o[name] = val; + } + return o; + }, {}); + + return ` ${_.map(attributes, (val: string, name: string) => `${name}="${_.escape(val)}"`).join(' ')}`; +}; + +type ButtonGroupType = { + grouping: string, + render: Function +} + +class ButtonGroup { + private buttons: Button[] + + constructor() { + this.buttons = [] + } + + public static fromArray = function (array: string[]) { + const btnGroup = new ButtonGroup(); + _.each(array, (btnName: string) => { + const button = Button.load(btnName) as Button + btnGroup.addButton(button); + }); + return btnGroup; + } + + private addButton(button: Button) { + this.buttons.push(button); + return this; + } + + render() { + if (this.buttons && this.buttons.length === 1) { + this.buttons[0].grouping = ''; + } else if (this.buttons && this.buttons.length > 1) { + _.first(this.buttons).grouping = 'grouped-left'; + _.last(this.buttons).grouping = 'grouped-right'; + _.each(this.buttons.slice(1, -1), (btn: Button) => { + btn.grouping = 'grouped-middle'; + }); + } + + return _.map(this.buttons, (btn: ButtonGroup) => { + if (btn) return btn.render(); + }).join('\n'); + } +} + + +class Button { + protected attributes: AttributeObj + grouping: string + + constructor(attributes: AttributeObj) { + this.attributes = attributes + this.grouping = "" + } + + public static load(btnName: string) { + const button = module.exports.availableButtons[btnName]; + try { + if (button.constructor === Button || button.constructor === SelectButton) { + return button; + } else { + return new Button(button); + } + } catch (e) { + console.warn('Error loading button', btnName); + return false; + } + } + + render() { + const liAttributes = { + 'data-type': 'button', + 'data-key': this.attributes.command, + }; + return tag('li', liAttributes, + tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId}, + tag('button', { + 'class': ` ${this.attributes.class}`, + 'data-l10n-id': this.attributes.localizationId, + }))); + } +} + +type SelectButtonOptions = { + value: string, + text: string, + attributes: AttributeObj +} + +class SelectButton extends Button { + private readonly options: SelectButtonOptions[]; + + constructor(attrs: AttributeObj) { + super(attrs); + this.options = [] + } + + addOption(value: string, text: string, attributes: AttributeObj) { + this.options.push({ + value, + text, + attributes, + }) + return this; + } + + select(attributes: AttributeObj) { + const options: string[] = []; + + _.each(this.options, (opt: AttributeSelect) => { + const a = _.extend({ + value: opt.value, + }, opt.attributes); + + options.push(tag('option', a, opt.text)); + }); + return tag('select', attributes, options.join('')); + } + + render() { + const attributes = { + 'id': this.attributes.id, + 'data-key': this.attributes.command, + 'data-type': 'select', + }; + return tag('li', attributes, this.select({id: this.attributes.selectId})); + } +} + + +type AttributeSelect = { + value: string, + attributes: AttributeObj, + text: string +} + +class Separator { + constructor() { + } + + public render() { + return tag('li', {class: 'separator'}); + + } +} + +module.exports = { + availableButtons: { + bold: defaultButtonAttributes('bold'), + italic: defaultButtonAttributes('italic'), + underline: defaultButtonAttributes('underline'), + strikethrough: defaultButtonAttributes('strikethrough'), + + orderedlist: { + command: 'insertorderedlist', + localizationId: 'pad.toolbar.ol.title', + class: 'buttonicon buttonicon-insertorderedlist', + }, + + unorderedlist: { + command: 'insertunorderedlist', + localizationId: 'pad.toolbar.ul.title', + class: 'buttonicon buttonicon-insertunorderedlist', + }, + + indent: defaultButtonAttributes('indent'), + outdent: { + command: 'outdent', + localizationId: 'pad.toolbar.unindent.title', + class: 'buttonicon buttonicon-outdent', + }, + + undo: defaultButtonAttributes('undo'), + redo: defaultButtonAttributes('redo'), + + clearauthorship: { + command: 'clearauthorship', + localizationId: 'pad.toolbar.clearAuthorship.title', + class: 'buttonicon buttonicon-clearauthorship', + }, + + importexport: { + command: 'import_export', + localizationId: 'pad.toolbar.import_export.title', + class: 'buttonicon buttonicon-import_export', + }, + + timeslider: { + command: 'showTimeSlider', + localizationId: 'pad.toolbar.timeslider.title', + class: 'buttonicon buttonicon-history', + }, + + savedrevision: defaultButtonAttributes('savedRevision'), + settings: defaultButtonAttributes('settings'), + embed: defaultButtonAttributes('embed'), + showusers: defaultButtonAttributes('showusers'), + + timeslider_export: { + command: 'import_export', + localizationId: 'timeslider.toolbar.exportlink.title', + class: 'buttonicon buttonicon-import_export', + }, + + timeslider_settings: { + command: 'settings', + localizationId: 'pad.toolbar.settings.title', + class: 'buttonicon buttonicon-settings', + }, + + timeslider_returnToPad: { + command: 'timeslider_returnToPad', + localizationId: 'timeslider.toolbar.returnbutton', + class: 'buttontext', + }, + }, + + registerButton(buttonName: string, buttonInfo: any) { + this.availableButtons[buttonName] = buttonInfo; + }, + + button: (attributes: AttributeObj) => new Button(attributes), + + separator: () => (new Separator()).render(), + + selectButton: (attributes: AttributeObj) => new SelectButton(attributes), + + /* + * Valid values for whichMenu: 'left' | 'right' | 'timeslider-right' + * Valid values for page: 'pad' | 'timeslider' + */ + menu(buttons: string[][], isReadOnly: boolean, whichMenu: string, page: string) { + if (isReadOnly) { + // The best way to detect if it's the left editbar is to check for a bold button + if (buttons[0].indexOf('bold') !== -1) { + // Clear all formatting buttons + buttons = []; + } else { + // Remove Save Revision from the right menu + removeItem(buttons[0], 'savedrevision'); + } + } else if ((buttons[0].indexOf('savedrevision') === -1) && + (whichMenu === 'right') && (page === 'pad')) { + /* + * This pad is not read only + * + * Add back the savedrevision button (the "star") if is not already there, + * but only on the right toolbar, and only if we are showing a pad (dont't + * do it in the timeslider). + * + * This is a quick fix for #3702 (and subsequent issue #3767): it was + * sufficient to visit a single read only pad to cause the disappearence + * of the star button from all the pads. + */ + buttons[0].push('savedrevision'); + } + + const groups = _.map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render()); + return groups.join(this.separator()); + }, +}; diff --git a/src/package.json b/src/package.json index ad5537947..fa63f6179 100644 --- a/src/package.json +++ b/src/package.json @@ -108,7 +108,8 @@ "test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api", "dev": "node --import tsx node/server.ts", - "prod": "node --import tsx node/server.ts" + "prod": "node --import tsx node/server.ts", + "ts-check": "tsc --noEmit --watch" }, "version": "1.9.6", "license": "Apache-2.0"