diff --git a/packages/lesswrong/components/forumEvents/ForumEventBanner.tsx b/packages/lesswrong/components/forumEvents/ForumEventBanner.tsx index 64e4626eeea..627ee8da9da 100644 --- a/packages/lesswrong/components/forumEvents/ForumEventBanner.tsx +++ b/packages/lesswrong/components/forumEvents/ForumEventBanner.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Components, registerComponent } from "../../lib/vulcan-lib"; import { useLocation } from "../../lib/routeUtil"; +import { hasForumEvents } from "../../lib/betas"; type BannerType = "frontpage" | "postpage"; @@ -13,6 +14,11 @@ export const ForumEventBanner = () => { const {currentRoute} = useLocation(); const bannerType = bannerTypes[currentRoute?.name ?? ""]; const {ForumEventFrontpageBanner, ForumEventPostPageBanner} = Components; + + if (!hasForumEvents) { + return null; + } + switch (bannerType) { case "frontpage": return ( diff --git a/packages/lesswrong/lib/vulcan-lib/components.tsx b/packages/lesswrong/lib/vulcan-lib/components.tsx index 6bf31c6d34b..658d7cdc634 100644 --- a/packages/lesswrong/lib/vulcan-lib/components.tsx +++ b/packages/lesswrong/lib/vulcan-lib/components.tsx @@ -86,22 +86,22 @@ type EmailRenderContextType = { export const EmailRenderContext = React.createContext(null); -const classNameProxy = (componentName: string) => { +const classNameProxy = (prefix: string) => { return new Proxy({}, { get: function(obj: any, prop: any) { // Check that the prop is really a string. This isn't an error that comes // up normally, but apparently React devtools will try to query for non- // string properties sometimes when using the component debugger. if (typeof prop === "string") - return `${componentName}-${prop}`; + return prefix+prop; else - return `${componentName}-invalid`; + return prefix+'invalid'; } }); } const addClassnames = (componentName: string, styles: any) => { - const classesProxy = classNameProxy(componentName); + const classesProxy = classNameProxy(componentName+'-'); return (WrappedComponent: any) => forwardRef((props, ref) => { const emailRenderContext = React.useContext(EmailRenderContext); if (emailRenderContext?.isEmailRender) { @@ -114,7 +114,7 @@ const addClassnames = (componentName: string, styles: any) => { } export const useStyles = (styles: (theme: ThemeType) => JssStyles, componentName: keyof ComponentTypes) => { - return classNameProxy(componentName); + return classNameProxy(componentName+'-'); }; // Register a component. Takes a name, a raw component, and ComponentOptions diff --git a/packages/lesswrong/lib/vulcan-users/permissions.ts b/packages/lesswrong/lib/vulcan-users/permissions.ts index ff0d3908e05..2eb1182bbed 100644 --- a/packages/lesswrong/lib/vulcan-users/permissions.ts +++ b/packages/lesswrong/lib/vulcan-users/permissions.ts @@ -201,23 +201,34 @@ export const userCanReadField = ( document: ObjectsByCollectionName[N], ): boolean => { const canRead = field.canRead; + const userGroups = userGetGroups(user); if (canRead) { - return userHasFieldPermissions(user, canRead, document); + return userHasFieldPermissions(user, userGroups, canRead, document); } return false; }; -const userHasFieldPermissions = (user: UsersCurrent|DbUser|null, canRead: FieldPermissions, document: T): boolean => { +const userHasFieldPermissions = ( + user: UsersCurrent|DbUser|null, + userGroups: string[], + canRead: FieldPermissions, + document: T +): boolean => { if (typeof canRead === 'string') { // if canRead is just a string, we assume it's the name of a group and pass it to isMemberOf - return canRead === 'guests' || userIsMemberOf(user, canRead); + if (canRead === 'guests') return true; + for (let group of userGroups) { + if (group===canRead) + return true; + } + return false; } else if (typeof canRead === 'function') { // if canRead is a function, execute it with user and document passed. it must return a boolean return canRead(user, document); } else if (Array.isArray(canRead) && canRead.length > 0) { // if canRead is an array, we do a recursion on every item and return true if one of the items return true for (const group of canRead) { - if (userHasFieldPermissions(user, group, document)) { + if (userHasFieldPermissions(user, userGroups, group, document)) { return true; } } @@ -271,10 +282,15 @@ export const restrictViewableFieldsSingle = function = {}; + const userGroups = userGetGroups(user); + for (const fieldName in doc) { const fieldSchema = schema[fieldName]; - if (fieldSchema && userCanReadField(user, fieldSchema, doc)) { - restrictedDocument[fieldName] = doc[fieldName]; + if (fieldSchema) { + const canRead = fieldSchema.canRead; + if (canRead && userHasFieldPermissions(user, userGroups, canRead, doc)) { + restrictedDocument[fieldName] = doc[fieldName]; + } } } diff --git a/packages/lesswrong/server/apolloServer.ts b/packages/lesswrong/server/apolloServer.ts index 32295dba97f..459cbb0da0a 100644 --- a/packages/lesswrong/server/apolloServer.ts +++ b/packages/lesswrong/server/apolloServer.ts @@ -4,7 +4,7 @@ import { GraphQLError, GraphQLFormattedError } from 'graphql'; import { isDevelopment, getInstanceSettings, getServerPort, isProduction } from '../lib/executionEnvironment'; import { renderWithCache, getThemeOptionsFromReq } from './vulcan-lib/apollo-ssr/renderPage'; -import { pickerMiddleware } from './vendor/picker'; +import { pickerMiddleware, addStaticRoute } from './vulcan-lib/staticRoutes'; import voyagerMiddleware from 'graphql-voyager/middleware/express'; import { graphiqlMiddleware } from './vulcan-lib/apollo-server/graphiql'; import getPlaygroundConfig from './vulcan-lib/apollo-server/playground'; @@ -27,7 +27,6 @@ import { addAuthMiddlewares, expressSessionSecretSetting } from './authenticatio import { addForumSpecificMiddleware } from './forumSpecificMiddleware'; import { addSentryMiddlewares, logGraphqlQueryStarted, logGraphqlQueryFinished } from './logging'; import { addClientIdMiddleware } from './clientIdMiddleware'; -import { addStaticRoute } from './vulcan-lib/staticRoutes'; import { classesForAbTestGroups } from '../lib/abTestImpl'; import expressSession from 'express-session'; import MongoStore from './vendor/ConnectMongo/MongoStore'; diff --git a/packages/lesswrong/server/repos/PageCacheRepo.ts b/packages/lesswrong/server/repos/PageCacheRepo.ts index 1644eca13ea..15d4e2988c1 100644 --- a/packages/lesswrong/server/repos/PageCacheRepo.ts +++ b/packages/lesswrong/server/repos/PageCacheRepo.ts @@ -97,7 +97,12 @@ class PageCacheRepo extends AbstractRepo<"PageCache"> { renderedAt: now, expiresAt: new Date(now.getTime() + maxCacheAgeMs), ttlMs: maxCacheAgeMs, - renderResult, + + // Stringify renderResult before handing it to the postgres library. We + // do this because the string can be large, and if we pass it as a JSON + // object, the postgres library will stringify it in a slower way that + // adds bignum support (which we don't use). + renderResult: JSON.stringify(renderResult), schemaVersion: 1, createdAt: now, }); diff --git a/packages/lesswrong/server/resolvers/revisionResolvers.ts b/packages/lesswrong/server/resolvers/revisionResolvers.ts index f7c326a67ef..69cd76edcd8 100644 --- a/packages/lesswrong/server/resolvers/revisionResolvers.ts +++ b/packages/lesswrong/server/resolvers/revisionResolvers.ts @@ -100,7 +100,7 @@ augmentFieldsDict(Revisions, { type: 'String!', resolver: ({html}): string => { if (!html) return "" - const truncatedHtml = truncate(sanitize(html), PLAINTEXT_HTML_TRUNCATION_LENGTH) + const truncatedHtml = truncate(html, PLAINTEXT_HTML_TRUNCATION_LENGTH) return htmlToTextPlaintextDescription(truncatedHtml).substring(0, PLAINTEXT_DESCRIPTION_LENGTH); } } diff --git a/packages/lesswrong/server/vendor/picker.ts b/packages/lesswrong/server/vendor/picker.ts deleted file mode 100644 index 934d9b506df..00000000000 --- a/packages/lesswrong/server/vendor/picker.ts +++ /dev/null @@ -1,149 +0,0 @@ -// Picker -// Adapted from https://github.com/meteorhacks/picker -// -// (The MIT License) -// -// Copyright (c) 2014 MeteorHacks PVT Ltd. hello@meteorhacks.com -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the 'Software'), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import pathToRegexp from 'path-to-regexp'; -import URL from 'url'; -import type { NextFunction, ParamsDictionary, Query, Response } from 'express-serve-static-core'; -import type { RequestHandler } from 'express'; -import type { IncomingMessage, ServerResponse } from 'http'; -const urlParse = URL.parse; - -type Req = Parameters[0]; -type Res = Response, number>; -type FilterFunction = (req: Req, res: Res) => any; -type RouteCallback = (props: any, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void | Promise; - -class PickerImp { - filterFunction: FilterFunction | null - routes: (pathToRegexp.PathRegExp & { callback: any })[] - subRouters: PickerImp[] - middlewares: any - - constructor(filterFunction: FilterFunction | null) { - this.filterFunction = filterFunction; - this.routes = []; - this.subRouters = []; - this.middlewares = []; - } - - middleware = (callback: any) => { - this.middlewares.push(callback); - }; - - route = (path: pathToRegexp.Path, callback: RouteCallback) => { - var regExp = pathToRegexp(path); - const regExpWithCallback = Object.assign(regExp, { callback }); - this.routes.push(regExpWithCallback); - return this; - }; - - filter = (callback: FilterFunction) => { - var subRouter = new PickerImp(callback); - this.subRouters.push(subRouter); - return subRouter; - }; - - _dispatch = (req: Req, res: Res, bypass: NextFunction) => { - var self = this; - var currentRoute = 0; - var currentSubRouter = 0; - var currentMiddleware = 0; - - if(this.filterFunction) { - var result = this.filterFunction(req, res); - if(!result) { - return bypass(); - } - } - - processNextMiddleware(); - function processNextMiddleware () { - var middleware = self.middlewares[currentMiddleware++]; - if(middleware) { - self._processMiddleware(middleware, req, res, processNextMiddleware); - } else { - processNextRoute(); - } - } - - function processNextRoute () { - var route = self.routes[currentRoute++]; - if(route) { - var uri = req.url.replace(/\?.*/, ''); - var m = uri.match(route); - if(m) { - var params = self._buildParams(route.keys, m); - params.query = urlParse(req.url, true).query; - self._processRoute(route.callback, params, req, res, bypass); - } else { - processNextRoute(); - } - } else { - processNextSubRouter(); - } - } - - function processNextSubRouter () { - var subRouter = self.subRouters[currentSubRouter++]; - if(subRouter) { - subRouter._dispatch(req, res, processNextSubRouter); - } else { - bypass(); - } - } - }; - - _buildParams = (keys: pathToRegexp.Key[], m: RegExpMatchArray) => { - var params: any = {}; - for(var lc=1; lc { - doCall(); - - function doCall () { - void callback.call(null, params, req, res, next); - } - }; - - _processMiddleware = (middleware: any, req: Req, res: Res, next: () => void) => { - doCall(); - - function doCall() { - middleware.call(null, req, res, next); - } - }; -} - -export const Picker = new PickerImp(null); -export const pickerMiddleware: RequestHandler> = function(req, res, next) { - Picker._dispatch(req, res, next); -} diff --git a/packages/lesswrong/server/vulcan-lib/staticRoutes.ts b/packages/lesswrong/server/vulcan-lib/staticRoutes.ts index cc0b3fb8376..e7bccd5af4c 100644 --- a/packages/lesswrong/server/vulcan-lib/staticRoutes.ts +++ b/packages/lesswrong/server/vulcan-lib/staticRoutes.ts @@ -1,9 +1,74 @@ -import type { NextFunction } from 'express'; +// Adapted from https://github.com/meteorhacks/picker +// +// Which is under the MIT License: +// +// Copyright (c) 2014 MeteorHacks PVT Ltd. hello@meteorhacks.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the 'Software'), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + import type { IncomingMessage, ServerResponse } from 'http'; -import { Picker } from '../vendor/picker'; +import pathToRegexp from 'path-to-regexp'; +import URL from 'url'; +import type { NextFunction, ParamsDictionary, Query, Response } from 'express-serve-static-core'; +import type { RequestHandler } from 'express'; +const urlParse = URL.parse; + +type Req = Parameters[0]; +type Res = Response, number>; +type RouteCallback = (props: any, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void | Promise; + +let routes: (pathToRegexp.PathRegExp & { callback: any })[] = []; + +function dispatch(req: Req, res: Res, next: NextFunction) { + for (const route of routes) { + var uri = req.url.replace(/\?.*/, ''); + var m = uri.match(route); + if (!m) continue; + + var params = buildParams(route.keys, m); + params.query = urlParse(req.url, true).query; + + route.callback.call(null, params, req, res, next); + return; + } + next(); +}; + +function buildParams(keys: pathToRegexp.Key[], m: RegExpMatchArray) { + var params: any = {}; + for(var lc=1; lc> = function(req, res, next) { + dispatch(req, res, next); +} /// Add a route which renders by putting things into the http response body /// directly, rather than using all the Meteor/Apollo/etc stuff. -export const addStaticRoute = (url: string, handler: (props: any, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void|Promise) => { - Picker.route(url, handler); -} +export function addStaticRoute(path: pathToRegexp.Path, callback: RouteCallback) { + var regExp = pathToRegexp(path); + const regExpWithCallback = Object.assign(regExp, { callback }); + routes.push(regExpWithCallback); +};