Просмотр исходного кода

Merge pull request #6375 from weseek/imprv/page-redirect-chains-operation

imprv: Implement page redirection
Yuki Takei 3 лет назад
Родитель
Сommit
96708ce597

+ 18 - 6
packages/app/src/pages/[[...path]].page.tsx

@@ -7,6 +7,7 @@ import {
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, isClient, isIPageInfoForEntity, isServer, IUser, IUserHasId, pagePathUtils, pathUtils,
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
+import mongoose from 'mongoose';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -29,6 +30,7 @@ import { RendererConfig } from '~/interfaces/services/renderer';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { PageModel, PageDocument } from '~/server/models/page';
+import { PageRedirectModel } from '~/server/models/page-redirect';
 import UserUISettings from '~/server/models/user-ui-settings';
 import Xss from '~/services/xss';
 import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
@@ -41,11 +43,11 @@ import loggerFactory from '~/utils/logger';
 
 // import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
 // import GrowiSubNavigationSwitcher from '../client/js/components/Navbar/GrowiSubNavigationSwitcher';
+import ForbiddenPage from '../components/ForbiddenPage';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
-import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 import { NotCreatablePage } from '../components/NotCreatablePage';
-import ForbiddenPage from '../components/ForbiddenPage';
+import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 
 // import { serializeUserSecurely } from '../server/models/serializers/user-serializer';
 // import PageStatusAlert from '../client/js/components/PageStatusAlert';
@@ -121,8 +123,7 @@ type Props = CommonProps & {
 
   pageWithMeta: IPageToShowRevisionWithMeta,
   // pageUser?: any,
-  // redirectTo?: string;
-  // redirectFrom?: string;
+  redirectFrom?: string;
 
   // shareLinkId?: string;
   isLatestRevision?: boolean
@@ -355,17 +356,28 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   const { revisionId } = req.query;
 
   const Page = crowi.model('Page') as PageModel;
+  const PageRedirect = mongoose.model('PageRedirect') as PageRedirectModel;
   const { pageService } = crowi;
 
-  const { currentPathname } = props;
+  let currentPathname = props.currentPathname;
 
   const pageId = getPageIdFromPathname(currentPathname);
   const isPermalink = _isPermalink(currentPathname);
 
   const { user } = req;
 
-  // check whether the specified page path hits to multiple pages
   if (!isPermalink) {
+    // check redirects
+    const chains = await PageRedirect.retrievePageRedirectChains(currentPathname);
+    if (chains != null) {
+      // overwrite currentPathname
+      currentPathname = chains.end.toPath;
+      props.currentPathname = currentPathname;
+      // set redirectFrom
+      props.redirectFrom = chains.start.fromPath;
+    }
+
+    // check whether the specified page path hits to multiple pages
     const count = await Page.countByPathAndViewer(currentPathname, user, null, true);
     if (count > 1) {
       throw new MultiplePagesHitsError(currentPathname);

+ 25 - 1
packages/app/src/server/models/page-redirect.ts

@@ -6,6 +6,11 @@ import {
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
+export interface IPageRedirectChains {
+  start: IPageRedirect,
+  end: IPageRedirect,
+}
+
 export interface IPageRedirect {
   fromPath: string,
   toPath: string,
@@ -14,7 +19,8 @@ export interface IPageRedirect {
 export interface PageRedirectDocument extends IPageRedirect, Document {}
 
 export interface PageRedirectModel extends Model<PageRedirectDocument> {
-  [x:string]: any // TODO: improve type
+  retrievePageRedirectChains(fromPath: string, storedChains?: IPageRedirectChains): Promise<IPageRedirectChains>
+  removePageRedirectByToPath(toPath: string): Promise<void>
 }
 
 const schema = new Schema<PageRedirectDocument, PageRedirectModel>({
@@ -24,10 +30,28 @@ const schema = new Schema<PageRedirectDocument, PageRedirectModel>({
   toPath: { type: String, required: true },
 });
 
+schema.statics.retrievePageRedirectChains = async function(fromPath: string, storedChains?: IPageRedirectChains): Promise<IPageRedirectChains|null> {
+  const chainedRedirect = await this.findOne({ fromPath });
+
+  if (chainedRedirect == null) {
+    return storedChains ?? null;
+  }
+
+  const chains = storedChains ?? { start: chainedRedirect, end: chainedRedirect };
+  chains.end = chainedRedirect;
+
+  // find the end recursively
+  return this.retrievePageRedirectChains(chainedRedirect.toPath, chains);
+};
+
 schema.statics.removePageRedirectByToPath = async function(toPath: string): Promise<void> {
   await this.deleteMany({ toPath });
 
   return;
 };
 
+// schema.statics.removePageRedirectsByChains = async function(redirectChains: IPageRedirectChains): Promise<void> {
+//   return;
+// };
+
 export default getOrCreateModel<PageRedirectDocument, PageRedirectModel>('PageRedirect', schema);

+ 86 - 0
packages/app/test/integration/models/page-redirect.test.js

@@ -0,0 +1,86 @@
+import mongoose from 'mongoose';
+
+import { IPageRedirect, PageRedirectModel } from '../../../src/server/models/page-redirect';
+import { getInstance } from '../setup-crowi';
+
+describe('PageRedirect', () => {
+  // eslint-disable-next-line no-unused-vars
+  let crowi;
+  let PageRedirect;
+
+  beforeAll(async() => {
+    crowi = await getInstance();
+
+    PageRedirect = mongoose.model('PageRedirect');
+  });
+
+  describe('.removePageRedirectByToPath', () => {
+    test('works fine', async() => {
+      // setup:
+      await PageRedirect.insertMany([
+        { fromPath: '/org/path1', toPath: '/path1' },
+        { fromPath: '/org/path2', toPath: '/path2' },
+        { fromPath: '/org/path3', toPath: '/path3' },
+        { fromPath: '/org/path33', toPath: '/path3' },
+      ]);
+      expect(await PageRedirect.findOne({ fromPath: '/org/path1' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path2' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path3' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path33' })).not.toBeNull();
+
+      // when:
+      // remove all documents that have { toPath: '/path/3' }
+      await PageRedirect.removePageRedirectByToPath('/path3');
+
+      // then:
+      const r1 = await PageRedirect.findOne({ fromPath: '/org/path1' });
+      const r2 = await PageRedirect.findOne({ fromPath: '/org/path2' });
+      const r3 = await PageRedirect.findOne({ fromPath: '/org/path3' });
+      const r4 = await PageRedirect.findOne({ fromPath: '/org/path33' });
+      expect(r1).not.toBeNull();
+      expect(r2).not.toBeNull();
+      expect(r3).toBeNull();
+      expect(r4).toBeNull();
+    });
+  });
+
+  describe('.retrievePageRedirectChains', () => {
+    test('shoud return null when data is not found', async() => {
+      // setup:
+      expect(await PageRedirect.findOne({ fromPath: '/path1' })).toBeNull();
+
+      // when:
+      // retrieve
+      const chains = await PageRedirect.retrievePageRedirectChains('/path1');
+
+      // then:
+      expect(chains).toBeNull();
+    });
+
+    test('shoud return IPageRedirectChains', async() => {
+      // setup:
+      await PageRedirect.insertMany([
+        { fromPath: '/path1', toPath: '/path2' },
+        { fromPath: '/path2', toPath: '/path3' },
+        { fromPath: '/path3', toPath: '/path4' },
+      ]);
+      expect(await PageRedirect.findOne({ fromPath: '/path1' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/path2' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/path3' })).not.toBeNull();
+
+      // when:
+      // retrieve
+      const chains = await PageRedirect.retrievePageRedirectChains('/path1');
+
+      // then:
+      expect(chains).not.toBeNull();
+      expect(chains.start).not.toBeNull();
+      expect(chains.start.fromPath).toEqual('/path1');
+      expect(chains.start.toPath).toEqual('/path2');
+      expect(chains.end).not.toBeNull();
+      expect(chains.end.fromPath).toEqual('/path3');
+      expect(chains.end.toPath).toEqual('/path4');
+    });
+  });
+
+});