mirror of
https://github.com/requarks/wiki.git
synced 2026-10-12 05:54:49 -05:00
fix: view page source permission error for guests
This commit is contained in:
4 files changed
+65
-10
No files matched your search
+29
-4
@@ -127,6 +127,23 @@ export function mayOnPage(
|
||||
return WIKI.models.groups.checkAccess(WIKI.models.groups.actorForRequest(req), permission, page)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this requester may be handed a page's SOURCE.
|
||||
*
|
||||
* `read:source` is the permission that exists to say so, and a rule grants it to whoever it names —
|
||||
* the guests group included, which is how a wiki opens "view source" to the public. Whoever may write
|
||||
* the page is covered as well, since the editor loads the source in order to edit it: a rule granting
|
||||
* `write:pages` without `read:source` would otherwise be a page that cannot be edited.
|
||||
*/
|
||||
const SOURCE_PERMISSIONS = ['read:source', 'write:pages', 'manage:pages']
|
||||
|
||||
export function mayReadSource(
|
||||
req: FastifyRequest,
|
||||
page: { path: string; locale?: string; tags?: string[] }
|
||||
): boolean {
|
||||
return SOURCE_PERMISSIONS.some((permission) => mayOnPage(req, permission, page))
|
||||
}
|
||||
|
||||
/**
|
||||
* Every page permission this requester holds at a path.
|
||||
*
|
||||
@@ -604,7 +621,7 @@ async function routes(app: FastifyInstance) {
|
||||
schema: {
|
||||
summary: 'Get a single page',
|
||||
description:
|
||||
"Addressed either by ID or by the hash of its path, which is how a page view asks for one. A hash only identifies a page within a locale, so `locale` picks between translations — the site's primary one when absent.\n\nReadable without a session, because a wiki is read by people who are not logged in — but an anonymous request only ever sees published pages, and never their source. Per-page access rules are not implemented yet.\n\nA password-protected page answers with its metadata and `isLocked: true`, its body withheld, until the session satisfies `POST …/unlock` — or unless the requester may edit the page, for whom the password is not a barrier.",
|
||||
"Addressed either by ID or by the hash of its path, which is how a page view asks for one. A hash only identifies a page within a locale, so `locale` picks between translations — the site's primary one when absent.\n\nReadable without a session, because a wiki is read by people who are not logged in — but an anonymous request only ever sees published pages. `withContent` is answered against `read:source` on the page — or `write:pages`, since the editor loads the source to edit it — which a group's rules grant to whoever they name, guests included.\n\nA password-protected page answers with its metadata and `isLocked: true`, its body withheld, until the session satisfies `POST …/unlock` — or unless the requester may edit the page, for whom the password is not a barrier.",
|
||||
tags: ['Pages'],
|
||||
params: {
|
||||
type: 'object',
|
||||
@@ -626,7 +643,8 @@ async function routes(app: FastifyInstance) {
|
||||
withContent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Include the source, which only an editor needs.'
|
||||
description:
|
||||
'Include the source. Withheld from a requester who may neither read the source nor write the page here.'
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
@@ -646,8 +664,15 @@ async function routes(app: FastifyInstance) {
|
||||
siteId: req.params.siteId,
|
||||
...(isId ? { id: req.params.pageIdOrHash } : { hash: req.params.pageIdOrHash }),
|
||||
locale: req.query.locale,
|
||||
// -> The source is what an editor loads, and editing is not something an anonymous reader does
|
||||
withContent: Boolean(req.query.withContent) && Boolean(actor),
|
||||
/*
|
||||
-> The source is a page rule's to grant, not a session's to have: `mayReadSource` is the
|
||||
whole of it, and the guests group holds it wherever a rule says so. Asked as a predicate
|
||||
because the answer depends on the page, which a request addressing one by hash does not
|
||||
have in hand yet.
|
||||
*/
|
||||
withContent: req.query.withContent
|
||||
? (target: { path: string; locale: string; tags: string[] }) => mayReadSource(req, target)
|
||||
: false,
|
||||
publicOnly: !actor,
|
||||
// -> Answered once the page is known, since a hash does not say which page it is yet
|
||||
unlocked: (pageId) => unlockedFor(req, pageId),
|
||||
|
||||
+19
-2
@@ -1152,7 +1152,12 @@ class Pages {
|
||||
id?: string
|
||||
hash?: string
|
||||
locale?: string
|
||||
withContent?: boolean
|
||||
/**
|
||||
* Include the source. A predicate for a caller whose answer depends on the page — `read:source`
|
||||
* is granted by a page rule, and a rule is chosen by path, locale and tags, none of which a
|
||||
* request addressing a page by hash has in hand before the row is read.
|
||||
*/
|
||||
withContent?: boolean | ((page: { path: string; locale: string; tags: string[] }) => boolean)
|
||||
/** Restrict to what a reader with no session may see: published pages. */
|
||||
publicOnly?: boolean
|
||||
unlocked?: boolean | ((pageId: string) => boolean)
|
||||
@@ -1193,6 +1198,14 @@ class Pages {
|
||||
return null
|
||||
}
|
||||
const isUnlocked = typeof unlocked === 'function' ? unlocked(row.page.id) : unlocked
|
||||
const includeContent =
|
||||
typeof withContent === 'function'
|
||||
? withContent({
|
||||
path: row.page.path,
|
||||
locale: row.page.locale,
|
||||
tags: row.page.tags ?? []
|
||||
})
|
||||
: withContent
|
||||
return this.toPage(
|
||||
{
|
||||
...row.page,
|
||||
@@ -1206,7 +1219,11 @@ class Pages {
|
||||
publicOnly
|
||||
})
|
||||
},
|
||||
{ withContent, withPassword, locked: Boolean(row.page.password) && !isUnlocked }
|
||||
{
|
||||
withContent: includeContent,
|
||||
withPassword,
|
||||
locked: Boolean(row.page.password) && !isUnlocked
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -116,8 +116,13 @@
|
||||
@click="viewPageHistory">
|
||||
<w-tooltip anchor="center left" self="center right">Page History</w-tooltip>
|
||||
</w-btn>
|
||||
<!--
|
||||
`read:source` likewise, granted per path by a rule — so guests have it wherever a rule says so,
|
||||
and the API answers the overlay behind this button on exactly that permission.
|
||||
-->
|
||||
<w-btn
|
||||
class="h-12"
|
||||
v-if="canViewSource"
|
||||
flat
|
||||
icon="la:code"
|
||||
:color="editorStore.isActive ? `white` : `grey`"
|
||||
@@ -284,6 +289,15 @@ const isRedirect = computed(() => pageStore.editor === 'redirect')
|
||||
*/
|
||||
const hasPageActions = computed(() => flagsStore.experimental || userStore.can('write:pages'))
|
||||
|
||||
/*
|
||||
Whoever may write the page may read its source too — the editor is what opens it — so the button is
|
||||
not hidden from an author whose rule grants the one and not the other. The API decides the same way.
|
||||
*/
|
||||
const canViewSource = computed(
|
||||
() =>
|
||||
userStore.can('read:source') || userStore.can('write:pages') || userStore.can('manage:pages')
|
||||
)
|
||||
|
||||
// METHODS
|
||||
|
||||
function togglePageProperties() {
|
||||
|
||||
@@ -123,8 +123,8 @@ async function load() {
|
||||
if (!pageData?.id) {
|
||||
throw new Error(t('pageSource.notFound'))
|
||||
}
|
||||
// -> The source is withheld from a reader without a session, the field being left out entirely
|
||||
// rather than blanked — an empty string is a page that genuinely has no content
|
||||
// -> The source is withheld from a reader whose rules do not grant `read:source` here, the field
|
||||
// being left out entirely rather than blanked — an empty string is a page with no content
|
||||
if (pageData.content === undefined) {
|
||||
state.notice = t('pageSource.unavailable')
|
||||
return
|
||||
@@ -134,8 +134,7 @@ async function load() {
|
||||
// contentType was stored
|
||||
state.contentType = pageData.contentType || pageData.editor || ''
|
||||
} catch (err) {
|
||||
const message =
|
||||
err.response?.status === 404 ? t('pageSource.notFound') : apiErrorMessage(err)
|
||||
const message = err.response?.status === 404 ? t('pageSource.notFound') : apiErrorMessage(err)
|
||||
state.notice = message
|
||||
notify({
|
||||
type: 'negative',
|
||||
|
||||
Reference in new issue
Block a user