Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 237 additions & 2 deletions src/profile/profileLogic.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,237 @@
import { NamedNode } from 'rdflib'
import { literal, NamedNode, st, sym } from 'rdflib'
import { ACL_LINK } from '../acl/aclLogic'
import { CrossOriginForbiddenError, FetchError, NotEditableError, SameOriginForbiddenError, UnauthorizedError, WebOperationError } from '../logic/CustomError'
import * as debug from '../util/debug'
import { ns as namespace } from '../util/ns'
import { privateTypeIndexDocument, publicTypeIndexDocument } from '../typeIndex/typeIndexDocuments'
import { createContainerLogic } from '../util/containerLogic'
import { differentOrigin, suggestPreferencesFile } from '../util/utils'
import { ProfileLogic } from '../types'

export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
const ns = namespace
const containerLogic = createContainerLogic(store)

function isAbsoluteHttpUri(uri: string | null | undefined): boolean {
return !!uri && (uri.startsWith('https://') || uri.startsWith('http://'))
}

function docDirUri(node: NamedNode): string | null {
const doc = node.doc()
const dir = doc.dir()
if (dir?.uri && isAbsoluteHttpUri(dir.uri)) return dir.uri
const docUri = doc.uri
if (!docUri || !isAbsoluteHttpUri(docUri)) return null
const withoutFragment = docUri.split('#')[0]
const lastSlash = withoutFragment.lastIndexOf('/')
if (lastSlash === -1) return null
return withoutFragment.slice(0, lastSlash + 1)
}

function suggestTypeIndexInPreferences(preferencesFile: NamedNode, filename: string): NamedNode {
const dirUri = docDirUri(preferencesFile)
if (!dirUri) throw new Error(`Cannot derive directory for preferences file ${preferencesFile.uri}`)
return sym(dirUri + filename)
}

function isNotFoundError(err: any): boolean {
if (err?.response?.status === 404) return true
const text = `${err?.message || err || ''}`
return text.includes('404') || text.includes('Not Found')
}

function ownerOnlyContainerAcl(webId: string): string {
return [
'@prefix acl: <http://www.w3.org/ns/auth/acl#>.',
'',
'<#owner>',
'a acl:Authorization;',
`acl:agent <${webId}>;`,
'acl:accessTo <./>;',
'acl:default <./>;',
'acl:mode acl:Read, acl:Write, acl:Control.'
].join('\n')
}

function publicTypeIndexAcl(webId: string, publicTypeIndex: NamedNode): string {
const fileName = new URL(publicTypeIndex.uri).pathname.split('/').pop() || 'publicTypeIndex.ttl'
return [
'# ACL resource for the Public Type Index',
'',
'@prefix acl: <http://www.w3.org/ns/auth/acl#>.',
'@prefix foaf: <http://xmlns.com/foaf/0.1/>.',
'',
'<#owner>',
' a acl:Authorization;',
'',
' acl:agent',
` <${webId}>;`,
'',
` acl:accessTo <./${fileName}>;`,
'',
' acl:mode',
' acl:Read, acl:Write, acl:Control.',
'',
'# Public-readable',
'<#public>',
' a acl:Authorization;',
'',
' acl:agentClass foaf:Agent;',
'',
` acl:accessTo <./${fileName}>;`,
'',
' acl:mode acl:Read.'
].join('\n')
}

async function ensureContainerExists(containerUri: string): Promise<void> {
const containerNode = sym(containerUri)
try {
await store.fetcher.load(containerNode)
return
} catch (err) {
if (!isNotFoundError(err)) throw err
}
await containerLogic.createContainer(containerUri)
}

async function ensureOwnerOnlyAclForSettings(user: NamedNode, preferencesFile: NamedNode): Promise<void> {
const dirUri = docDirUri(preferencesFile)
if (!dirUri) throw new Error(`Cannot derive settings directory from ${preferencesFile.uri}`)
await ensureContainerExists(dirUri)

const containerNode = sym(dirUri)
let aclDocUri: string | undefined
try {
await store.fetcher.load(containerNode)
aclDocUri = store.any(containerNode, ACL_LINK)?.value
} catch (err) {
if (!isNotFoundError(err)) throw err
}
if (!aclDocUri) {
// Fallback for servers/tests where rel=acl is not exposed in mocked headers.
aclDocUri = `${dirUri}.acl`
}
const aclDoc = sym(aclDocUri)
try {
await store.fetcher.load(aclDoc)
return
} catch (err) {
if (!isNotFoundError(err)) throw err
}

await store.fetcher.webOperation('PUT', aclDoc.uri, {
data: ownerOnlyContainerAcl(user.uri),
contentType: 'text/turtle'
})
}

