page-path-utils.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import nodePath from 'path';
  2. import escapeStringRegexp from 'escape-string-regexp';
  3. import { isValidObjectId } from './objectid-utils';
  4. import { addTrailingSlash } from './path-utils';
  5. /**
  6. * Whether path is the top page
  7. * @param path
  8. */
  9. export const isTopPage = (path: string): boolean => {
  10. return path === '/';
  11. };
  12. /**
  13. * Whether path is the top page of users
  14. * @param path
  15. */
  16. export const isUsersTopPage = (path: string): boolean => {
  17. return path === '/user';
  18. };
  19. /**
  20. * Whether the path is permalink
  21. * @param path
  22. */
  23. export const isPermalink = (path: string): boolean => {
  24. const pageIdStr = path.substring(1);
  25. return isValidObjectId(pageIdStr);
  26. };
  27. /**
  28. * Whether path is user's home page
  29. * @param path
  30. */
  31. export const isUsersHomePage = (path: string): boolean => {
  32. // https://regex101.com/r/utVQct/1
  33. if (path.match(/^\/user\/[^/]+$/)) {
  34. return true;
  35. }
  36. return false;
  37. };
  38. /**
  39. * Whether path is the protected pages for systems
  40. * @param path
  41. */
  42. export const isUsersProtectedPages = (path: string): boolean => {
  43. return isUsersTopPage(path) || isUsersHomePage(path);
  44. };
  45. /**
  46. * Whether path is movable
  47. * @param path
  48. */
  49. export const isMovablePage = (path: string): boolean => {
  50. return !isTopPage(path) && !isUsersProtectedPages(path);
  51. };
  52. /**
  53. * Whether path belongs to the user page
  54. * @param path
  55. */
  56. export const isUserPage = (path: string): boolean => {
  57. // https://regex101.com/r/MwifLR/1
  58. if (path.match(/^\/user\/.*?$/)) {
  59. return true;
  60. }
  61. return false;
  62. };
  63. /**
  64. * Whether path is the top page of users
  65. * @param path
  66. */
  67. export const isTrashTopPage = (path: string): boolean => {
  68. return path === '/trash';
  69. };
  70. /**
  71. * Whether path belongs to the trash page
  72. * @param path
  73. */
  74. export const isTrashPage = (path: string): boolean => {
  75. // https://regex101.com/r/BSDdRr/1
  76. if (path.match(/^\/trash(\/.*)?$/)) {
  77. return true;
  78. }
  79. return false;
  80. };
  81. /**
  82. * Whether path belongs to the shared page
  83. * @param path
  84. */
  85. export const isSharedPage = (path: string): boolean => {
  86. // https://regex101.com/r/ZjdOiB/1
  87. if (path.match(/^\/share(\/.*)?$/)) {
  88. return true;
  89. }
  90. return false;
  91. };
  92. const restrictedPatternsToCreate: Array<RegExp> = [
  93. /\^|\$|\*|\+|#|%|\?/,
  94. /^\/-\/.*/,
  95. /^\/_r\/.*/,
  96. /^\/_apix?(\/.*)?/,
  97. /^\/?https?:\/\/.+$/, // avoid miss in renaming
  98. /\/{2,}/, // avoid miss in renaming
  99. /\s+\/\s+/, // avoid miss in renaming
  100. /.+\/edit$/,
  101. /.+\.md$/,
  102. /^(\.\.)$/, // see: https://github.com/weseek/growi/issues/3582
  103. /(\/\.\.)\/?/, // see: https://github.com/weseek/growi/issues/3582
  104. /^\/(_search|_private-legacy-pages)(\/.*|$)/,
  105. /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments|tags|share)(\/.*|$)/,
  106. /^\/user\/[^/]+$/, // see: https://regex101.com/r/utVQct/1
  107. ];
  108. export const isCreatablePage = (path: string): boolean => {
  109. return !restrictedPatternsToCreate.some(pattern => path.match(pattern));
  110. };
  111. /**
  112. * return user path
  113. * @param user
  114. */
  115. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  116. export const userPageRoot = (user: any): string => {
  117. if (!user || !user.username) {
  118. return '';
  119. }
  120. return `/user/${user.username}`;
  121. };
  122. /**
  123. * return user path
  124. * @param parentPath
  125. * @param childPath
  126. * @param newPath
  127. */
  128. export const convertToNewAffiliationPath = (oldPath: string, newPath: string, childPath: string): string => {
  129. if (newPath == null) {
  130. throw new Error('Please input the new page path');
  131. }
  132. const pathRegExp = new RegExp(`^${escapeStringRegexp(oldPath)}`, 'i');
  133. return childPath.replace(pathRegExp, newPath);
  134. };
  135. /**
  136. * Encode SPACE and IDEOGRAPHIC SPACE
  137. * @param {string} path
  138. * @returns {string}
  139. */
  140. export const encodeSpaces = (path?:string): string | undefined => {
  141. if (path == null) {
  142. return undefined;
  143. }
  144. // Encode SPACE and IDEOGRAPHIC SPACE
  145. return path.replace(/ /g, '%20').replace(/\u3000/g, '%E3%80%80');
  146. };
  147. /**
  148. * Generate editor path
  149. * @param {string} paths
  150. * @returns {string}
  151. */
  152. export const generateEditorPath = (...paths: string[]): string => {
  153. const joinedPath = [...paths].join('/');
  154. if (!isCreatablePage(joinedPath)) {
  155. throw new Error('Invalid characters on path');
  156. }
  157. try {
  158. const url = new URL(joinedPath, 'https://dummy');
  159. return `${url.pathname}#edit`;
  160. }
  161. catch (err) {
  162. throw new Error('Invalid path format');
  163. }
  164. };
  165. /**
  166. * returns ancestors paths
  167. * @param {string} path
  168. * @param {string[]} ancestorPaths
  169. * @returns {string[]}
  170. */
  171. export const collectAncestorPaths = (path: string, ancestorPaths: string[] = []): string[] => {
  172. if (isTopPage(path)) return ancestorPaths;
  173. const parentPath = nodePath.dirname(path);
  174. ancestorPaths.push(parentPath);
  175. return collectAncestorPaths(parentPath, ancestorPaths);
  176. };
  177. /**
  178. * return paths without duplicate area of regexp /^${path}\/.+/i
  179. * ex. expect(omitDuplicateAreaPathFromPaths(['/A', '/A/B', '/A/B/C'])).toStrictEqual(['/A'])
  180. * @param paths paths to be tested
  181. * @returns omitted paths
  182. */
  183. export const omitDuplicateAreaPathFromPaths = (paths: string[]): string[] => {
  184. const uniquePaths = Array.from(new Set(paths));
  185. return uniquePaths.filter((path) => {
  186. const isDuplicate = uniquePaths.filter(p => (new RegExp(`^${p}\\/.+`, 'i')).test(path)).length > 0;
  187. return !isDuplicate;
  188. });
  189. };
  190. /**
  191. * return pages with path without duplicate area of regexp /^${path}\/.+/i
  192. * if the pages' path are the same, it will NOT omit any of them since the other attributes will not be the same
  193. * @param paths paths to be tested
  194. * @returns omitted paths
  195. */
  196. export const omitDuplicateAreaPageFromPages = (pages: any[]): any[] => {
  197. return pages.filter((page) => {
  198. const isDuplicate = pages.some(p => (new RegExp(`^${p.path}\\/.+`, 'i')).test(page.path));
  199. return !isDuplicate;
  200. });
  201. };
  202. /**
  203. * Check if the area of either path1 or path2 includes the area of the other path
  204. * The area of path is the same as /^\/hoge\//i
  205. * @param pathToTest string
  206. * @param pathToBeTested string
  207. * @returns boolean
  208. */
  209. export const isEitherOfPathAreaOverlap = (path1: string, path2: string): boolean => {
  210. if (path1 === path2) {
  211. return true;
  212. }
  213. const path1WithSlash = addTrailingSlash(path1);
  214. const path2WithSlash = addTrailingSlash(path2);
  215. const path1Area = new RegExp(`^${escapeStringRegexp(path1WithSlash)}`, 'i');
  216. const path2Area = new RegExp(`^${escapeStringRegexp(path2WithSlash)}`, 'i');
  217. if (path1Area.test(path2) || path2Area.test(path1)) {
  218. return true;
  219. }
  220. return false;
  221. };
  222. /**
  223. * Check if the area of pathToTest includes the area of pathToBeTested
  224. * The area of path is the same as /^\/hoge\//i
  225. * @param pathToTest string
  226. * @param pathToBeTested string
  227. * @returns boolean
  228. */
  229. export const isPathAreaOverlap = (pathToTest: string, pathToBeTested: string): boolean => {
  230. if (pathToTest === pathToBeTested) {
  231. return true;
  232. }
  233. const pathWithSlash = addTrailingSlash(pathToTest);
  234. const pathAreaToTest = new RegExp(`^${escapeStringRegexp(pathWithSlash)}`, 'i');
  235. if (pathAreaToTest.test(pathToBeTested)) {
  236. return true;
  237. }
  238. return false;
  239. };
  240. /**
  241. * Determine whether can move by fromPath and toPath
  242. * @param fromPath string
  243. * @param toPath string
  244. * @returns boolean
  245. */
  246. export const canMoveByPath = (fromPath: string, toPath: string): boolean => {
  247. return !isPathAreaOverlap(fromPath, toPath);
  248. };
  249. /**
  250. * check if string has '/' in it
  251. */
  252. export const hasSlash = (str: string): boolean => {
  253. return str.includes('/');
  254. };
  255. /**
  256. * Generate RegExp instance for one level lower path
  257. */
  258. export const generateChildrenRegExp = (path: string): RegExp => {
  259. // https://regex101.com/r/laJGzj/1
  260. // ex. /any_level1
  261. if (isTopPage(path)) return new RegExp(/^\/[^/]+$/);
  262. // https://regex101.com/r/mrDJrx/1
  263. // ex. /parent/any_child OR /any_level1
  264. return new RegExp(`^${path}(\\/[^/]+)\\/?$`);
  265. };