Add support for client-side room alias hash `#` redirects to the correct URL (#111)

This helps when someone just pastes a room alias on the end of the domain,

 - `/#room-alias:server` -> `/r/room-alias:server`
 - `/r/#room-alias:server/date/2022/10/27` -> `/r/room-alias:server/date/2022/10/27`

Since these redirects happen on the client, we can't write any e2e tests. Those e2e tests do everything but run client-side JavaScript.

Follow-up to https://github.com/matrix-org/matrix-public-archive/pull/107

Part of https://github.com/matrix-org/matrix-public-archive/issues/25
This commit is contained in:
Eric Eastwood 2022-10-28 00:32:24 -05:00 committed by GitHub
parent 7a88ea0c19
commit 2b4ecb737a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 198 additions and 2 deletions

View File

@ -19,6 +19,7 @@ const generateViteConfigForEntryPoint = require('./generate-vite-config-for-entr
const entryPoints = [ const entryPoints = [
path.resolve(__dirname, '../public/js/entry-client-hydrogen.js'), path.resolve(__dirname, '../public/js/entry-client-hydrogen.js'),
path.resolve(__dirname, '../public/js/entry-client-room-directory.js'), path.resolve(__dirname, '../public/js/entry-client-room-directory.js'),
path.resolve(__dirname, '../public/js/entry-client-room-alias-hash-redirect.js'),
]; ];
async function buildClientScripts(extraConfig = {}) { async function buildClientScripts(extraConfig = {}) {

View File

@ -155,6 +155,32 @@
color: initial; color: initial;
} }
.RoomDirectoryView_notificationToast {
position: fixed;
top: 20px;
left: 20px;
max-width: 450px;
margin-right: 20px;
padding: 20px;
background: hsl(207deg 36% 18% / 90%);
border-radius: 8px;
color: #fff;
}
.RoomDirectoryView_notificationToastTitle {
margin-top: 0;
margin-bottom: 0;
font-size: 14px;
font-weight: bold;
}
.RoomDirectoryView_notificationToastDescription {
margin-top: 1em;
margin-bottom: 0;
}
.RoomDirectoryView_mainContent { .RoomDirectoryView_mainContent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -0,0 +1,18 @@
import assert from 'matrix-public-archive-shared/lib/assert';
import MatrixPublicArchiveURLCreator from 'matrix-public-archive-shared/lib/url-creator';
import redirectIfRoomAliasInHash from 'matrix-public-archive-shared/lib/redirect-if-room-alias-in-hash';
const config = window.matrixPublicArchiveContext.config;
assert(config);
assert(config.basePath);
const matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(config.basePath);
console.log(`Trying to redirect based on pageHash=${window.location.hash}`);
const isRedirecting = redirectIfRoomAliasInHash(matrixPublicArchiveURLCreator);
// Show the message while we're trying to redirect or if we found nothing, remove the
// message
document.querySelector('.js-try-redirect-message').style.display = isRedirecting
? 'inline'
: 'none';

View File

@ -0,0 +1,62 @@
'use strict';
const assert = require('assert');
const urlJoin = require('url-join');
const safeJson = require('../lib/safe-json');
const sanitizeHtml = require('../lib/sanitize-html');
const config = require('../lib/config');
const basePath = config.get('basePath');
assert(basePath);
// Since everything after the hash (`#`) won't make it to the server, let's serve a 404
// page that will potentially redirect them to the correct place if they tried
// `/r/#room-alias:server/date/2022/10/27` -> `/r/room-alias:server/date/2022/10/27`
function clientSideRoomAliasHashRedirectRoute(req, res) {
const cspNonce = res.locals.cspNonce;
const hydrogenStylesUrl = urlJoin(basePath, '/hydrogen-styles.css');
const stylesUrl = urlJoin(basePath, '/css/styles.css');
const jsBundleUrl = urlJoin(basePath, '/js/entry-client-room-alias-hash-redirect.es.js');
const context = {
config: {
basePath,
},
};
const serializedContext = JSON.stringify(context);
const pageHtml = `
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page not found - Matrix Public Archive</title>
<link href="${hydrogenStylesUrl}" rel="stylesheet" nonce="${cspNonce}">
<link href="${stylesUrl}" rel="stylesheet" nonce="${cspNonce}">
</head>
${/* We add the .hydrogen class here just to get normal body styles */ ''}
<body class="hydrogen">
<h1>
404: Page not found.
<span class="js-try-redirect-message" style="display: none">One sec while we try to redirect you to the right place.</span>
</h1>
<p>If there was a #room_alias:server hash in the URL, we tried redirecting you to the right place.</p>
<p>
Otherwise, you're simply in a place that does not exist.
You can ${sanitizeHtml(`<a href="${basePath}">go back to the homepage</a>.`)}
</p>
<script type="text/javascript" nonce="${cspNonce}">
window.matrixPublicArchiveContext = ${safeJson(serializedContext)}
</script>
<script type="text/javascript" src="${jsBundleUrl}" nonce="${cspNonce}"></script>
</body>
</html>
`;
res.status(404);
res.set('Content-Type', 'text/html');
res.send(pageHtml);
}
module.exports = clientSideRoomAliasHashRedirectRoute;

View File

@ -8,6 +8,7 @@ const { handleTracingMiddleware } = require('../tracing/tracing-middleware');
const getVersionTags = require('../lib/get-version-tags'); const getVersionTags = require('../lib/get-version-tags');
const preventClickjackingMiddleware = require('./prevent-clickjacking-middleware'); const preventClickjackingMiddleware = require('./prevent-clickjacking-middleware');
const contentSecurityPolicyMiddleware = require('./content-security-policy-middleware'); const contentSecurityPolicyMiddleware = require('./content-security-policy-middleware');
const clientSideRoomAliasHashRedirectRoute = require('./client-side-room-alias-hash-redirect-route');
const redirectToCorrectArchiveUrlIfBadSigil = require('./redirect-to-correct-archive-url-if-bad-sigil-middleware'); const redirectToCorrectArchiveUrlIfBadSigil = require('./redirect-to-correct-archive-url-if-bad-sigil-middleware');
function installRoutes(app) { function installRoutes(app) {
@ -62,6 +63,11 @@ function installRoutes(app) {
// For room aliases (/r) or room ID's (/roomid) // For room aliases (/r) or room ID's (/roomid)
app.use('/:entityDescriptor(r|roomid)/:roomIdOrAliasDirty', require('./room-routes')); app.use('/:entityDescriptor(r|roomid)/:roomIdOrAliasDirty', require('./room-routes'));
// Since everything after the hash (`#`) won't make it to the server, let's serve a 404
// page that will potentially redirect them to the correct place if they tried
// `/r/#room-alias:server/date/2022/10/27` -> `/r/room-alias:server/date/2022/10/27`
app.use('/:entityDescriptor(r|roomid)', clientSideRoomAliasHashRedirectRoute);
// Correct any honest mistakes: If someone accidentally put the sigil in the URL, then // Correct any honest mistakes: If someone accidentally put the sigil in the URL, then
// redirect them to the correct URL without the sigil to the correct path above. // redirect them to the correct URL without the sigil to the correct path above.
app.use('/:roomIdOrAliasDirty', redirectToCorrectArchiveUrlIfBadSigil); app.use('/:roomIdOrAliasDirty', redirectToCorrectArchiveUrlIfBadSigil);

View File

@ -0,0 +1,45 @@
'use strict';
// https://spec.matrix.org/v1.1/appendices/#room-aliases
// - `#room_alias:domain`
// - `#room-alias:server/date/2022/10/27`
const BASIC_ROOM_ALIAS_REGEX = /^(#(?:[^/:]+):(?:[^/]+))/;
// Returns `true` if redirecting, otherwise `false`
function redirectIfRoomAliasInHash(matrixPublicArchiveURLCreator, redirectCallback) {
function handleHashChange() {
const pageHash = window.location.hash;
const match = pageHash.match(BASIC_ROOM_ALIAS_REGEX);
if (match) {
const roomAlias = match[0];
const newLocation = matrixPublicArchiveURLCreator.archiveUrlForRoom(roomAlias);
console.log(`Saw room alias in hash, redirecting to newLocation=${newLocation}`);
window.location = newLocation;
if (redirectCallback) {
redirectCallback();
}
return true;
}
return false;
}
const eventHandler = {
handleEvent(e) {
if (e.type === 'hashchange') {
handleHashChange();
}
},
};
window.addEventListener('hashchange', eventHandler);
// Handle the initial hash
if (window.location) {
return handleHashChange();
}
return false;
}
module.exports = redirectIfRoomAliasInHash;

View File

@ -11,6 +11,7 @@ const { Platform, Navigation, createRouter } = require('hydrogen-view-sdk');
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator'); const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
const ArchiveHistory = require('matrix-public-archive-shared/lib/archive-history'); const ArchiveHistory = require('matrix-public-archive-shared/lib/archive-history');
const supressBlankAnchorsReloadingThePage = require('matrix-public-archive-shared/lib/supress-blank-anchors-reloading-the-page'); const supressBlankAnchorsReloadingThePage = require('matrix-public-archive-shared/lib/supress-blank-anchors-reloading-the-page');
const redirectIfRoomAliasInHash = require('matrix-public-archive-shared/lib/redirect-if-room-alias-in-hash');
const RoomDirectoryView = require('matrix-public-archive-shared/views/RoomDirectoryView'); const RoomDirectoryView = require('matrix-public-archive-shared/views/RoomDirectoryView');
const RoomDirectoryViewModel = require('matrix-public-archive-shared/viewmodels/RoomDirectoryViewModel'); const RoomDirectoryViewModel = require('matrix-public-archive-shared/viewmodels/RoomDirectoryViewModel');
@ -31,6 +32,15 @@ const matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(config.b
supressBlankAnchorsReloadingThePage(); supressBlankAnchorsReloadingThePage();
let roomDirectoryViewModel;
let isRedirecting = false;
isRedirecting = redirectIfRoomAliasInHash(matrixPublicArchiveURLCreator, () => {
isRedirecting = true;
if (roomDirectoryViewModel) {
roomDirectoryViewModel.setPageRedirectingFromUrlHash(true);
}
});
async function mountHydrogen() { async function mountHydrogen() {
console.log('Mounting Hydrogen...'); console.log('Mounting Hydrogen...');
console.time('Completed mounting Hydrogen'); console.time('Completed mounting Hydrogen');
@ -69,7 +79,7 @@ async function mountHydrogen() {
// page don't say `undefined`. // page don't say `undefined`.
urlRouter.attach(); urlRouter.attach();
const roomDirectoryViewModel = new RoomDirectoryViewModel({ roomDirectoryViewModel = new RoomDirectoryViewModel({
// Hydrogen options // Hydrogen options
navigation: navigation, navigation: navigation,
urlCreator: urlRouter, urlCreator: urlRouter,
@ -84,6 +94,8 @@ async function mountHydrogen() {
nextPaginationToken, nextPaginationToken,
prevPaginationToken, prevPaginationToken,
}); });
// Update the model with the initial value
roomDirectoryViewModel.setPageRedirectingFromUrlHash(isRedirecting);
const view = new RoomDirectoryView(roomDirectoryViewModel); const view = new RoomDirectoryView(roomDirectoryViewModel);

View File

@ -35,6 +35,8 @@ class RoomDirectoryViewModel extends ViewModel {
this._homeserverName = homeserverName; this._homeserverName = homeserverName;
this._matrixPublicArchiveURLCreator = matrixPublicArchiveURLCreator; this._matrixPublicArchiveURLCreator = matrixPublicArchiveURLCreator;
this._isPageRedirectingFromUrlHash = false;
this._pageSearchParameters = pageSearchParameters; this._pageSearchParameters = pageSearchParameters;
// Default to what the page started with // Default to what the page started with
this._searchTerm = pageSearchParameters.searchTerm; this._searchTerm = pageSearchParameters.searchTerm;
@ -106,6 +108,15 @@ class RoomDirectoryViewModel extends ViewModel {
this.homeserverSelectionModalViewModel.setOpen(shouldShowAddServerModal); this.homeserverSelectionModalViewModel.setOpen(shouldShowAddServerModal);
} }
setPageRedirectingFromUrlHash(newValue) {
this._isPageRedirectingFromUrlHash = newValue;
this.emitChange('isPageRedirectingFromUrlHash');
}
get isPageRedirectingFromUrlHash() {
return this._isPageRedirectingFromUrlHash;
}
get homeserverUrl() { get homeserverUrl() {
return this._homeserverUrl; return this._homeserverUrl;
} }
@ -266,7 +277,7 @@ class RoomDirectoryViewModel extends ViewModel {
const deduplicatedHomeserverList = Object.keys(deduplicatedHomeserverMap); const deduplicatedHomeserverList = Object.keys(deduplicatedHomeserverMap);
this._availableHomeserverList = deduplicatedHomeserverList; this._availableHomeserverList = deduplicatedHomeserverList;
this.emit('availableHomeserverList'); this.emitChange('availableHomeserverList');
} }
get availableHomeserverList() { get availableHomeserverList() {

View File

@ -288,6 +288,21 @@ class RoomDirectoryView extends TemplateView {
t.a({ className: 'RoomDirectoryView_paginationButton', href: vm.nextPageUrl }, 'Next'), t.a({ className: 'RoomDirectoryView_paginationButton', href: vm.nextPageUrl }, 'Next'),
]), ]),
]), ]),
t.if(
(vm) => vm.isPageRedirectingFromUrlHash,
(t /*, vm*/) => {
return t.div({ className: 'RoomDirectoryView_notificationToast', role: 'alert' }, [
t.h5(
{ className: 'RoomDirectoryView_notificationToastTitle' },
'Found room alias in URL #hash'
),
t.p(
{ className: 'RoomDirectoryView_notificationToastDescription' },
'One sec while we try to redirect you to the right place.'
),
]);
}
),
] ]
); );
} }