async function ensurePublicTypeIndexAclOnCreate(user: NamedNode, publicTypeIndex: NamedNode, ensureAcl = false): Promise<void> {
const created = await utilityLogic.loadOrCreateWithContentOnCreate(publicTypeIndex, publicTypeIndexDocument())
if (!created && !ensureAcl) return

let aclDocUri: string | undefined
try {
await store.fetcher.load(publicTypeIndex)
aclDocUri = store.any(publicTypeIndex, ACL_LINK)?.value
} catch (err) {
if (!isNotFoundError(err)) throw err
}
if (!aclDocUri) {
aclDocUri = `${publicTypeIndex.uri}.acl`
}

const aclDoc = sym(aclDocUri)
try {
await store.fetcher.load(aclDoc)
return
} catch (err) {
if (!isNotFoundError(err)) throw err
}

await store.fetcher.webOperation('PUT', aclDoc.uri, {
data: publicTypeIndexAcl(user.uri, publicTypeIndex),
contentType: 'text/turtle'
})
}

async function ensurePrivateTypeIndexOnCreate(privateTypeIndex: NamedNode): Promise<void> {
await utilityLogic.loadOrCreateWithContentOnCreate(privateTypeIndex, privateTypeIndexDocument())
}

async function initializePreferencesDefaults(user: NamedNode, preferencesFile: NamedNode): Promise<void> {
const preferencesDoc = preferencesFile.doc() as NamedNode
const profileDoc = user.doc() as NamedNode
await store.fetcher.load(preferencesDoc)

const profilePublicTypeIndex =
(store.any(user, ns.solid('publicTypeIndex'), null, profileDoc) as NamedNode | null)
const preferencesPublicTypeIndex =
(store.any(user, ns.solid('publicTypeIndex'), null, preferencesDoc) as NamedNode | null)
const publicTypeIndex =
profilePublicTypeIndex ||
preferencesPublicTypeIndex ||
suggestTypeIndexInPreferences(preferencesFile, 'publicTypeIndex.ttl')
const privateTypeIndex =
(store.any(user, ns.solid('privateTypeIndex'), null, preferencesDoc) as NamedNode | null) ||
suggestTypeIndexInPreferences(preferencesFile, 'privateTypeIndex.ttl')

// Keep discovery consistent with typeIndexLogic, which resolves publicTypeIndex from the profile doc.
const createdProfilePublicTypeIndexLink = !profilePublicTypeIndex
if (createdProfilePublicTypeIndexLink) {
await utilityLogic.followOrCreateLinkWithContentOnCreate(
user,
ns.solid('publicTypeIndex') as NamedNode,
publicTypeIndex,
profileDoc,
publicTypeIndexDocument()
)
}

const toInsert: any[] = []
if (!store.holds(preferencesDoc, ns.rdf('type'), ns.space('ConfigurationFile'), preferencesDoc)) {
toInsert.push(st(preferencesDoc, ns.rdf('type'), ns.space('ConfigurationFile'), preferencesDoc))
}
if (!store.holds(preferencesDoc, ns.dct('title'), undefined, preferencesDoc)) {
toInsert.push(st(preferencesDoc, ns.dct('title'), literal('Preferences file'), preferencesDoc))
}
if (!store.holds(user, ns.solid('publicTypeIndex'), publicTypeIndex, preferencesDoc)) {
toInsert.push(st(user, ns.solid('publicTypeIndex'), publicTypeIndex, preferencesDoc))
}
if (!store.holds(user, ns.solid('privateTypeIndex'), privateTypeIndex, preferencesDoc)) {
toInsert.push(st(user, ns.solid('privateTypeIndex'), privateTypeIndex, preferencesDoc))
}

if (toInsert.length > 0) {
await store.updater.update([], toInsert)
await store.fetcher.load(preferencesDoc)
}

await ensurePublicTypeIndexAclOnCreate(user, publicTypeIndex, createdProfilePublicTypeIndexLink)
await ensurePrivateTypeIndexOnCreate(privateTypeIndex)
}

async function ensurePreferencesDocExists(preferencesFile: NamedNode): Promise<boolean> {
try {
await store.fetcher.load(preferencesFile)
return false
} catch (err) {
if (isNotFoundError(err)) {
await utilityLogic.loadOrCreateIfNotExists(preferencesFile)
return true
}
if (err.response?.status === 401) {
throw new UnauthorizedError()
}
if (err.response?.status === 403) {
if (differentOrigin(preferencesFile)) {
throw new CrossOriginForbiddenError()
}
throw new SameOriginForbiddenError()
}
throw err
}
}

