Compare commits

...

23 Commits

Author SHA1 Message Date
Akash Nimare
35ad6fbad0 release: New release v5.4.3. 2020-09-09 12:24:43 +05:30
Anders Kaseorg
97f8fe71af Escape all strings inserted into CSS selectors.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-04 22:52:42 -07:00
Anders Kaseorg
a9d59b3dcd CVE-2020-24582: Escape all strings interpolated into HTML.
Also fix various variable names to consistently indicate which strings
contain HTML.

Some of these changes close cross-site scripting vulnerabilities, and
others are for consistency.  It’s important to be meticulously
consistent about escaping so that changes that would introduce
vulnerabilities stand out as obviously wrong.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-04 22:52:38 -07:00
Anders Kaseorg
b7240e1c40 Upgrade dependencies, including Electron 9.3.0.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-09-03 17:00:51 -07:00
Anders Kaseorg
62aa849657 Upgrade dependencies, including Electron 9.2.1.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-08-25 15:42:06 -07:00
Anders Kaseorg
c302ebe282 general-section: Convert .filter(…)[0] to .find(…).
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-08-25 15:40:40 -07:00
Anders Kaseorg
6404bed519 tests: Fix E2E tests for Spectron 11.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-08-25 15:37:06 -07:00
Manav Mehta
8d4d168988 Update changelog.md for release 5.4.2 (#1017) 2020-08-12 23:18:38 +05:30
Akash Nimare
d4d3805be8 release: New release v5.4.2. 2020-08-11 16:09:23 +05:30
Akash Nimare
e853af40c4 electron: Update electron to v9.2.0. 2020-08-11 15:37:13 +05:30
Manav Mehta
941200cf3b changelog: Update changelog for release 5.4.1-beta. 2020-07-29 16:22:11 +05:30
Akash Nimare
cf1f659ebf release: New beta release v5.4.1-beta. 2020-07-29 13:40:59 +05:30
Akash Nimare
eb381a87bc electron-builder: Update builder to latest version. 2020-07-29 01:54:48 +05:30
Manav Mehta
68bc0ae4a0 readme: Add new screenshot URLs.
Update the screenshots to accomodate new Zulip logo and both the day and night modes
2020-07-29 01:31:57 +05:30
Manav Mehta
178bc7f401 macos: Update dock icon.
The icon in macOS was stretched to the boundaries making it larger than the other icons.
A padding of 30px on all sides makes it coherent with the others.

Fixes: #1003.
2020-07-27 01:12:27 +05:30
Anders Kaseorg
0f1245b975 Upgrade dependencies, including Electron 9.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-24 01:37:41 -07:00
Anders Kaseorg
960312a932 notification: Move loadBots call to preload, to break an import cycle.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-24 01:37:07 -07:00
Anders Kaseorg
0e00f3bbce Commit package-lock.json update missed in v5.4.0 release.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-24 00:50:45 -07:00
Anders Kaseorg
ec205f68a6 Send only needed data from tabs over IPC.
Fixes exceptions from the structured clone algorithm raised by
Electron 9.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-24 00:39:38 -07:00
Anders Kaseorg
5fe5989710 xo: Enable import/newline-after-import.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-23 23:18:25 -07:00
Anders Kaseorg
69141b5395 Remove spurious 'use-strict' [sic] directives.
The directive is 'use strict'.  It’s not necessary in TypeScript.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-23 23:09:12 -07:00
Anders Kaseorg
8d66f05924 xo: Sort imports with import/order.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-07-23 23:06:41 -07:00
Manav Mehta
e7330dbff8 Update changelog for v5.4.0 and the license year to 2020 (#1000)
Co-authored-by: Akash Nimare <akashnimare@users.noreply.github.com>
2020-07-21 22:05:04 +05:30
58 changed files with 2194 additions and 2906 deletions

View File

@@ -6,7 +6,8 @@
Desktop client for Zulip. Available for Mac, Linux, and Windows.
<img src="http://i.imgur.com/ChzTq4F.png"/>
<img src="https://i.imgur.com/s1o6TRA.png"/>
<img src="https://i.imgur.com/vekKnW4.png"/>
# Download
Please see the [installation guide](https://zulip.com/help/desktop-app-install-guide).

View File

@@ -1,13 +1,15 @@
import {app, dialog, session} from 'electron';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
import util from 'util';
import {linuxUpdateNotification} from './linuxupdater'; // Required only in case of linux
import log from 'electron-log';
import isDev from 'electron-is-dev';
import log from 'electron-log';
import {UpdateDownloadedEvent, UpdateInfo, autoUpdater} from 'electron-updater';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinkUtil from '../renderer/js/utils/link-util';
import {linuxUpdateNotification} from './linuxupdater'; // Required only in case of linux
const sleep = util.promisify(setTimeout);
export async function appUpdater(updateFromMenu = false): Promise<void> {

View File

@@ -1,18 +1,20 @@
import {sentryInit} from '../renderer/js/utils/sentry-util';
import {appUpdater} from './autoupdater';
import {setAutoLaunch} from './startup';
import electron, {app, dialog, ipcMain, session} from 'electron';
import fs from 'fs';
import path from 'path';
import windowStateKeeper from 'electron-window-state';
import path from 'path';
import fs from 'fs';
import electron, {app, dialog, ipcMain, session} from 'electron';
import * as AppMenu from './menu';
import * as BadgeSettings from '../renderer/js/pages/preference/badge-settings';
import * as CertificateUtil from '../renderer/js/utils/certificate-util';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as ProxyUtil from '../renderer/js/utils/proxy-util';
import {sentryInit} from '../renderer/js/utils/sentry-util';
import {appUpdater} from './autoupdater';
import * as AppMenu from './menu';
import {_getServerSettings, _saveServerIcon, _isOnline} from './request';
import {setAutoLaunch} from './startup';
let mainWindowState: windowStateKeeper.State;
@@ -353,7 +355,7 @@ ${error}`
AppMenu.setMenu(props);
const activeTab = props.tabs[props.activeTabIndex];
if (activeTab) {
mainWindow.setTitle(`Zulip - ${activeTab.webview.props.name}`);
mainWindow.setTitle(`Zulip - ${activeTab.webviewName}`);
}
});

View File

@@ -2,9 +2,11 @@ import {app, Notification, net} from 'electron';
import getStream from 'get-stream';
import semver from 'semver';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinuxUpdateUtil from '../renderer/js/utils/linux-update-util';
import Logger from '../renderer/js/utils/logger-util';
import {fetchResponse} from './request';
const logger = new Logger({

View File

@@ -1,15 +1,17 @@
import {app, shell, BrowserWindow, Menu} from 'electron';
import {appUpdater} from './autoupdater';
import AdmZip from 'adm-zip';
import * as DNDUtil from '../renderer/js/utils/dnd-util';
import type {TabData} from '../renderer/js/main';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as DNDUtil from '../renderer/js/utils/dnd-util';
import * as LinkUtil from '../renderer/js/utils/link-util';
import * as t from '../renderer/js/utils/translation-util';
import type {ServerOrFunctionalTab} from '../renderer/js/main';
import {appUpdater} from './autoupdater';
export interface MenuProps {
tabs: ServerOrFunctionalTab[];
tabs: TabData[];
activeTabIndex?: number;
enableMenu?: boolean;
}
@@ -213,7 +215,7 @@ function getHelpSubmenu(): Electron.MenuItemConstructorOptions[] {
];
}
function getWindowSubmenu(tabs: ServerOrFunctionalTab[], activeTabIndex: number): Electron.MenuItemConstructorOptions[] {
function getWindowSubmenu(tabs: TabData[], activeTabIndex: number): Electron.MenuItemConstructorOptions[] {
const initialSubmenu: Electron.MenuItemConstructorOptions[] = [{
label: t.__('Minimize'),
role: 'minimize'
@@ -229,17 +231,17 @@ function getWindowSubmenu(tabs: ServerOrFunctionalTab[], activeTabIndex: number)
});
tabs.forEach(tab => {
// Do not add functional tab settings to list of windows in menu bar
if (tab.props.role === 'function' && tab.props.name === 'Settings') {
if (tab.role === 'function' && tab.name === 'Settings') {
return;
}
initialSubmenu.push({
label: tab.props.name,
accelerator: tab.props.role === 'function' ? '' : `${ShortcutKey} + ${tab.props.index + 1}`,
checked: tab.props.index === activeTabIndex,
label: tab.name,
accelerator: tab.role === 'function' ? '' : `${ShortcutKey} + ${tab.index + 1}`,
checked: tab.index === activeTabIndex,
click(_item, focusedWindow) {
if (focusedWindow) {
sendAction('switch-server-tab', tab.props.index);
sendAction('switch-server-tab', tab.index);
}
},
type: 'checkbox'
@@ -545,20 +547,20 @@ async function checkForUpdate(): Promise<void> {
await appUpdater(true);
}
function getNextServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number {
function getNextServer(tabs: TabData[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex + 1) % tabs.length;
}
while (tabs[activeTabIndex].props.role !== 'server');
while (tabs[activeTabIndex].role !== 'server');
return activeTabIndex;
}
function getPreviousServer(tabs: ServerOrFunctionalTab[], activeTabIndex: number): number {
function getPreviousServer(tabs: TabData[], activeTabIndex: number): number {
do {
activeTabIndex = (activeTabIndex - 1 + tabs.length) % tabs.length;
}
while (tabs[activeTabIndex].props.role !== 'server');
while (tabs[activeTabIndex].role !== 'server');
return activeTabIndex;
}

View File

@@ -3,12 +3,13 @@ import fs from 'fs';
import path from 'path';
import stream from 'stream';
import util from 'util';
import * as Messages from '../resources/messages';
import Logger from '../renderer/js/utils/logger-util';
import {ServerConf} from '../renderer/js/utils/domain-util';
import escape from 'escape-html';
import getStream from 'get-stream';
import {ServerConf} from '../renderer/js/utils/domain-util';
import Logger from '../renderer/js/utils/logger-util';
import * as Messages from '../resources/messages';
export async function fetchResponse(request: ClientRequest): Promise<IncomingMessage> {
return new Promise((resolve, reject) => {
request.on('response', resolve);
@@ -71,7 +72,7 @@ export const _getServerSettings = async (domain: string, session: Electron.sessi
// Following check handles both the cases
icon: realm_icon.startsWith('/') ? realm_uri + realm_icon : realm_icon,
url: realm_uri,
alias: escape(realm_name)
alias: realm_name
};
};

View File

@@ -2,6 +2,7 @@ import {app} from 'electron';
import AutoLaunch from 'auto-launch';
import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util';
export const setAutoLaunch = async (AutoLaunchValue: boolean): Promise<void> => {

View File

@@ -26,7 +26,7 @@
const { app } = require('electron').remote;
const version_tag = document.querySelector('#version');
version_tag.innerHTML = 'v' + app.getVersion();
version_tag.textContent = 'v' + app.getVersion();
</script>
<script>require('./js/shared/preventdrag.js')</script>
</body>

View File

@@ -1,5 +1,5 @@
import {clipboard} from 'electron';
import crypto from 'crypto';
import {clipboard} from 'electron';
// This helper is exposed via electron_bridge for use in the social
// login flow.

View File

@@ -1,7 +1,7 @@
export default class BaseComponent {
generateNodeFromTemplate(template: string): Element | null {
generateNodeFromHTML(html: string): Element | null {
const wrapper = document.createElement('div');
wrapper.innerHTML = template;
wrapper.innerHTML = html;
return wrapper.firstElementChild;
}
}

View File

@@ -1,5 +1,7 @@
import {remote, ContextMenuParams} from 'electron';
import * as t from '../utils/translation-util';
const {clipboard, Menu} = remote;
export const contextMenu = (webContents: Electron.WebContents, event: Event, props: ContextMenuParams) => {

View File

@@ -1,3 +1,5 @@
import {htmlEscape} from 'escape-goat';
import Tab, {TabProps} from './tab';
export default class FunctionalTab extends Tab {
@@ -8,19 +10,21 @@ export default class FunctionalTab extends Tab {
this.init();
}
template(): string {
return `<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button">
<i class="material-icons">close</i>
</div>
<div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i>
</div>
</div>`;
templateHTML(): string {
return htmlEscape`
<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tab-badge close-button">
<i class="material-icons">close</i>
</div>
<div class="server-tab">
<i class="material-icons">${this.props.materialIcon}</i>
</div>
</div>
`;
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template());
this.$el = this.generateNodeFromHTML(this.templateHTML());
if (this.props.name !== 'Settings') {
this.props.$root.append(this.$el);
this.$closeButton = this.$el.querySelectorAll('.server-tab-badge')[0];

View File

@@ -1,7 +1,8 @@
import {ipcRenderer, remote} from 'electron';
import * as LinkUtil from '../utils/link-util';
import * as ConfigUtil from '../utils/config-util';
import * as LinkUtil from '../utils/link-util';
import type WebView from './webview';
const {shell, app} = remote;

View File

@@ -1,8 +1,11 @@
import {ipcRenderer} from 'electron';
import Tab, {TabProps} from './tab';
import {htmlEscape} from 'escape-goat';
import * as SystemUtil from '../utils/system-util';
import Tab, {TabProps} from './tab';
export default class ServerTab extends Tab {
$badge: Element;
@@ -11,19 +14,21 @@ export default class ServerTab extends Tab {
this.init();
}
template(): string {
return `<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none">${this.props.name}</div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src='${this.props.icon}'/>
</div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div>`;
templateHTML(): string {
return htmlEscape`
<div class="tab" data-tab-id="${this.props.tabIndex}">
<div class="server-tooltip" style="display:none">${this.props.name}</div>
<div class="server-tab-badge"></div>
<div class="server-tab">
<img class="server-icons" src="${this.props.icon}"/>
</div>
<div class="server-tab-shortcut">${this.generateShortcutText()}</div>
</div>
`;
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template());
this.$el = this.generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$el);
this.registerListeners();
this.$badge = this.$el.querySelectorAll('.server-tab-badge')[0];
@@ -32,7 +37,7 @@ export default class ServerTab extends Tab {
updateBadge(count: number): void {
if (count > 0) {
const formattedCount = count > 999 ? '1K+' : count.toString();
this.$badge.innerHTML = formattedCount;
this.$badge.textContent = formattedCount;
this.$badge.classList.add('active');
} else {
this.$badge.classList.remove('active');

View File

@@ -1,5 +1,5 @@
import WebView from './webview';
import BaseComponent from './base';
import WebView from './webview';
export interface TabProps {
role: string;

View File

@@ -1,12 +1,15 @@
import {ipcRenderer, remote} from 'electron';
import path from 'path';
import fs from 'fs';
import path from 'path';
import {htmlEscape} from 'escape-goat';
import * as ConfigUtil from '../utils/config-util';
import * as SystemUtil from '../utils/system-util';
import BaseComponent from './base';
import handleExternalLink from './handle-external-link';
import {contextMenu} from './context-menu';
import handleExternalLink from './handle-external-link';
const {app, dialog} = remote;
@@ -49,25 +52,27 @@ export default class WebView extends BaseComponent {
this.$webviewsContainer = document.querySelector('#webviews-container').classList;
}
template(): string {
return `<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
${this.props.preload ? 'preload="js/preload.js"' : ''}
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="
${this.props.nodeIntegration ? '' : 'contextIsolation,'}
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
javascript
">
</webview>`;
templateHTML(): string {
return htmlEscape`
<webview
class="disabled"
data-tab-id="${this.props.tabIndex}"
src="${this.props.url}"
` + (this.props.nodeIntegration ? 'nodeIntegration' : '') + htmlEscape`
` + (this.props.preload ? 'preload="js/preload.js"' : '') + htmlEscape`
partition="persist:webviewsession"
name="${this.props.name}"
webpreferences="
${this.props.nodeIntegration ? '' : 'contextIsolation,'}
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
javascript
">
</webview>
`;
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag;
this.$el = this.generateNodeFromHTML(this.templateHTML()) as Electron.WebviewTag;
this.domReady = new Promise(resolve => {
this.$el.addEventListener('dom-ready', () => resolve(), true);
});

View File

@@ -1,5 +1,4 @@
import {ipcRenderer} from 'electron';
import {EventEmitter} from 'events';
import {ClipboardDecrypterImpl} from './clipboard-decrypter';

View File

@@ -1,8 +1,8 @@
import {remote} from 'electron';
import SendFeedback from '@electron-elements/send-feedback';
import path from 'path';
import fs from 'fs';
import path from 'path';
import SendFeedback from '@electron-elements/send-feedback';
const {app} = remote;

View File

@@ -1,27 +1,27 @@
import {ipcRenderer, remote, clipboard} from 'electron';
import {feedbackHolder} from './feedback';
import path from 'path';
import escape from 'escape-html';
import isDev from 'electron-is-dev';
const {session, app, Menu, dialog} = remote;
import * as Messages from '../../resources/messages';
import FunctionalTab from './components/functional-tab';
import ServerTab from './components/server-tab';
import WebView from './components/webview';
import {feedbackHolder} from './feedback';
import * as ConfigUtil from './utils/config-util';
import * as DNDUtil from './utils/dnd-util';
import type {DNDSettings} from './utils/dnd-util';
import * as DomainUtil from './utils/domain-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import Logger from './utils/logger-util';
import ReconnectUtil from './utils/reconnect-util';
// eslint-disable-next-line import/no-unassigned-import
import './tray';
import * as DomainUtil from './utils/domain-util';
import WebView from './components/webview';
import ServerTab from './components/server-tab';
import FunctionalTab from './components/functional-tab';
import * as ConfigUtil from './utils/config-util';
import * as DNDUtil from './utils/dnd-util';
import ReconnectUtil from './utils/reconnect-util';
import Logger from './utils/logger-util';
import * as CommonUtil from './utils/common-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as LinkUtil from './utils/link-util';
import * as Messages from '../../resources/messages';
import type {DNDSettings} from './utils/dnd-util';
const {session, app, Menu, dialog} = remote;
interface FunctionalTabProps {
name: string;
@@ -59,7 +59,14 @@ const logger = new Logger({
});
const rendererDirectory = path.resolve(__dirname, '..');
export type ServerOrFunctionalTab = ServerTab | FunctionalTab;
type ServerOrFunctionalTab = ServerTab | FunctionalTab;
export interface TabData {
role: string;
name: string;
index: number;
webviewName: string;
}
class ServerManagerView {
$addServerButton: HTMLButtonElement;
@@ -116,7 +123,7 @@ class ServerManagerView {
this.$fullscreenPopup = document.querySelector('#fullscreen-popup');
this.$fullscreenEscapeKey = process.platform === 'darwin' ? '^⌘F' : 'F11';
this.$fullscreenPopup.innerHTML = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
this.$fullscreenPopup.textContent = `Press ${this.$fullscreenEscapeKey} to exit full screen`;
this.loading = new Set();
this.activeTabIndex = -1;
@@ -349,7 +356,7 @@ class ServerManagerView {
this.tabs.push(new ServerTab({
role: 'server',
icon: server.icon,
name: CommonUtil.decodeString(server.alias),
name: server.alias,
$root: this.$tabsContainer,
onClick: this.activateLastTab.bind(this, index),
index,
@@ -362,7 +369,7 @@ class ServerManagerView {
tabIndex,
url: server.url,
role: 'server',
name: CommonUtil.decodeString(server.alias),
name: server.alias,
hasPermission: (origin: string, permission: string) =>
origin === server.url && permission === 'notifications',
isActive: () => index === this.activeTabIndex,
@@ -454,7 +461,7 @@ class ServerManagerView {
const $parent = $img.parentElement;
const $container = $parent.parentElement;
const webviewId = $container.dataset.tabId;
const $webview = document.querySelector(`webview[data-tab-id="${webviewId}"]`);
const $webview = document.querySelector(`webview[data-tab-id="${CSS.escape(webviewId)}"]`);
const realmName = $webview.getAttribute('name');
if (realmName === null) {
@@ -490,7 +497,7 @@ class ServerManagerView {
}
onHover(index: number): void {
// `this.$serverIconTooltip[index].innerHTML` already has realm name, so we are just
// `this.$serverIconTooltip[index].textContent` already has realm name, so we are just
// removing the style.
this.$serverIconTooltip[index].removeAttribute('style');
// To handle position of servers' tooltip due to scrolling of list of organizations
@@ -590,19 +597,13 @@ class ServerManagerView {
// not crash app when this.tabs is passed into
// ipcRenderer. Something about webview, and props.webview
// properties in ServerTab causes the app to crash.
get tabsForIpc(): ServerOrFunctionalTab[] {
const tabs: ServerOrFunctionalTab[] = [];
this.tabs.forEach((tab: ServerOrFunctionalTab) => {
const proto = Object.create(Object.getPrototypeOf(tab));
const tabClone = Object.assign(proto, tab);
tabClone.webview = {props: {}};
tabClone.webview.props.name = tab.webview.props.name;
delete tabClone.props.webview;
tabs.push(tabClone);
});
return tabs;
get tabsForIpc(): TabData[] {
return this.tabs.map(tab => ({
role: tab.props.role,
name: tab.props.name,
index: tab.props.index,
webviewName: tab.webview.props.name
}));
}
activateTab(index: number, hideOldTab = true): void {
@@ -681,8 +682,8 @@ class ServerManagerView {
this.functionalTabs.clear();
// Clear DOM elements
this.$tabsContainer.innerHTML = '';
this.$webviewsContainer.innerHTML = '';
this.$tabsContainer.textContent = '';
this.$webviewsContainer.textContent = '';
}
async reloadView(): Promise<void> {
@@ -926,11 +927,11 @@ class ServerManagerView {
if (domain.url.includes(serverURL)) {
const serverTooltipSelector = '.tab .server-tooltip';
const serverTooltips = document.querySelectorAll(serverTooltipSelector);
serverTooltips[index].innerHTML = escape(realmName);
this.tabs[index].props.name = escape(realmName);
serverTooltips[index].textContent = realmName;
this.tabs[index].props.name = realmName;
this.tabs[index].webview.props.name = realmName;
domain.alias = escape(realmName);
domain.alias = realmName;
DomainUtil.updateDomain(index, domain);
// Update the realm name also on the Window menu
ipcRenderer.send('update-menu', {
@@ -971,7 +972,7 @@ class ServerManagerView {
webviews.forEach(webview => {
const currentId = webview.getWebContentsId();
const tabId = webview.getAttribute('data-tab-id');
const concurrentTab: HTMLButtonElement = document.querySelector(`div[data-tab-id="${tabId}"]`);
const concurrentTab: HTMLButtonElement = document.querySelector(`div[data-tab-id="${CSS.escape(tabId)}"]`);
if (currentId === webviewId) {
concurrentTab.click();
}

View File

@@ -1,12 +1,14 @@
import {ipcRenderer} from 'electron';
import MacNotifier from 'node-mac-notifier';
import electron_bridge from '../electron-bridge';
import * as ConfigUtil from '../utils/config-util';
import {
appId, customReply, focusCurrentServer, parseReply
} from './helpers';
import MacNotifier from 'node-mac-notifier';
import * as ConfigUtil from '../utils/config-util';
import electron_bridge from '../electron-bridge';
type ReplyHandler = (response: string) => void;
type ClickHandler = () => void;
let replyHandler: ReplyHandler;

View File

@@ -1,8 +1,9 @@
import {ipcRenderer} from 'electron';
import {focusCurrentServer} from './helpers';
import * as ConfigUtil from '../utils/config-util';
import {focusCurrentServer} from './helpers';
const NativeNotification = window.Notification;
export default class BaseNotification extends NativeNotification {
constructor(title: string, options: NotificationOptions) {

View File

@@ -1,8 +1,8 @@
import {remote} from 'electron';
import electron_bridge from '../electron-bridge';
import {appId, loadBots} from './helpers';
import DefaultNotification from './default-notification';
import {appId} from './helpers';
const {app} = remote;
// From https://github.com/felixrieseberg/electron-windows-notifications#appusermodelid
@@ -67,7 +67,3 @@ export function newNotification(
actions: notification.actions
};
}
electron_bridge.once('zulip-loaded', async () => {
await loadBots();
});

View File

@@ -1,8 +1,8 @@
'use-strict';
import {remote, OpenDialogOptions} from 'electron';
import path from 'path';
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base';
import * as CertificateUtil from '../../utils/certificate-util';
import * as DomainUtil from '../../utils/domain-util';
@@ -26,8 +26,8 @@ export default class AddCertificate extends BaseComponent {
this._certFile = '';
}
template(): string {
return `
templateHTML(): string {
return htmlEscape`
<div class="settings-card certificates-card">
<div class="certificate-input">
<div>${t.__('Organization URL')}</div>
@@ -42,7 +42,7 @@ export default class AddCertificate extends BaseComponent {
}
init(): void {
this.$addCertificate = this.generateNodeFromTemplate(this.template());
this.$addCertificate = this.generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$addCertificate);
this.addCertificateButton = this.$addCertificate.querySelector('#add-certificate-button');
this.serverUrl = this.$addCertificate.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;

View File

@@ -1,5 +1,6 @@
import {ipcRenderer} from 'electron';
import escape from 'escape-html';
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base';
@@ -14,9 +15,9 @@ export default class BaseSection extends BaseComponent {
generateSettingOption(props: BaseSectionProps): void {
const {$element, disabled, value, clickHandler} = props;
$element.innerHTML = '';
$element.textContent = '';
const $optionControl = this.generateNodeFromTemplate(this.generateOptionTemplate(value, disabled));
const $optionControl = this.generateNodeFromHTML(this.generateOptionHTML(value, disabled));
$element.append($optionControl);
if (!disabled) {
@@ -24,39 +25,39 @@ export default class BaseSection extends BaseComponent {
}
}
generateOptionTemplate(settingOption: boolean, disabled?: boolean): string {
const label = disabled ? '<label class="disallowed" title="Setting locked by system administrator."/>' : '<label/>';
generateOptionHTML(settingOption: boolean, disabled?: boolean): string {
const labelHTML = disabled ? '<label class="disallowed" title="Setting locked by system administrator."></label>' : '<label></label>';
if (settingOption) {
return `
return htmlEscape`
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox" checked disabled>
${label}
<input class="toggle toggle-round" type="checkbox" checked disabled>
` + labelHTML + htmlEscape`
</div>
</div>
`;
}
return `
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox">
${label}
</div>
return htmlEscape`
<div class="action">
<div class="switch">
<input class="toggle toggle-round" type="checkbox">
` + labelHTML + htmlEscape`
</div>
`;
</div>
`;
}
/* A method that in future can be used to create dropdown menus using <select> <option> tags.
it needs an object which has ``key: value`` pairs and will return a string that can be appended to HTML
*/
generateSelectTemplate(options: {[key: string]: string}, className?: string, idName?: string): string {
let select = `<select class="${escape(className)}" id="${escape(idName)}">\n`;
generateSelectHTML(options: {[key: string]: string}, className?: string, idName?: string): string {
let html = htmlEscape`<select class="${className}" id="${idName}">\n`;
Object.keys(options).forEach(key => {
select += `<option name="${escape(key)}" value="${escape(key)}">${escape(options[key])}</option>\n`;
html += htmlEscape`<option name="${key}" value="${key}">${options[key]}</option>\n`;
});
select += '</select>';
return select;
html += '</select>';
return html;
}
reloadApp(): void {

View File

@@ -1,12 +1,15 @@
import {ipcRenderer} from 'electron';
import BaseSection from './base-section';
import {htmlEscape} from 'escape-goat';
import * as DomainUtil from '../../utils/domain-util';
import ServerInfoForm from './server-info-form';
import AddCertificate from './add-certificate';
import FindAccounts from './find-accounts';
import * as t from '../../utils/translation-util';
import AddCertificate from './add-certificate';
import BaseSection from './base-section';
import FindAccounts from './find-accounts';
import ServerInfoForm from './server-info-form';
interface ConnectedOrgSectionProps {
$root: Element;
}
@@ -23,8 +26,8 @@ export default class ConnectedOrgSection extends BaseSection {
this.props = props;
}
template(): string {
return `
templateHTML(): string {
return htmlEscape`
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__('Connected organizations')}</div>
<div class="title" id="existing-servers">${t.__('All the connected orgnizations will appear here.')}</div>
@@ -43,10 +46,10 @@ export default class ConnectedOrgSection extends BaseSection {
}
initServers(): void {
this.props.$root.innerHTML = '';
this.props.$root.textContent = '';
const servers = DomainUtil.getDomains();
this.props.$root.innerHTML = this.template();
this.props.$root.innerHTML = this.templateHTML();
this.$serverInfoContainer = document.querySelector('#server-info-container');
this.$existingServers = document.querySelector('#existing-servers');
@@ -56,7 +59,7 @@ export default class ConnectedOrgSection extends BaseSection {
const noServerText = t.__('All the connected orgnizations will appear here');
// Show noServerText if no servers are there otherwise hide it
this.$existingServers.innerHTML = servers.length === 0 ? noServerText : '';
this.$existingServers.textContent = servers.length === 0 ? noServerText : '';
for (const [i, server] of servers.entries()) {
new ServerInfoForm({

View File

@@ -1,4 +1,4 @@
'use-strict';
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base';
import * as LinkUtil from '../../utils/link-util';
@@ -18,8 +18,8 @@ export default class FindAccounts extends BaseComponent {
this.props = props;
}
template(): string {
return `
templateHTML(): string {
return htmlEscape`
<div class="settings-card certificate-card">
<div class="certificate-input">
<div>${t.__('Organization URL')}</div>
@@ -33,7 +33,7 @@ export default class FindAccounts extends BaseComponent {
}
init(): void {
this.$findAccounts = this.generateNodeFromTemplate(this.template());
this.$findAccounts = this.generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$findAccounts);
this.$findAccountsButton = this.$findAccounts.querySelector('#find-accounts-button');
this.$serverUrlField = this.$findAccounts.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;

View File

@@ -1,18 +1,20 @@
import {ipcRenderer, remote, OpenDialogOptions} from 'electron';
import path from 'path';
import Tagify from '@yaireo/tagify';
import {htmlEscape} from 'escape-goat';
import fs from 'fs-extra';
import ISO6391 from 'iso-639-1';
const {app, dialog, session} = remote;
const currentBrowserWindow = remote.getCurrentWindow();
import BaseSection from './base-section';
import supportedLocales from '../../../../translations/supported-locales.json';
import * as ConfigUtil from '../../utils/config-util';
import * as EnterpriseUtil from '../../utils/enterprise-util';
import * as t from '../../utils/translation-util';
import supportedLocales from '../../../../translations/supported-locales.json';
import Tagify from '@yaireo/tagify';
import ISO6391 from 'iso-639-1';
import BaseSection from './base-section';
const {app, dialog, session} = remote;
const currentBrowserWindow = remote.getCurrentWindow();
interface GeneralSectionProps {
$root: Element;
@@ -25,8 +27,8 @@ export default class GeneralSection extends BaseSection {
this.props = props;
}
template(): string {
return `
templateHTML(): string {
return htmlEscape`
<div class="settings-pane">
<div class="title">${t.__('Appearance')}</div>
<div id="appearance-option-settings" class="settings-card">
@@ -156,7 +158,7 @@ export default class GeneralSection extends BaseSection {
}
init(): void {
this.props.$root.innerHTML = this.template();
this.props.$root.innerHTML = this.templateHTML();
this.updateTrayOption();
this.updateBadgeOption();
this.updateSilentOption();
@@ -398,8 +400,8 @@ export default class GeneralSection extends BaseSection {
setLocale(): void {
const langDiv: HTMLSelectElement = document.querySelector('.lang-div');
const langList = this.generateSelectTemplate(supportedLocales, 'lang-menu');
langDiv.innerHTML += langList;
const langListHTML = this.generateSelectHTML(supportedLocales, 'lang-menu');
langDiv.innerHTML += langListHTML;
// `langMenu` is the select-option dropdown menu formed after executing the previous command
const langMenu: HTMLSelectElement = document.querySelector('.lang-menu');
@@ -515,7 +517,7 @@ export default class GeneralSection extends BaseSection {
const note: HTMLElement = document.querySelector('#note');
note.append(t.__('You can select a maximum of 3 languages for spellchecking.'));
const spellDiv: HTMLElement = document.querySelector('#spellcheck-langs');
spellDiv.innerHTML += `
spellDiv.innerHTML += htmlEscape`
<div class="setting-description">${t.__('Spellchecker Languages')}</div>
<input name='spellcheck' placeholder='Enter Languages'>`;
@@ -555,7 +557,7 @@ export default class GeneralSection extends BaseSection {
}
});
const configuredLanguages: string[] = ConfigUtil.getConfigItem('spellcheckerLanguages').map((code: string) => [...languagePairs].filter(pair => (pair[1] === code))[0][0]);
const configuredLanguages: string[] = ConfigUtil.getConfigItem('spellcheckerLanguages').map((code: string) => [...languagePairs].find(pair => (pair[1] === code))[0]);
tagify.addTags(configuredLanguages);
tagField.addEventListener('change', event => {

View File

@@ -1,3 +1,5 @@
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base';
import * as t from '../../utils/translation-util';
@@ -17,29 +19,29 @@ export default class PreferenceNav extends BaseComponent {
this.init();
}
template(): string {
let navItemsTemplate = '';
templateHTML(): string {
let navItemsHTML = '';
for (const navItem of this.navItems) {
navItemsTemplate += `<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>`;
navItemsHTML += htmlEscape`<div class="nav" id="nav-${navItem}">${t.__(navItem)}</div>`;
}
return `
return htmlEscape`
<div>
<div id="settings-header">${t.__('Settings')}</div>
<div id="nav-container">${navItemsTemplate}</div>
<div id="nav-container">` + navItemsHTML + htmlEscape`</div>
</div>
`;
}
init(): void {
this.$el = this.generateNodeFromTemplate(this.template());
this.$el = this.generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$el);
this.registerListeners();
}
registerListeners(): void {
for (const navItem of this.navItems) {
const $item = document.querySelector(`#nav-${navItem}`);
const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`);
$item.addEventListener('click', () => {
this.props.onItemSelected(navItem);
});
@@ -57,12 +59,12 @@ export default class PreferenceNav extends BaseComponent {
}
activate(navItem: string): void {
const $item = document.querySelector(`#nav-${navItem}`);
const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`);
$item.classList.add('active');
}
deactivate(navItem: string): void {
const $item = document.querySelector(`#nav-${navItem}`);
const $item = document.querySelector(`#nav-${CSS.escape(navItem)}`);
$item.classList.remove('active');
}
}

View File

@@ -1,9 +1,12 @@
import {ipcRenderer} from 'electron';
import BaseSection from './base-section';
import {htmlEscape} from 'escape-goat';
import * as ConfigUtil from '../../utils/config-util';
import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
interface NetworkSectionProps {
$root: Element;
}
@@ -20,8 +23,8 @@ export default class NetworkSection extends BaseSection {
this.props = props;
}
template(): string {
return `
templateHTML(): string {
return htmlEscape`
<div class="settings-pane">
<div class="title">${t.__('Proxy')}</div>
<div id="appearance-option-settings" class="settings-card">
@@ -58,7 +61,7 @@ export default class NetworkSection extends BaseSection {
}
init(): void {
this.props.$root.innerHTML = this.template();
this.props.$root.innerHTML = this.templateHTML();
this.$proxyPAC = document.querySelector('#proxy-pac-option .setting-input-value');
this.$proxyRules = document.querySelector('#proxy-rules-option .setting-input-value');
this.$proxyBypass = document.querySelector('#proxy-bypass-option .setting-input-value');

View File

@@ -1,5 +1,7 @@
import {ipcRenderer, remote} from 'electron';
import {htmlEscape} from 'escape-goat';
import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util';
import * as LinkUtil from '../../utils/link-util';
@@ -22,8 +24,8 @@ export default class NewServerForm extends BaseComponent {
this.props = props;
}
template(): string {
return `
templateHTML(): string {
return htmlEscape`
<div class="server-input-container">
<div class="title">${t.__('Organization URL')}</div>
<div class="add-server-info-row">
@@ -56,20 +58,20 @@ export default class NewServerForm extends BaseComponent {
}
initForm(): void {
this.$newServerForm = this.generateNodeFromTemplate(this.template());
this.$newServerForm = this.generateNodeFromHTML(this.templateHTML());
this.$saveServerButton = this.$newServerForm.querySelector('#connect');
this.props.$root.innerHTML = '';
this.props.$root.textContent = '';
this.props.$root.append(this.$newServerForm);
this.$newServerUrl = this.$newServerForm.querySelectorAll('input.setting-input-value')[0] as HTMLInputElement;
}
async submitFormHandler(): Promise<void> {
this.$saveServerButton.innerHTML = 'Connecting...';
this.$saveServerButton.textContent = 'Connecting...';
let serverConf;
try {
serverConf = await DomainUtil.checkDomain(this.$newServerUrl.value);
} catch (error) {
this.$saveServerButton.innerHTML = 'Connect';
this.$saveServerButton.textContent = 'Connect';
await dialog.showMessageBox({
type: 'error',
message: error.toString(),

View File

@@ -1,14 +1,15 @@
import {ipcRenderer} from 'electron';
import BaseComponent from '../../components/base';
import Nav from './nav';
import ServersSection from './servers-section';
import GeneralSection from './general-section';
import NetworkSection from './network-section';
import ConnectedOrgSection from './connected-org-section';
import ShortcutsSection from './shortcuts-section';
import type {DNDSettings} from '../../utils/dnd-util';
import ConnectedOrgSection from './connected-org-section';
import GeneralSection from './general-section';
import Nav from './nav';
import NetworkSection from './network-section';
import ServersSection from './servers-section';
import ShortcutsSection from './shortcuts-section';
type Section = ServersSection | GeneralSection | NetworkSection | ConnectedOrgSection | ShortcutsSection;
export default class PreferenceView extends BaseComponent {

View File

@@ -1,8 +1,10 @@
import {remote, ipcRenderer} from 'electron';
import {htmlEscape} from 'escape-goat';
import * as Messages from '../../../../resources/messages';
import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util';
import * as Messages from '../../../../resources/messages';
import * as t from '../../utils/translation-util';
const {dialog} = remote;
@@ -26,8 +28,8 @@ export default class ServerInfoForm extends BaseComponent {
this.props = props;
}
template(): string {
return `
templateHTML(): string {
return htmlEscape`
<div class="settings-card">
<div class="server-info-left">
<img class="server-info-icon" src="${this.props.server.icon}"/>
@@ -56,7 +58,7 @@ export default class ServerInfoForm extends BaseComponent {
}
initForm(): void {
this.$serverInfoForm = this.generateNodeFromTemplate(this.template());
this.$serverInfoForm = this.generateNodeFromHTML(this.templateHTML());
this.$serverInfoAlias = this.$serverInfoForm.querySelectorAll('.server-info-alias')[0];
this.$serverIcon = this.$serverInfoForm.querySelectorAll('.server-info-icon')[0];
this.$deleteServerButton = this.$serverInfoForm.querySelectorAll('.server-delete-action')[0];

View File

@@ -1,6 +1,9 @@
import {htmlEscape} from 'escape-goat';
import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
import NewServerForm from './new-server-form';
import * as t from '../../utils/translation-util';
interface ServersSectionProps {
$root: Element;
@@ -14,16 +17,16 @@ export default class ServersSection extends BaseSection {
this.props = props;
}
template(): string {
return `
<div class="add-server-modal">
<div class="modal-container">
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__('Add a Zulip organization')}</div>
<div id="new-server-container"></div>
templateHTML(): string {
return htmlEscape`
<div class="add-server-modal">
<div class="modal-container">
<div class="settings-pane" id="server-settings-pane">
<div class="page-title">${t.__('Add a Zulip organization')}</div>
<div id="new-server-container"></div>
</div>
</div>
</div>
</div>
`;
}
@@ -32,9 +35,9 @@ export default class ServersSection extends BaseSection {
}
initServers(): void {
this.props.$root.innerHTML = '';
this.props.$root.textContent = '';
this.props.$root.innerHTML = this.template();
this.props.$root.innerHTML = this.templateHTML();
this.$newServerContainer = document.querySelector('#new-server-container');
this.initNewServerForm();

View File

@@ -1,7 +1,10 @@
import BaseSection from './base-section';
import {htmlEscape} from 'escape-goat';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
import BaseSection from './base-section';
interface ShortcutsSectionProps {
$root: Element;
}
@@ -13,14 +16,14 @@ export default class ShortcutsSection extends BaseSection {
this.props = props;
}
// TODO - Deduplicate templateMac and templateWinLin functions. In theory
// TODO - Deduplicate templateMacHTML and templateWinLinHTML functions. In theory
// they both should be the same the only thing different should be the userOSKey
// variable but there seems to be inconsistences between both function, one has more
// lines though one may just be using more new lines and other thing is the use of +.
templateMac(): string {
templateMacHTML(): string {
const userOSKey = '⌘';
return `
return htmlEscape`
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div>
<div class="title">${t.__('Application Shortcuts')}</div>
@@ -181,10 +184,10 @@ export default class ShortcutsSection extends BaseSection {
`;
}
templateWinLin(): string {
templateWinLinHTML(): string {
const userOSKey = 'Ctrl';
return `
return htmlEscape`
<div class="settings-pane">
<div class="settings-card tip"><p><b><i class="material-icons md-14">settings</i>${t.__('Tip')}: </b>${t.__('These desktop app shortcuts extend the Zulip webapp\'s')} <span id="open-hotkeys-link"> ${t.__('keyboard shortcuts')}</span>.</p></div>
<div class="title">${t.__('Application Shortcuts')}</div>
@@ -339,7 +342,7 @@ export default class ShortcutsSection extends BaseSection {
init(): void {
this.props.$root.innerHTML = (process.platform === 'darwin') ?
this.templateMac() : this.templateWinLin();
this.templateMacHTML() : this.templateWinLinHTML();
this.openHotkeysExternalLink();
}
}

View File

@@ -3,18 +3,20 @@ import fs from 'fs';
import isDev from 'electron-is-dev';
import electron_bridge from './electron-bridge';
import {loadBots} from './notification/helpers';
import * as NetworkError from './pages/network';
// eslint-disable-next-line import/no-unassigned-import
import './notification';
// Prevent drag and drop event in main process which prevents remote code executaion
// eslint-disable-next-line import/no-unassigned-import
import './shared/preventdrag';
import electron_bridge from './electron-bridge';
contextBridge.exposeInMainWorld('raw_electron_bridge', electron_bridge);
electron_bridge.once('zulip-loaded', async () => {
await loadBots();
});
ipcRenderer.on('logout', () => {
// Create the menu for the below
const dropdown: HTMLElement = document.querySelector('.dropdown-toggle');

View File

@@ -1,6 +1,6 @@
import {ipcRenderer, remote, WebviewTag, NativeImage} from 'electron';
import path from 'path';
import * as ConfigUtil from './utils/config-util';
const {Tray, Menu, nativeImage, BrowserWindow} = remote;

View File

@@ -1,9 +1,10 @@
import electron from 'electron';
import {JsonDB} from 'node-json-db';
import {initSetUp} from './default-util';
import fs from 'fs';
import path from 'path';
import {JsonDB} from 'node-json-db';
import {initSetUp} from './default-util';
import Logger from './logger-util';
const {app, dialog} =

View File

@@ -1,8 +0,0 @@
// Unescape already encoded/escaped strings
export function decodeString(stringInput: string): string {
const parser = new DOMParser();
const dom = parser.parseFromString(
'<!doctype html><body>' + stringInput,
'text/html');
return dom.body.textContent;
}

View File

@@ -1,10 +1,11 @@
import {JsonDB} from 'node-json-db';
import electron from 'electron';
import fs from 'fs';
import path from 'path';
import electron from 'electron';
import Logger from './logger-util';
import {JsonDB} from 'node-json-db';
import * as EnterpriseUtil from './enterprise-util';
import Logger from './logger-util';
const logger = new Logger({
file: 'config-util.log',

View File

@@ -1,12 +1,13 @@
import {JsonDB} from 'node-json-db';
import {remote, ipcRenderer} from 'electron';
import fs from 'fs';
import path from 'path';
import Logger from './logger-util';
import {remote, ipcRenderer} from 'electron';
import {JsonDB} from 'node-json-db';
import * as Messages from '../../../resources/messages';
import * as EnterpriseUtil from './enterprise-util';
import * as Messages from '../../../resources/messages';
import Logger from './logger-util';
const {app, dialog} = remote;

View File

@@ -1,9 +1,10 @@
import {shell} from 'electron';
import escape from 'escape-html';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {htmlEscape} from 'escape-goat';
export function isUploadsUrl(server: string, url: URL): boolean {
return url.origin === server && url.pathname.startsWith('/user_uploads/');
}
@@ -18,12 +19,12 @@ export async function openBrowser(url: URL): Promise<void> {
path.join(os.tmpdir(), 'zulip-redirect-')
);
const file = path.join(dir, 'redirect.html');
fs.writeFileSync(file, `\
fs.writeFileSync(file, htmlEscape`\
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Refresh" content="0; url=${escape(url.href)}" />
<meta http-equiv="Refresh" content="0; url=${url.href}" />
<title>Redirecting</title>
<style>
html {
@@ -32,11 +33,11 @@ export async function openBrowser(url: URL): Promise<void> {
</style>
</head>
<body>
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p>
<p>Opening <a href="${url.href}">${url.href}</a>…</p>
</body>
</html>
`);
shell.openItem(file);
await shell.openPath(file);
setTimeout(() => {
fs.unlinkSync(file);
fs.rmdirSync(dir);

View File

@@ -1,8 +1,9 @@
import {JsonDB} from 'node-json-db';
import electron from 'electron';
import fs from 'fs';
import path from 'path';
import electron from 'electron';
import {JsonDB} from 'node-json-db';
import Logger from './logger-util';
const remote =

View File

@@ -1,11 +1,12 @@
import {Console} from 'console'; // eslint-disable-line node/prefer-global/console
import {initSetUp} from './default-util';
import {sentryInit, captureException} from './sentry-util';
import electron from 'electron';
import fs from 'fs';
import os from 'os';
import isDev from 'electron-is-dev';
import electron from 'electron';
import {initSetUp} from './default-util';
import {sentryInit, captureException} from './sentry-util';
interface LoggerOptions {
timestamp?: true | (() => string);

View File

@@ -1,7 +1,10 @@
import {ipcRenderer} from 'electron';
import type WebView from '../components/webview';
import backoff from 'backoff';
import {htmlEscape} from 'escape-goat';
import type WebView from '../components/webview';
import Logger from './logger-util';
const logger = new Logger({
@@ -58,7 +61,7 @@ export default class ReconnectUtil {
logger.log('There is no internet connection, try checking network cables, modem and router.');
const errorMessageHolder = document.querySelector('#description');
if (errorMessageHolder) {
errorMessageHolder.innerHTML = `
errorMessageHolder.innerHTML = htmlEscape`
<div>Your internet connection doesn't seem to work properly!</div>
<div>Verify that it works and then click try again.</div>`;
}

View File

@@ -1,5 +1,4 @@
import {init} from '@sentry/electron';
import isDev from 'electron-is-dev';
export const sentryInit = (): void => {

View File

@@ -1,5 +1,4 @@
import {ipcRenderer} from 'electron';
import os from 'os';
export const connectivityERR: string[] = [

View File

@@ -1,5 +1,7 @@
import path from 'path';
import i18n from 'i18n';
import * as ConfigUtil from './config-util';
i18n.configure({

Binary file not shown.

Binary file not shown.

View File

@@ -2,6 +2,44 @@
All notable changes to the Zulip desktop app are documented in this file.
### v5.4.2 --2020-08-12
**Potential Fixes**:
* macOS: Electron 9 upgrade is a potential fix for the ['grey screen issue'](https://chat.zulip.org/#narrow/stream/9-issues/topic/Grey.20Window.20on.20macOS) reported.
**Dependencies**:
* Upgrade all dependencies, including Electron 9.2.0.
### v5.4.1-beta --2020-07-29
**Fixes**:
* Resized the large application icon on macOS dock to be coherent with other icons.
**Potential Fixes**:
* macOS: Electron 9 upgrade is a potential fix for the ['grey screen issue'](https://chat.zulip.org/#narrow/stream/9-issues/topic/Grey.20Window.20on.20macOS) reported.
**Dependencies**:
* Upgrade all dependencies, including Electron 9.1.1.
### v5.4.0 --2020-07-21
**New features**:
* Added support for certificates from system store.
* Added support for Slovak as application language.
**Fixes**:
* Fix bug in *Copy Link* and add *Copy Email* option in context menu.
* Enable *Copy* option in context menu only when copying is possible.
* Remove leading and trailing separators in context menu on non-mac systems.
* ignoreCerts: Accommodate WebSocket URLs in certificate-error handler.
**Dependencies**:
* Upgrade all dependencies, including Electron 8.4.0.
**Deprecations**:
* This release supports certificates from Zulip store as well as system store. Zulip certificate store will be deprecated in the next release.
Users are hereby requested to move to system store. For more information, please see the [documentation](https://zulip.com/help/custom-certificates).
### v5.3.0 --2020-06-24
**Security fixes**:

View File

@@ -1,16 +1,16 @@
'use strict';
const gulp = require('gulp');
const {execSync} = require('child_process');
const electron = require('electron-connect').server.create({
verbose: true
});
const tape = require('gulp-tape');
const tapColorize = require('tap-colorize');
const ts = require('gulp-typescript');
const tsProject = ts.createProject('tsconfig.json');
const glob = require('glob');
const {execSync} = require('child_process');
const gulp = require('gulp');
const tape = require('gulp-tape');
const ts = require('gulp-typescript');
const tapColorize = require('tap-colorize');
const tsProject = ts.createProject('tsconfig.json');
const baseFilePattern = 'app/+(main|renderer)/**/*';
const globOptions = {cwd: __dirname};
const jsFiles = glob.sync(baseFilePattern + '.js', globOptions);

4371
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "zulip",
"productName": "Zulip",
"version": "5.4.0",
"version": "5.4.3",
"main": "./app/main",
"description": "Zulip Desktop App",
"license": "Apache-2.0",
@@ -50,7 +50,7 @@
"files": [
"app/**/*"
],
"copyright": "©2019 Kandra Labs, Inc.",
"copyright": "©2020 Kandra Labs, Inc.",
"mac": {
"category": "public.app-category.productivity",
"target": [
@@ -146,20 +146,20 @@
],
"dependencies": {
"@electron-elements/send-feedback": "^2.0.3",
"@sentry/electron": "^1.4.0",
"@yaireo/tagify": "^3.15.3",
"@sentry/electron": "^2.0.0",
"@yaireo/tagify": "^3.17.10",
"adm-zip": "^0.4.16",
"auto-launch": "^5.0.5",
"backoff": "^2.5.0",
"electron-is-dev": "^1.2.0",
"electron-log": "^4.2.2",
"electron-updater": "^4.3.1",
"electron-log": "^4.2.4",
"electron-updater": "^4.3.4",
"electron-window-state": "^5.0.3",
"escape-html": "^1.0.3",
"escape-goat": "^3.0.0",
"fs-extra": "^9.0.1",
"get-stream": "^5.1.0",
"i18n": "^0.10.0",
"iso-639-1": "^2.1.3",
"get-stream": "^6.0.0",
"i18n": "^0.13.2",
"iso-639-1": "^2.1.4",
"nan": "^2.14.0",
"node-json-db": "^1.1.0",
"semver": "^7.3.2"
@@ -171,17 +171,14 @@
"@types/adm-zip": "^0.4.33",
"@types/auto-launch": "^5.0.1",
"@types/backoff": "^2.5.1",
"@types/escape-html": "^1.0.0",
"@types/fs-extra": "^9.0.1",
"@types/i18n": "^0.8.6",
"@types/node": "^14.0.23",
"@types/i18n": "^0.8.7",
"@types/node": "^14.6.4",
"@types/requestidlecallback": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.1",
"devtron": "^1.4.0",
"dotenv": "^8.2.0",
"electron": "^8.4.0",
"electron-builder": "^22.7.0",
"electron": "^9.3.0",
"electron-builder": "^22.8.0",
"electron-connect": "^0.6.3",
"electron-notarize": "^1.0.0",
"glob": "^7.1.6",
@@ -192,18 +189,29 @@
"nodemon": "^2.0.4",
"pre-commit": "^1.2.2",
"rimraf": "^3.0.2",
"spectron": "^10.0.1",
"stylelint": "^13.6.1",
"spectron": "^11.1.0",
"stylelint": "^13.7.0",
"tap-colorize": "^1.2.0",
"tape": "^5.0.1",
"typescript": "^3.9.7",
"xo": "^0.32.1"
"typescript": "^4.0.2",
"xo": "^0.33.1"
},
"xo": {
"rules": {
"@typescript-eslint/no-dynamic-delete": "off",
"@typescript-eslint/prefer-readonly-parameter-types": "off",
"arrow-body-style": "error",
"import/first": "error",
"import/newline-after-import": "error",
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always"
}
],
"import/unambiguous": "error",
"max-lines": [
"warn",

View File

@@ -1,11 +1,11 @@
'use strict';
const path = require('path');
const dotenv = require('dotenv');
const {notarize} = require('electron-notarize');
dotenv.config({path: path.join(__dirname, '/../.env')});
const {notarize} = require('electron-notarize');
exports.default = async function (context) {
const {electronPlatformName, appOutDir} = context;
if (electronPlatformName !== 'darwin') {

View File

@@ -1,5 +1,6 @@
'use strict';
const test = require('tape');
const setup = require('./setup');
test('app runs', async t => {
@@ -9,7 +10,7 @@ test('app runs', async t => {
try {
await setup.waitForLoad(app, t);
await app.client.windowByIndex(1); // Focus on webview
await app.client.waitForExist('//*[@id="connect"]'); // Id of the connect button
await (await app.client.$('//*[@id="connect"]')).waitForExist(); // Id of the connect button
await setup.endTest(app, t);
} catch (error) {
await setup.endTest(app, t, error || 'error');

View File

@@ -1,8 +1,9 @@
'use strict';
const {Application} = require('spectron');
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const {Application} = require('spectron');
const config = require('./config');
@@ -66,6 +67,7 @@ async function wait(ms) {
// Quit the app, end the test, either in success (!err) or failure (err)
async function endTest(app, t, err) {
await app.client.windowByIndex(0);
await app.stop();
t.end(err);
}

View File

@@ -1,5 +1,6 @@
'use strict';
const test = require('tape');
const setup = require('./setup');
test('add-organization', async t => {
@@ -9,12 +10,12 @@ test('add-organization', async t => {
try {
await setup.waitForLoad(app, t);
await app.client.windowByIndex(1); // Focus on webview
await app.client.setValue('.setting-input-value', 'chat.zulip.org');
await app.client.click('#connect');
await (await app.client.$('.setting-input-value')).setValue('chat.zulip.org');
await (await app.client.$('#connect')).click();
await setup.wait(5000);
await app.client.windowByIndex(0); // Switch focus back to main win
await app.client.windowByIndex(1); // Switch focus back to org webview
await app.client.waitForExist('//*[@id="id_username"]');
await (await app.client.$('//*[@id="id_username"]')).waitForExist();
await setup.endTest(app, t);
} catch (error) {
await setup.endTest(app, t, error || 'error');

View File

@@ -1,5 +1,6 @@
'use strict';
const test = require('tape');
const setup = require('./setup');
// Create new org link should open in the default browser [WIP]
@@ -11,7 +12,7 @@ test('new-org-link', async t => {
try {
await setup.waitForLoad(app, t);
await app.client.windowByIndex(1); // Focus on webview
await app.client.click('#open-create-org-link'); // Click on new org link button
await (await app.client.$('#open-create-org-link')).click(); // Click on new org link button
await setup.wait(5000);
await setup.endTest(app, t);
} catch (error) {