Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3529767
D7786.id26360.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D7786.id26360.diff
View Options
diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js
--- a/keyserver/src/responders/website-responders.js
+++ b/keyserver/src/responders/website-responders.js
@@ -4,10 +4,12 @@
import { detect as detectBrowser } from 'detect-browser';
import type { $Response, $Request } from 'express';
import fs from 'fs';
+import _isEqual from 'lodash/fp/isEqual.js';
import _keyBy from 'lodash/fp/keyBy.js';
import * as React from 'react';
// eslint-disable-next-line import/extensions
import ReactDOMServer from 'react-dom/server';
+import t from 'tcomb';
import { promisify } from 'util';
import { baseLegalPolicies } from 'lib/facts/policies.js';
@@ -23,17 +25,28 @@
createPendingThread,
} from 'lib/shared/thread-utils.js';
import { defaultWebEnabledApps } from 'lib/types/enabled-apps.js';
+import { entryStoreValidator } from 'lib/types/entry-types.js';
import { defaultCalendarFilters } from 'lib/types/filter-types.js';
-import { defaultNumberPerThread } from 'lib/types/message-types.js';
+import {
+ defaultNumberPerThread,
+ messageStoreValidator,
+} from 'lib/types/message-types.js';
import { defaultEnabledReports } from 'lib/types/report-types.js';
import { defaultConnectionInfo } from 'lib/types/socket-types.js';
import { threadPermissions } from 'lib/types/thread-permission-types.js';
import { threadTypes } from 'lib/types/thread-types-enum.js';
+import { threadStoreValidator } from 'lib/types/thread-types.js';
+import {
+ currentUserInfoValidator,
+ userInfosValidator,
+} from 'lib/types/user-types.js';
import { currentDateInTimeZone } from 'lib/utils/date-utils.js';
import { ServerError } from 'lib/utils/errors.js';
import { promiseAll } from 'lib/utils/promises.js';
import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js';
+import { tBool, tNumber, tShape, tString } from 'lib/utils/validation-utils.js';
import getTitle from 'web/title/getTitle.js';
+import { navInfoValidator } from 'web/types/nav-types.js';
import { navInfoFromURL } from 'web/url-utils.js';
import { fetchEntryInfos } from '../fetchers/entry-fetchers.js';
@@ -49,6 +62,7 @@
import { Viewer } from '../session/viewer.js';
import { streamJSON, waitForStream } from '../utils/json-stream.js';
import { getAppURLFactsFromRequestURL } from '../utils/urls.js';
+import { validateOutput } from '../utils/validation-utils.js';
const { renderToNodeStream } = ReactDOMServer;
@@ -141,6 +155,86 @@
}
}
+const initialReduxStateValidator = tShape({
+ navInfo: navInfoValidator,
+ deviceID: t.Nil,
+ currentUserInfo: currentUserInfoValidator,
+ draftStore: t.irreducible('default draftStore', _isEqual({ drafts: {} })),
+ sessionID: t.maybe(t.String),
+ entryStore: entryStoreValidator,
+ threadStore: threadStoreValidator,
+ userStore: tShape({
+ userInfos: userInfosValidator,
+ inconsistencyReports: t.irreducible(
+ 'default inconsistencyReports',
+ _isEqual([]),
+ ),
+ }),
+ messageStore: messageStoreValidator,
+ updatesCurrentAsOf: t.Number,
+ loadingStatuses: t.irreducible('default loadingStatuses', _isEqual({})),
+ calendarFilters: t.irreducible(
+ 'defaultCalendarFilters',
+ _isEqual(defaultCalendarFilters),
+ ),
+ urlPrefix: tString(''),
+ windowDimensions: t.irreducible(
+ 'default windowDimensions',
+ _isEqual({ width: 0, height: 0 }),
+ ),
+ baseHref: t.String,
+ notifPermissionAlertInfo: t.irreducible(
+ 'default notifPermissionAlertInfo',
+ _isEqual(defaultNotifPermissionAlertInfo),
+ ),
+ connection: tShape({
+ status: tString('connecting'),
+ queuedActivityUpdates: t.irreducible(
+ 'default queuedActivityUpdates',
+ _isEqual([]),
+ ),
+ actualizedCalendarQuery: tShape({
+ startDate: t.String,
+ endDate: t.String,
+ filters: t.irreducible(
+ 'default filters',
+ _isEqual(defaultCalendarFilters),
+ ),
+ }),
+ lateResponses: t.irreducible('default lateResponses', _isEqual([])),
+ showDisconnectedBar: tBool(false),
+ }),
+ watchedThreadIDs: t.irreducible('default watchedThreadIDs', _isEqual([])),
+ lifecycleState: tString('active'),
+ enabledApps: t.irreducible(
+ 'defaultWebEnabledApps',
+ _isEqual(defaultWebEnabledApps),
+ ),
+ reportStore: t.irreducible(
+ 'default reportStore',
+ _isEqual({
+ enabledReports: defaultEnabledReports,
+ queuedReports: [],
+ }),
+ ),
+ nextLocalID: tNumber(0),
+ cookie: t.Nil,
+ deviceToken: t.Nil,
+ dataLoaded: t.Boolean,
+ windowActive: tBool(true),
+ userPolicies: t.irreducible('default userPolicies', _isEqual({})),
+ cryptoStore: t.irreducible(
+ 'default cryptoStore',
+ _isEqual({
+ primaryIdentityKeys: null,
+ notificationIdentityKeys: null,
+ }),
+ ),
+ pushApiPublicKey: t.maybe(t.String),
+ _persist: t.Nil,
+ commServicesAccessToken: t.Nil,
+});
+
async function websiteResponder(
viewer: Viewer,
req: $Request,
@@ -432,7 +526,12 @@
_persist: null,
commServicesAccessToken: null,
});
- const jsonStream = streamJSON(res, initialReduxState);
+ const validatedInitialReduxState = validateOutput(
+ viewer,
+ initialReduxStateValidator,
+ initialReduxState,
+ );
+ const jsonStream = streamJSON(res, validatedInitialReduxState);
await waitForStream(jsonStream);
res.end(html`
diff --git a/lib/types/entry-types.js b/lib/types/entry-types.js
--- a/lib/types/entry-types.js
+++ b/lib/types/entry-types.js
@@ -61,6 +61,11 @@
+daysToEntries: { +[day: string]: string[] },
+lastUserInteractionCalendar: number,
};
+export const entryStoreValidator: TInterface<EntryStore> = tShape<EntryStore>({
+ entryInfos: t.dict(tID, rawEntryInfoValidator),
+ daysToEntries: t.dict(t.String, t.list(tID)),
+ lastUserInteractionCalendar: t.Number,
+});
export type CalendarQuery = {
+startDate: string,
diff --git a/lib/types/message-types.js b/lib/types/message-types.js
--- a/lib/types/message-types.js
+++ b/lib/types/message-types.js
@@ -390,6 +390,13 @@
lastNavigatedTo: number, // millisecond timestamp
lastPruned: number, // millisecond timestamp
};
+const threadMessageInfoValidator: TInterface<ThreadMessageInfo> =
+ tShape<ThreadMessageInfo>({
+ messageIDs: t.list(tID),
+ startReached: t.Boolean,
+ lastNavigatedTo: t.Number,
+ lastPruned: t.Number,
+ });
// Tracks client-local information about a message that hasn't been assigned an
// ID by the server yet. As soon as the client gets an ack from the server for
@@ -397,10 +404,18 @@
export type LocalMessageInfo = {
+sendFailed?: boolean,
};
+const localMessageInfoValidator: TInterface<LocalMessageInfo> =
+ tShape<LocalMessageInfo>({
+ sendFailed: t.maybe(t.Boolean),
+ });
export type MessageStoreThreads = {
+[threadID: string]: ThreadMessageInfo,
};
+const messageStoreThreadsValidator: TDict<MessageStoreThreads> = t.dict(
+ tID,
+ threadMessageInfoValidator,
+);
export type MessageStore = {
+messages: { +[id: string]: RawMessageInfo },
@@ -408,6 +423,13 @@
+local: { +[id: string]: LocalMessageInfo },
+currentAsOf: number,
};
+export const messageStoreValidator: TInterface<MessageStore> =
+ tShape<MessageStore>({
+ messages: t.dict(tID, rawMessageInfoValidator),
+ threads: messageStoreThreadsValidator,
+ local: t.dict(t.String, localMessageInfoValidator),
+ currentAsOf: t.Number,
+ });
// MessageStore messages ops
export type RemoveMessageOperation = {
diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js
--- a/lib/types/thread-types.js
+++ b/lib/types/thread-types.js
@@ -28,7 +28,10 @@
import { type ThreadType, threadTypeValidator } from './thread-types-enum.js';
import type { ClientUpdateInfo, ServerUpdateInfo } from './update-types.js';
import type { UserInfo, UserInfos } from './user-types.js';
-import { type ThreadEntity } from '../utils/entity-text.js';
+import {
+ type ThreadEntity,
+ threadEntityValidator,
+} from '../utils/entity-text.js';
import { tID, tShape } from '../utils/validation-utils.js';
export type MemberInfo = {
@@ -49,6 +52,11 @@
+username: ?string,
+isViewer: boolean,
};
+const relativeMemberInfoValidator = tShape<RelativeMemberInfo>({
+ ...memberInfoValidator.meta.props,
+ username: t.maybe(t.String),
+ isViewer: t.Boolean,
+});
export type RoleInfo = {
+id: string,
@@ -133,6 +141,25 @@
+repliesCount: number,
+pinnedCount?: number,
};
+export const threadInfoValidator: TInterface<ThreadInfo> = tShape<ThreadInfo>({
+ id: tID,
+ type: threadTypeValidator,
+ name: t.maybe(t.String),
+ uiName: t.union([t.String, threadEntityValidator]),
+ avatar: t.maybe(clientAvatarValidator),
+ description: t.maybe(t.String),
+ color: t.String,
+ creationTime: t.Number,
+ parentThreadID: t.maybe(tID),
+ containingThreadID: t.maybe(tID),
+ community: t.maybe(tID),
+ members: t.list(relativeMemberInfoValidator),
+ roles: t.dict(tID, roleInfoValidator),
+ currentUser: threadCurrentUserInfoValidator,
+ sourceMessageID: t.maybe(tID),
+ repliesCount: t.Number,
+ pinnedCount: t.maybe(t.Number),
+});
export type ResolvedThreadInfo = {
+id: string,
@@ -185,6 +212,10 @@
export type ThreadStore = {
+threadInfos: { +[id: string]: RawThreadInfo },
};
+export const threadStoreValidator: TInterface<ThreadStore> =
+ tShape<ThreadStore>({
+ threadInfos: t.dict(tID, rawThreadInfoValidator),
+ });
export type RemoveThreadOperation = {
+type: 'remove',
diff --git a/lib/utils/entity-text.js b/lib/utils/entity-text.js
--- a/lib/utils/entity-text.js
+++ b/lib/utils/entity-text.js
@@ -2,12 +2,18 @@
import invariant from 'invariant';
import * as React from 'react';
+import t, { type TInterface, type TUnion } from 'tcomb';
import type { GetENSNames } from './ens-helpers.js';
+import { tID, tShape, tString } from './validation-utils.js';
import { useENSNames } from '../hooks/ens-cache.js';
import { threadNoun } from '../shared/thread-utils.js';
import { stringForUser } from '../shared/user-utils.js';
-import { type ThreadType, threadTypes } from '../types/thread-types-enum.js';
+import {
+ type ThreadType,
+ threadTypes,
+ threadTypeValidator,
+} from '../types/thread-types-enum.js';
import { type RawThreadInfo, type ThreadInfo } from '../types/thread-types.js';
import { basePluralize } from '../utils/text-utils.js';
@@ -18,6 +24,13 @@
+isViewer?: ?boolean,
+possessive?: ?boolean, // eg. `user's` instead of `user`
};
+export const userEntityValidator: TInterface<UserEntity> = tShape<UserEntity>({
+ type: tString('user'),
+ id: t.String,
+ username: t.maybe(t.String),
+ isViewer: t.maybe(t.Boolean),
+ possessive: t.maybe(t.Boolean),
+});
// Comments explain how thread name will appear from user4's perspective
export type ThreadEntity =
@@ -48,6 +61,28 @@
+possessive?: ?boolean, // eg. `this thread's` instead of `this thread`
};
+export const threadEntityValidator: TUnion<ThreadEntity> = t.union([
+ tShape({
+ type: tString('thread'),
+ id: tID,
+ name: t.maybe(t.String),
+ display: tString('uiName'),
+ uiName: t.union([t.list(userEntityValidator), t.String]),
+ ifJustViewer: t.maybe(t.enums.of(['just_you_string', 'viewer_username'])),
+ }),
+ tShape({
+ type: tString('thread'),
+ id: tID,
+ name: t.maybe(t.String),
+ display: tString('shortName'),
+ threadType: t.maybe(threadTypeValidator),
+ parentThreadID: t.maybe(tID),
+ alwaysDisplayShortName: t.maybe(t.Boolean),
+ subchannel: t.maybe(t.Boolean),
+ possessive: t.maybe(t.Boolean),
+ }),
+]);
+
type ColorEntity = {
+type: 'color',
+hex: string,
diff --git a/web/types/nav-types.js b/web/types/nav-types.js
--- a/web/types/nav-types.js
+++ b/web/types/nav-types.js
@@ -1,13 +1,25 @@
// @flow
+import t from 'tcomb';
+import type { TInterface } from 'tcomb';
-import type { BaseNavInfo } from 'lib/types/nav-types.js';
-import type { ThreadInfo } from 'lib/types/thread-types.js';
+import { type BaseNavInfo } from 'lib/types/nav-types.js';
+import {
+ type ThreadInfo,
+ threadInfoValidator,
+} from 'lib/types/thread-types.js';
+import { tID, tShape } from 'lib/utils/validation-utils.js';
export type NavigationTab = 'calendar' | 'chat' | 'settings';
+const navigationTabValidator = t.enums.of(['calendar', 'chat', 'settings']);
export type NavigationSettingsSection = 'account' | 'danger-zone';
+const navigationSettingsSectionValidator = t.enums.of([
+ 'account',
+ 'danger-zone',
+]);
export type NavigationChatMode = 'view' | 'create';
+const navigationChatModeValidator = t.enums.of(['view', 'create']);
export type NavInfo = {
...$Exact<BaseNavInfo>,
@@ -18,3 +30,14 @@
+selectedUserList?: $ReadOnlyArray<string>,
+chatMode?: NavigationChatMode,
};
+
+export const navInfoValidator: TInterface<NavInfo> = tShape<$Exact<NavInfo>>({
+ startDate: t.String,
+ endDate: t.String,
+ tab: navigationTabValidator,
+ activeChatThreadID: t.maybe(tID),
+ pendingThread: t.maybe(threadInfoValidator),
+ settingsSection: t.maybe(navigationSettingsSectionValidator),
+ selectedUserList: t.maybe(t.list(t.String)),
+ chatMode: t.maybe(navigationChatModeValidator),
+});
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Dec 25, 9:11 PM (12 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2702020
Default Alt Text
D7786.id26360.diff (12 KB)
Attached To
Mode
D7786: [web] Validate website responder
Attached
Detach File
Event Timeline
Log In to Comment