/**
* loads the preference without throwing errors - if it can create it it does so.
Expand Down Expand Up @@ -34,7 +259,17 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
const possiblePreferencesFile = suggestPreferencesFile(user)
let preferencesFile
try {
preferencesFile = await utilityLogic.followOrCreateLink(user, ns.space('preferencesFile') as NamedNode, possiblePreferencesFile, user.doc())
const existingPreferencesFile = store.any(user, ns.space('preferencesFile'), null, user.doc()) as NamedNode | null
if (existingPreferencesFile) {
preferencesFile = existingPreferencesFile
} else {
preferencesFile = await utilityLogic.followOrCreateLink(user, ns.space('preferencesFile') as NamedNode, possiblePreferencesFile, user.doc())
}

await ensureOwnerOnlyAclForSettings(user, preferencesFile as NamedNode)

await ensurePreferencesDocExists(preferencesFile as NamedNode)
await initializePreferencesDefaults(user, preferencesFile as NamedNode)
} catch (err) {
const message = `User ${user} has no pointer in profile to preferences file.`
debug.warn(message)
Expand Down
17 changes: 17 additions & 0 deletions src/typeIndex/typeIndexDocuments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function publicTypeIndexDocument(): string {
return [
'@prefix solid: <http://www.w3.org/ns/solid/terms#>.',
'<>',
' a solid:TypeIndex ;',
' a solid:ListedDocument.'
].join('\n')
}

export function privateTypeIndexDocument(): string {
return [
'@prefix solid: <http://www.w3.org/ns/solid/terms#>.',
'<>',
' a solid:TypeIndex ;',
' a solid:UnlistedDocument.'
].join('\n')
}
40 changes: 30 additions & 10 deletions src/typeIndex/typeIndexLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ScopedApp, TypeIndexLogic, TypeIndexScope } from '../types'
import * as debug from '../util/debug'
import { ns as namespace } from '../util/ns'
import { newThing } from '../util/utils'
import { privateTypeIndexDocument, publicTypeIndexDocument } from './typeIndexDocuments'

export function createTypeIndexLogic(store, authn, profileLogic, utilityLogic): TypeIndexLogic {
const ns = namespace
Expand Down Expand Up @@ -32,11 +33,20 @@ export function createTypeIndexLogic(store, authn, profileLogic, utilityLogic):
}
let publicTypeIndex
try {
publicTypeIndex =
store.any(user, ns.solid('publicTypeIndex'), undefined, profile) ||
(suggestion
? await utilityLogic.followOrCreateLink(user, ns.solid('publicTypeIndex') as NamedNode, suggestion, profile)
: null)
const existingPublicTypeIndex = store.any(user, ns.solid('publicTypeIndex'), undefined, profile)
if (existingPublicTypeIndex) {
publicTypeIndex = existingPublicTypeIndex
} else if (suggestion) {
publicTypeIndex = await utilityLogic.followOrCreateLinkWithContentOnCreate(
user,
ns.solid('publicTypeIndex') as NamedNode,
suggestion,
profile,
publicTypeIndexDocument()
)
} else {
publicTypeIndex = null
}
} catch (err) {
const message = `User ${user} has no pointer in profile to publicTypeIndex file: ${err}`
debug.warn(message)
Expand All @@ -63,11 +73,21 @@ export function createTypeIndexLogic(store, authn, profileLogic, utilityLogic):
}
let privateTypeIndex
try {
privateTypeIndex = store.any(user, ns.solid('privateTypeIndex'), undefined, profile) ||
(suggestedPrivateTypeIndex
? await utilityLogic.followOrCreateLink(user, ns.solid('privateTypeIndex') as NamedNode, suggestedPrivateTypeIndex, preferencesFile)
: null)
} catch (err) {
const existingPrivateTypeIndex = store.any(user, ns.solid('privateTypeIndex'), undefined, profile)
if (existingPrivateTypeIndex) {
privateTypeIndex = existingPrivateTypeIndex
} else if (suggestedPrivateTypeIndex) {
privateTypeIndex = await utilityLogic.followOrCreateLinkWithContentOnCreate(
user,
ns.solid('privateTypeIndex') as NamedNode,
suggestedPrivateTypeIndex,
preferencesFile,
privateTypeIndexDocument()
)
} else {
privateTypeIndex = null
}
} catch (err) {
const message = `User ${user} has no pointer in preference file to privateTypeIndex file: ${err}`
debug.warn(message)
}
Expand Down
Loading
Loading