solid-server 5.6.9-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.acl +10 -0
- package/.github/workflows/ci.yml +47 -0
- package/.nvmrc +1 -0
- package/.snyk +35 -0
- package/.well-known/.acl +15 -0
- package/CHANGELOG.md +198 -0
- package/CONTRIBUTING.md +139 -0
- package/CONTRIBUTORS.md +36 -0
- package/Dockerfile +22 -0
- package/LICENSE.md +23 -0
- package/README.md +453 -0
- package/bin/lib/cli-utils.js +85 -0
- package/bin/lib/cli.js +39 -0
- package/bin/lib/init.js +94 -0
- package/bin/lib/invalidUsernames.js +148 -0
- package/bin/lib/migrateLegacyResources.js +69 -0
- package/bin/lib/options.js +399 -0
- package/bin/lib/start.js +148 -0
- package/bin/lib/updateIndex.js +56 -0
- package/bin/solid +3 -0
- package/bin/solid-test +12 -0
- package/bin/solid.js +3 -0
- package/common/css/solid.css +58 -0
- package/common/fonts/glyphicons-halflings-regular.eot +0 -0
- package/common/fonts/glyphicons-halflings-regular.svg +288 -0
- package/common/fonts/glyphicons-halflings-regular.ttf +0 -0
- package/common/fonts/glyphicons-halflings-regular.woff +0 -0
- package/common/fonts/glyphicons-halflings-regular.woff2 +0 -0
- package/common/img/.gitkeep +0 -0
- package/common/js/auth-buttons.js +65 -0
- package/common/js/solid.js +454 -0
- package/common/well-known/security.txt +2 -0
- package/config/defaults.js +25 -0
- package/config/usernames-blacklist.json +4 -0
- package/config.json-default +22 -0
- package/default-templates/emails/delete-account.js +49 -0
- package/default-templates/emails/invalid-username.js +30 -0
- package/default-templates/emails/reset-password.js +49 -0
- package/default-templates/emails/welcome.js +39 -0
- package/default-templates/new-account/.acl +26 -0
- package/default-templates/new-account/.meta +5 -0
- package/default-templates/new-account/.meta.acl +25 -0
- package/default-templates/new-account/.well-known/.acl +19 -0
- package/default-templates/new-account/favicon.ico +0 -0
- package/default-templates/new-account/favicon.ico.acl +26 -0
- package/default-templates/new-account/inbox/.acl +26 -0
- package/default-templates/new-account/private/.acl +10 -0
- package/default-templates/new-account/profile/.acl +19 -0
- package/default-templates/new-account/profile/card$.ttl +25 -0
- package/default-templates/new-account/public/.acl +19 -0
- package/default-templates/new-account/robots.txt +3 -0
- package/default-templates/new-account/robots.txt.acl +26 -0
- package/default-templates/new-account/settings/.acl +20 -0
- package/default-templates/new-account/settings/prefs.ttl +15 -0
- package/default-templates/new-account/settings/privateTypeIndex.ttl +4 -0
- package/default-templates/new-account/settings/publicTypeIndex.ttl +4 -0
- package/default-templates/new-account/settings/publicTypeIndex.ttl.acl +25 -0
- package/default-templates/new-account/settings/serverSide.ttl.acl +13 -0
- package/default-templates/new-account/settings/serverSide.ttl.inactive +12 -0
- package/default-templates/server/.acl +10 -0
- package/default-templates/server/.well-known/.acl +15 -0
- package/default-templates/server/favicon.ico +0 -0
- package/default-templates/server/favicon.ico.acl +15 -0
- package/default-templates/server/index.html +55 -0
- package/default-templates/server/robots.txt +3 -0
- package/default-templates/server/robots.txt.acl +15 -0
- package/default-views/account/account-deleted.hbs +17 -0
- package/default-views/account/delete-confirm.hbs +51 -0
- package/default-views/account/delete-link-sent.hbs +17 -0
- package/default-views/account/delete.hbs +51 -0
- package/default-views/account/invalid-username.hbs +22 -0
- package/default-views/account/register-disabled.hbs +6 -0
- package/default-views/account/register-form.hbs +132 -0
- package/default-views/account/register.hbs +24 -0
- package/default-views/auth/auth-hidden-fields.hbs +8 -0
- package/default-views/auth/change-password.hbs +58 -0
- package/default-views/auth/goodbye.hbs +23 -0
- package/default-views/auth/login-required.hbs +34 -0
- package/default-views/auth/login-tls.hbs +11 -0
- package/default-views/auth/login-username-password.hbs +28 -0
- package/default-views/auth/login.hbs +55 -0
- package/default-views/auth/no-permission.hbs +29 -0
- package/default-views/auth/password-changed.hbs +27 -0
- package/default-views/auth/reset-link-sent.hbs +21 -0
- package/default-views/auth/reset-password.hbs +52 -0
- package/default-views/auth/sharing.hbs +49 -0
- package/default-views/shared/create-account.hbs +8 -0
- package/default-views/shared/error.hbs +5 -0
- package/docs/how-to-delete-your-account.md +56 -0
- package/docs/login-and-grant-access-to-application.md +32 -0
- package/examples/custom-error-handling.js +31 -0
- package/examples/ldp-with-webid.js +12 -0
- package/examples/simple-express-app.js +20 -0
- package/examples/simple-ldp-server.js +8 -0
- package/favicon.ico +0 -0
- package/favicon.ico.acl +15 -0
- package/index.html +48 -0
- package/index.js +3 -0
- package/lib/acl-checker.js +274 -0
- package/lib/api/accounts/user-accounts.js +88 -0
- package/lib/api/authn/force-user.js +21 -0
- package/lib/api/authn/index.js +5 -0
- package/lib/api/authn/webid-oidc.js +202 -0
- package/lib/api/authn/webid-tls.js +69 -0
- package/lib/api/index.js +6 -0
- package/lib/capability-discovery.js +54 -0
- package/lib/common/fs-utils.js +43 -0
- package/lib/common/template-utils.js +50 -0
- package/lib/common/user-utils.js +28 -0
- package/lib/create-app.js +322 -0
- package/lib/create-server.js +107 -0
- package/lib/debug.js +17 -0
- package/lib/handlers/allow.js +82 -0
- package/lib/handlers/auth-proxy.js +63 -0
- package/lib/handlers/copy.js +39 -0
- package/lib/handlers/cors-proxy.js +95 -0
- package/lib/handlers/delete.js +23 -0
- package/lib/handlers/error-pages.js +212 -0
- package/lib/handlers/get.js +219 -0
- package/lib/handlers/index.js +42 -0
- package/lib/handlers/options.js +33 -0
- package/lib/handlers/patch/n3-patch-parser.js +49 -0
- package/lib/handlers/patch/sparql-update-parser.js +16 -0
- package/lib/handlers/patch.js +203 -0
- package/lib/handlers/post.js +99 -0
- package/lib/handlers/put.js +56 -0
- package/lib/handlers/restrict-to-top-domain.js +13 -0
- package/lib/header.js +136 -0
- package/lib/http-error.js +34 -0
- package/lib/ldp-container.js +161 -0
- package/lib/ldp-copy.js +73 -0
- package/lib/ldp-middleware.js +32 -0
- package/lib/ldp.js +620 -0
- package/lib/lock.js +10 -0
- package/lib/metadata.js +10 -0
- package/lib/models/account-manager.js +603 -0
- package/lib/models/account-template.js +152 -0
- package/lib/models/authenticator.js +333 -0
- package/lib/models/oidc-manager.js +53 -0
- package/lib/models/solid-host.js +131 -0
- package/lib/models/user-account.js +112 -0
- package/lib/models/webid-tls-certificate.js +184 -0
- package/lib/payment-pointer-discovery.js +83 -0
- package/lib/requests/add-cert-request.js +138 -0
- package/lib/requests/auth-request.js +234 -0
- package/lib/requests/create-account-request.js +468 -0
- package/lib/requests/delete-account-confirm-request.js +170 -0
- package/lib/requests/delete-account-request.js +144 -0
- package/lib/requests/login-request.js +205 -0
- package/lib/requests/password-change-request.js +201 -0
- package/lib/requests/password-reset-email-request.js +199 -0
- package/lib/requests/sharing-request.js +259 -0
- package/lib/resource-mapper.js +198 -0
- package/lib/server-config.js +167 -0
- package/lib/services/blacklist-service.js +33 -0
- package/lib/services/email-service.js +162 -0
- package/lib/services/token-service.js +47 -0
- package/lib/utils.js +254 -0
- package/lib/webid/index.js +13 -0
- package/lib/webid/lib/get.js +27 -0
- package/lib/webid/lib/parse.js +12 -0
- package/lib/webid/tls/index.js +185 -0
- package/package.json +172 -0
- package/renovate.json +5 -0
- package/robots.txt +3 -0
- package/robots.txt.acl +15 -0
- package/static/account-recovery.html +78 -0
- package/static/popup-redirect.html +1 -0
- package/static/signup.html +108 -0
- package/static/signup.html.acl +14 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Express handler for LDP PATCH requests
|
|
2
|
+
|
|
3
|
+
module.exports = handler
|
|
4
|
+
|
|
5
|
+
const bodyParser = require('body-parser')
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const debug = require('../debug').handlers
|
|
8
|
+
const error = require('../http-error')
|
|
9
|
+
const $rdf = require('rdflib')
|
|
10
|
+
const crypto = require('crypto')
|
|
11
|
+
const { overQuota, getContentType } = require('../utils')
|
|
12
|
+
const withLock = require('../lock')
|
|
13
|
+
|
|
14
|
+
// Patch parsers by request body content type
|
|
15
|
+
const PATCH_PARSERS = {
|
|
16
|
+
'application/sparql-update': require('./patch/sparql-update-parser.js'),
|
|
17
|
+
'application/sparql-update-single-match': require('./patch/sparql-update-parser.js'),
|
|
18
|
+
'text/n3': require('./patch/n3-patch-parser.js')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_FOR_NEW_CONTENT_TYPE = 'text/turtle'
|
|
22
|
+
|
|
23
|
+
// Handles a PATCH request
|
|
24
|
+
async function patchHandler (req, res, next) {
|
|
25
|
+
debug(`PATCH -- ${req.originalUrl}`)
|
|
26
|
+
res.header('MS-Author-Via', 'SPARQL')
|
|
27
|
+
try {
|
|
28
|
+
// Obtain details of the target resource
|
|
29
|
+
const ldp = req.app.locals.ldp
|
|
30
|
+
let path, contentType
|
|
31
|
+
let resourceExists = true
|
|
32
|
+
try {
|
|
33
|
+
// First check if the file already exists
|
|
34
|
+
({ path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req }))
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// If the file doesn't exist, request one to be created with the default content type
|
|
37
|
+
({ path, contentType } = await ldp.resourceMapper.mapUrlToFile(
|
|
38
|
+
{ url: req, createIfNotExists: true, contentType: DEFAULT_FOR_NEW_CONTENT_TYPE }))
|
|
39
|
+
// check if a folder with same name exists
|
|
40
|
+
await ldp.checkItemName(req)
|
|
41
|
+
resourceExists = false
|
|
42
|
+
}
|
|
43
|
+
const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname })
|
|
44
|
+
const resource = { path, contentType, url }
|
|
45
|
+
debug('PATCH -- Target <%s> (%s)', url, contentType)
|
|
46
|
+
|
|
47
|
+
// Obtain details of the patch document
|
|
48
|
+
const patch = {}
|
|
49
|
+
patch.text = req.body ? req.body.toString() : ''
|
|
50
|
+
patch.uri = `${url}#patch-${hash(patch.text)}`
|
|
51
|
+
patch.contentType = getContentType(req.headers)
|
|
52
|
+
debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType)
|
|
53
|
+
const parsePatch = PATCH_PARSERS[patch.contentType]
|
|
54
|
+
if (!parsePatch) {
|
|
55
|
+
throw error(415, `Unsupported patch content type: ${patch.contentType}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Parse the patch document and verify permissions
|
|
59
|
+
const patchObject = await parsePatch(url, patch.uri, patch.text)
|
|
60
|
+
await checkPermission(req, patchObject, resourceExists)
|
|
61
|
+
|
|
62
|
+
// Create the enclosing directory, if necessary
|
|
63
|
+
await ldp.createDirectory(path, req.hostname)
|
|
64
|
+
|
|
65
|
+
// Patch the graph and write it back to the file
|
|
66
|
+
const result = await withLock(path, async () => {
|
|
67
|
+
const graph = await readGraph(resource)
|
|
68
|
+
await applyPatch(patchObject, graph, url)
|
|
69
|
+
return writeGraph(graph, resource, ldp.resourceMapper.resolveFilePath(req.hostname), ldp.serverUri)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Send the result to the client
|
|
73
|
+
res.send(result)
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return next(err)
|
|
76
|
+
}
|
|
77
|
+
return next()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Reads the request body and calls the actual patch handler
|
|
81
|
+
function handler (req, res, next) {
|
|
82
|
+
readEntity(req, res, () => patchHandler(req, res, next))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const readEntity = bodyParser.text({ type: () => true })
|
|
86
|
+
|
|
87
|
+
// Reads the RDF graph in the given resource
|
|
88
|
+
function readGraph (resource) {
|
|
89
|
+
// Read the resource's file
|
|
90
|
+
return new Promise((resolve, reject) =>
|
|
91
|
+
fs.readFile(resource.path, { encoding: 'utf8' }, function (err, fileContents) {
|
|
92
|
+
if (err) {
|
|
93
|
+
// If the file does not exist, assume empty contents
|
|
94
|
+
// (it will be created after a successful patch)
|
|
95
|
+
if (err.code === 'ENOENT') {
|
|
96
|
+
fileContents = ''
|
|
97
|
+
// Fail on all other errors
|
|
98
|
+
} else {
|
|
99
|
+
return reject(error(500, `Original file read error: ${err}`))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
debug('PATCH -- Read target file (%d bytes)', fileContents.length)
|
|
103
|
+
resolve(fileContents)
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
// Parse the resource's file contents
|
|
107
|
+
.then((fileContents) => {
|
|
108
|
+
const graph = $rdf.graph()
|
|
109
|
+
debug('PATCH -- Reading %s with content type %s', resource.url, resource.contentType)
|
|
110
|
+
try {
|
|
111
|
+
$rdf.parse(fileContents, graph, resource.url, resource.contentType)
|
|
112
|
+
} catch (err) {
|
|
113
|
+
throw error(500, `Patch: Target ${resource.contentType} file syntax error: ${err}`)
|
|
114
|
+
}
|
|
115
|
+
debug('PATCH -- Parsed target file')
|
|
116
|
+
return graph
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Verifies whether the user is allowed to perform the patch on the target
|
|
121
|
+
async function checkPermission (request, patchObject, resourceExists) {
|
|
122
|
+
// If no ACL object was passed down, assume permissions are okay.
|
|
123
|
+
if (!request.acl) return Promise.resolve(patchObject)
|
|
124
|
+
// At this point, we already assume append access,
|
|
125
|
+
// as this can be checked upfront before parsing the patch.
|
|
126
|
+
// Now that we know the details of the patch,
|
|
127
|
+
// we might need to perform additional checks.
|
|
128
|
+
let modes = []
|
|
129
|
+
// acl:default Write is required for create
|
|
130
|
+
if (!resourceExists) modes = ['Write']
|
|
131
|
+
const { acl, session: { userId } } = request
|
|
132
|
+
// Read access is required for DELETE and WHERE.
|
|
133
|
+
// If we would allows users without read access,
|
|
134
|
+
// they could use DELETE or WHERE to trigger 200 or 409,
|
|
135
|
+
// and thereby guess the existence of certain triples.
|
|
136
|
+
// DELETE additionally requires write access.
|
|
137
|
+
if (patchObject.delete) {
|
|
138
|
+
// ACTUALLY Read not needed by solid/test-suite only Write
|
|
139
|
+
modes = ['Read', 'Write']
|
|
140
|
+
// checks = [acl.can(userId, 'Read'), acl.can(userId, 'Write')]
|
|
141
|
+
} else if (patchObject.where) {
|
|
142
|
+
modes = modes.concat(['Read'])
|
|
143
|
+
// checks = [acl.can(userId, 'Read')]
|
|
144
|
+
}
|
|
145
|
+
const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode, request.method, resourceExists)))
|
|
146
|
+
const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true)
|
|
147
|
+
if (!allAllowed) {
|
|
148
|
+
// check owner with Control
|
|
149
|
+
const ldp = request.app.locals.ldp
|
|
150
|
+
if (request.path.endsWith('.acl') && userId === await ldp.getOwner(request.hostname)) return Promise.resolve(patchObject)
|
|
151
|
+
|
|
152
|
+
const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode)))
|
|
153
|
+
const error = errors.filter(error => !!error)
|
|
154
|
+
.reduce((prevErr, err) => prevErr.status > err.status ? prevErr : err, { status: 0 })
|
|
155
|
+
return Promise.reject(error)
|
|
156
|
+
}
|
|
157
|
+
return Promise.resolve(patchObject)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Applies the patch to the RDF graph
|
|
161
|
+
function applyPatch (patchObject, graph, url) {
|
|
162
|
+
debug('PATCH -- Applying patch')
|
|
163
|
+
return new Promise((resolve, reject) =>
|
|
164
|
+
graph.applyPatch(patchObject, graph.sym(url), (err) => {
|
|
165
|
+
if (err) {
|
|
166
|
+
const message = err.message || err // returns string at the moment
|
|
167
|
+
debug(`PATCH -- FAILED. Returning 409. Message: '${message}'`)
|
|
168
|
+
return reject(error(409, `The patch could not be applied. ${message}`))
|
|
169
|
+
}
|
|
170
|
+
resolve(graph)
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Writes the RDF graph to the given resource
|
|
176
|
+
function writeGraph (graph, resource, root, serverUri) {
|
|
177
|
+
debug('PATCH -- Writing patched file')
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const resourceSym = graph.sym(resource.url)
|
|
180
|
+
const serialized = $rdf.serialize(resourceSym, graph, resource.url, resource.contentType)
|
|
181
|
+
|
|
182
|
+
// First check if we are above quota
|
|
183
|
+
overQuota(root, serverUri).then((isOverQuota) => {
|
|
184
|
+
if (isOverQuota) {
|
|
185
|
+
return reject(error(413,
|
|
186
|
+
'User has exceeded their storage quota'))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fs.writeFile(resource.path, serialized, { encoding: 'utf8' }, function (err) {
|
|
190
|
+
if (err) {
|
|
191
|
+
return reject(error(500, `Failed to write file after patch: ${err}`))
|
|
192
|
+
}
|
|
193
|
+
debug('PATCH -- applied successfully')
|
|
194
|
+
resolve('Patch applied successfully.\n')
|
|
195
|
+
})
|
|
196
|
+
}).catch(() => reject(error(500, 'Error finding user quota')))
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Creates a hash of the given text
|
|
201
|
+
function hash (text) {
|
|
202
|
+
return crypto.createHash('md5').update(text).digest('hex')
|
|
203
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module.exports = handler
|
|
2
|
+
|
|
3
|
+
const Busboy = require('busboy')
|
|
4
|
+
const debug = require('debug')('solid:post')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const header = require('../header')
|
|
7
|
+
const patch = require('./patch')
|
|
8
|
+
const error = require('../http-error')
|
|
9
|
+
const { extensions } = require('mime-types')
|
|
10
|
+
const getContentType = require('../utils').getContentType
|
|
11
|
+
|
|
12
|
+
async function handler (req, res, next) {
|
|
13
|
+
const ldp = req.app.locals.ldp
|
|
14
|
+
const contentType = getContentType(req.headers)
|
|
15
|
+
debug('content-type is ', contentType)
|
|
16
|
+
// Handle SPARQL(-update?) query
|
|
17
|
+
if (contentType === 'application/sparql' ||
|
|
18
|
+
contentType === 'application/sparql-update') {
|
|
19
|
+
debug('switching to sparql query')
|
|
20
|
+
return patch(req, res, next)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle container path
|
|
24
|
+
let containerPath = req.path
|
|
25
|
+
if (containerPath[containerPath.length - 1] !== '/') {
|
|
26
|
+
containerPath += '/'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if container exists
|
|
30
|
+
let stats
|
|
31
|
+
try {
|
|
32
|
+
const ret = await ldp.exists(req.hostname, containerPath, false)
|
|
33
|
+
if (ret) {
|
|
34
|
+
stats = ret.stream
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return next(error(err, 'Container not valid'))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if container is a directory
|
|
41
|
+
if (stats && !stats.isDirectory()) {
|
|
42
|
+
debug('Path is not a container, 405!')
|
|
43
|
+
return next(error(405, 'Requested resource is not a container'))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Dispatch to the right handler
|
|
47
|
+
if (req.is('multipart/form-data')) {
|
|
48
|
+
multi()
|
|
49
|
+
} else {
|
|
50
|
+
one()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function multi () {
|
|
54
|
+
debug('receving multiple files')
|
|
55
|
+
|
|
56
|
+
const busboy = new Busboy({ headers: req.headers })
|
|
57
|
+
busboy.on('file', async function (fieldname, file, filename, encoding, mimetype) {
|
|
58
|
+
debug('One file received via multipart: ' + filename)
|
|
59
|
+
const { url: putUrl } = await ldp.resourceMapper.mapFileToUrl(
|
|
60
|
+
{ path: ldp.resourceMapper._rootPath + path.join(containerPath, filename), hostname: req.hostname })
|
|
61
|
+
try {
|
|
62
|
+
await ldp.put(putUrl, file, mimetype)
|
|
63
|
+
} catch (err) {
|
|
64
|
+
busboy.emit('error', err)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
busboy.on('error', function (err) {
|
|
68
|
+
debug('Error receiving the file: ' + err.message)
|
|
69
|
+
next(error(500, 'Error receiving the file'))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Handled by backpressure of streams!
|
|
73
|
+
busboy.on('finish', function () {
|
|
74
|
+
debug('Done storing files')
|
|
75
|
+
res.sendStatus(200)
|
|
76
|
+
next()
|
|
77
|
+
})
|
|
78
|
+
req.pipe(busboy)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function one () {
|
|
82
|
+
debug('Receving one file')
|
|
83
|
+
const { slug, link, 'content-type': contentType } = req.headers
|
|
84
|
+
const links = header.parseMetadataFromHeader(link)
|
|
85
|
+
const mimeType = contentType ? contentType.replace(/\s*;.*/, '') : ''
|
|
86
|
+
const extension = mimeType in extensions ? `.${extensions[mimeType][0]}` : ''
|
|
87
|
+
|
|
88
|
+
ldp.post(req.hostname, containerPath, req,
|
|
89
|
+
{ slug, extension, container: links.isBasicContainer, contentType }).then(
|
|
90
|
+
resourcePath => {
|
|
91
|
+
debug('File stored in ' + resourcePath)
|
|
92
|
+
header.addLinks(res, links)
|
|
93
|
+
res.set('Location', resourcePath)
|
|
94
|
+
res.sendStatus(201)
|
|
95
|
+
next()
|
|
96
|
+
},
|
|
97
|
+
err => next(err))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module.exports = handler
|
|
2
|
+
|
|
3
|
+
const bodyParser = require('body-parser')
|
|
4
|
+
const debug = require('debug')('solid:put')
|
|
5
|
+
const getContentType = require('../utils').getContentType
|
|
6
|
+
const HTTPError = require('../http-error')
|
|
7
|
+
const { stringToStream } = require('../utils')
|
|
8
|
+
|
|
9
|
+
async function handler (req, res, next) {
|
|
10
|
+
debug(req.originalUrl)
|
|
11
|
+
res.header('MS-Author-Via', 'SPARQL')
|
|
12
|
+
|
|
13
|
+
const contentType = req.get('content-type')
|
|
14
|
+
if (isAuxiliary(req)) {
|
|
15
|
+
if (contentType === 'text/turtle') {
|
|
16
|
+
return bodyParser.text({ type: () => true })(req, res, () => putAuxiliary(req, res, next))
|
|
17
|
+
} else return next(new HTTPError(415, 'RDF file contains invalid syntax'))
|
|
18
|
+
}
|
|
19
|
+
return putStream(req, res, next)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// TODO could be renamed as putResource (it now covers container and non-container)
|
|
23
|
+
async function putStream (req, res, next, stream = req) {
|
|
24
|
+
const ldp = req.app.locals.ldp
|
|
25
|
+
try {
|
|
26
|
+
debug('test ' + req.get('content-type') + getContentType(req.headers))
|
|
27
|
+
await ldp.put(req, stream, getContentType(req.headers))
|
|
28
|
+
debug('succeded putting the file/folder')
|
|
29
|
+
res.sendStatus(201)
|
|
30
|
+
return next()
|
|
31
|
+
} catch (err) {
|
|
32
|
+
debug('error putting the file/folder:' + err.message)
|
|
33
|
+
err.message = 'Can\'t write file/folder: ' + err.message
|
|
34
|
+
return next(err)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// needed to avoid breaking access with bad acl
|
|
39
|
+
// or breaking containement triples for meta
|
|
40
|
+
function putAuxiliary (req, res, next) {
|
|
41
|
+
const ldp = req.app.locals.ldp
|
|
42
|
+
const contentType = req.get('content-type')
|
|
43
|
+
const requestUri = ldp.resourceMapper.getRequestUrl(req)
|
|
44
|
+
|
|
45
|
+
if (ldp.isValidRdf(req.body, requestUri, contentType)) {
|
|
46
|
+
const stream = stringToStream(req.body)
|
|
47
|
+
return putStream(req, res, next, stream)
|
|
48
|
+
}
|
|
49
|
+
next(new HTTPError(400, 'RDF file contains invalid syntax'))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isAuxiliary (req) {
|
|
53
|
+
const originalUrlParts = req.originalUrl.split('.')
|
|
54
|
+
const ext = originalUrlParts[originalUrlParts.length - 1]
|
|
55
|
+
return (ext === 'acl' || ext === 'meta')
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const HTTPError = require('../http-error')
|
|
2
|
+
|
|
3
|
+
module.exports = function (req, res, next) {
|
|
4
|
+
const locals = req.app.locals
|
|
5
|
+
const ldp = locals.ldp
|
|
6
|
+
const serverUri = locals.host.serverUri
|
|
7
|
+
const hostname = ldp.resourceMapper.resolveUrl(req.hostname)
|
|
8
|
+
if (hostname === serverUri) {
|
|
9
|
+
return next()
|
|
10
|
+
}
|
|
11
|
+
const isLoggedIn = !!(req.session && req.session.userId)
|
|
12
|
+
return next(new HTTPError(isLoggedIn ? 403 : 401, 'Not allowed to access top-level APIs on accounts'))
|
|
13
|
+
}
|
package/lib/header.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module.exports.addLink = addLink
|
|
2
|
+
module.exports.addLinks = addLinks
|
|
3
|
+
module.exports.parseMetadataFromHeader = parseMetadataFromHeader
|
|
4
|
+
module.exports.linksHandler = linksHandler
|
|
5
|
+
module.exports.addPermissions = addPermissions
|
|
6
|
+
|
|
7
|
+
const li = require('li')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const metadata = require('./metadata.js')
|
|
10
|
+
const debug = require('./debug.js')
|
|
11
|
+
const utils = require('./utils.js')
|
|
12
|
+
const error = require('./http-error')
|
|
13
|
+
|
|
14
|
+
const MODES = ['Read', 'Write', 'Append', 'Control']
|
|
15
|
+
const PERMISSIONS = MODES.map(m => m.toLowerCase())
|
|
16
|
+
|
|
17
|
+
function addLink (res, value, rel) {
|
|
18
|
+
const oldLink = res.get('Link')
|
|
19
|
+
if (oldLink === undefined) {
|
|
20
|
+
res.set('Link', '<' + value + '>; rel="' + rel + '"')
|
|
21
|
+
} else {
|
|
22
|
+
res.set('Link', oldLink + ', ' + '<' + value + '>; rel="' + rel + '"')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function addLinks (res, fileMetadata) {
|
|
27
|
+
if (fileMetadata.isResource) {
|
|
28
|
+
addLink(res, 'http://www.w3.org/ns/ldp#Resource', 'type')
|
|
29
|
+
}
|
|
30
|
+
if (fileMetadata.isSourceResource) {
|
|
31
|
+
addLink(res, 'http://www.w3.org/ns/ldp#RDFSource', 'type')
|
|
32
|
+
}
|
|
33
|
+
if (fileMetadata.isContainer) {
|
|
34
|
+
addLink(res, 'http://www.w3.org/ns/ldp#Container', 'type')
|
|
35
|
+
}
|
|
36
|
+
if (fileMetadata.isBasicContainer) {
|
|
37
|
+
addLink(res, 'http://www.w3.org/ns/ldp#BasicContainer', 'type')
|
|
38
|
+
}
|
|
39
|
+
if (fileMetadata.isDirectContainer) {
|
|
40
|
+
addLink(res, 'http://www.w3.org/ns/ldp#DirectContainer', 'type')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function linksHandler (req, res, next) {
|
|
45
|
+
const ldp = req.app.locals.ldp
|
|
46
|
+
let filename
|
|
47
|
+
try {
|
|
48
|
+
// Hack: createIfNotExists is set to true for PUT or PATCH requests
|
|
49
|
+
// because the file might not exist yet at this point.
|
|
50
|
+
// But it will be created afterwards.
|
|
51
|
+
// This should be improved with the new server architecture.
|
|
52
|
+
({ path: filename } = await ldp.resourceMapper
|
|
53
|
+
.mapUrlToFile({ url: req, createIfNotExists: req.method === 'PUT' || req.method === 'PATCH' }))
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Silently ignore errors here
|
|
56
|
+
// Later handlers will error as well, but they will be able to given a more concrete error message (like 400 or 404)
|
|
57
|
+
return next()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (path.extname(filename) === ldp.suffixMeta) {
|
|
61
|
+
debug.metadata('Trying to access metadata file as regular file.')
|
|
62
|
+
|
|
63
|
+
return next(error(404, 'Trying to access metadata file as regular file'))
|
|
64
|
+
}
|
|
65
|
+
const fileMetadata = new metadata.Metadata()
|
|
66
|
+
if (filename.endsWith('/')) {
|
|
67
|
+
fileMetadata.isContainer = true
|
|
68
|
+
fileMetadata.isBasicContainer = true
|
|
69
|
+
} else {
|
|
70
|
+
fileMetadata.isResource = true
|
|
71
|
+
}
|
|
72
|
+
// Add LDP-required Accept-Post header for OPTIONS request to containers
|
|
73
|
+
if (fileMetadata.isContainer && req.method === 'OPTIONS') {
|
|
74
|
+
res.header('Accept-Post', '*/*')
|
|
75
|
+
}
|
|
76
|
+
// Add ACL and Meta Link in header
|
|
77
|
+
addLink(res, utils.pathBasename(req.path) + ldp.suffixAcl, 'acl')
|
|
78
|
+
addLink(res, utils.pathBasename(req.path) + ldp.suffixMeta, 'describedBy')
|
|
79
|
+
// Add other Link headers
|
|
80
|
+
addLinks(res, fileMetadata)
|
|
81
|
+
next()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseMetadataFromHeader (linkHeader) {
|
|
85
|
+
const fileMetadata = new metadata.Metadata()
|
|
86
|
+
if (linkHeader === undefined) {
|
|
87
|
+
return fileMetadata
|
|
88
|
+
}
|
|
89
|
+
const links = linkHeader.split(',')
|
|
90
|
+
for (const linkIndex in links) {
|
|
91
|
+
const link = links[linkIndex]
|
|
92
|
+
const parsedLinks = li.parse(link)
|
|
93
|
+
for (const rel in parsedLinks) {
|
|
94
|
+
if (rel === 'type') {
|
|
95
|
+
if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#Resource') {
|
|
96
|
+
fileMetadata.isResource = true
|
|
97
|
+
} else if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#RDFSource') {
|
|
98
|
+
fileMetadata.isSourceResource = true
|
|
99
|
+
} else if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#Container') {
|
|
100
|
+
fileMetadata.isContainer = true
|
|
101
|
+
} else if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#BasicContainer') {
|
|
102
|
+
fileMetadata.isBasicContainer = true
|
|
103
|
+
} else if (parsedLinks[rel] === 'http://www.w3.org/ns/ldp#DirectContainer') {
|
|
104
|
+
fileMetadata.isDirectContainer = true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return fileMetadata
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Adds a header that describes the user's permissions
|
|
113
|
+
async function addPermissions (req, res, next) {
|
|
114
|
+
const { acl, session } = req
|
|
115
|
+
if (!acl) return next()
|
|
116
|
+
|
|
117
|
+
// Turn permissions for the public and the user into a header
|
|
118
|
+
const ldp = req.app.locals.ldp
|
|
119
|
+
const resource = ldp.resourceMapper.resolveUrl(req.hostname, req.path)
|
|
120
|
+
let [publicPerms, userPerms] = await Promise.all([
|
|
121
|
+
getPermissionsFor(acl, null, req),
|
|
122
|
+
getPermissionsFor(acl, session.userId, req)
|
|
123
|
+
])
|
|
124
|
+
if (resource.endsWith('.acl') && userPerms === '' && session.userId === await ldp.getOwner(req.hostname)) userPerms = 'control'
|
|
125
|
+
debug.ACL(`Permissions on ${resource} for ${session.userId || '(none)'}: ${userPerms}`)
|
|
126
|
+
debug.ACL(`Permissions on ${resource} for public: ${publicPerms}`)
|
|
127
|
+
res.set('WAC-Allow', `user="${userPerms}",public="${publicPerms}"`)
|
|
128
|
+
next()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Gets the permissions string for the given user and resource
|
|
132
|
+
async function getPermissionsFor (acl, user, req) {
|
|
133
|
+
const accesses = MODES.map(mode => acl.can(user, mode))
|
|
134
|
+
const allowed = await Promise.all(accesses)
|
|
135
|
+
return PERMISSIONS.filter((mode, i) => allowed[i]).join(' ')
|
|
136
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module.exports = HTTPError
|
|
2
|
+
|
|
3
|
+
function HTTPError (status, message) {
|
|
4
|
+
if (!(this instanceof HTTPError)) {
|
|
5
|
+
return new HTTPError(status, message)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Error.captureStackTrace(this, this.constructor)
|
|
9
|
+
this.name = this.constructor.name
|
|
10
|
+
|
|
11
|
+
// If status is an object it will be of the form:
|
|
12
|
+
// {status: , message: }
|
|
13
|
+
if (typeof status === 'number') {
|
|
14
|
+
this.message = message || 'Error occurred'
|
|
15
|
+
this.status = status
|
|
16
|
+
} else {
|
|
17
|
+
const err = status
|
|
18
|
+
let _status
|
|
19
|
+
let _code
|
|
20
|
+
let _message
|
|
21
|
+
if (err && err.status) {
|
|
22
|
+
_status = err.status
|
|
23
|
+
}
|
|
24
|
+
if (err && err.code) {
|
|
25
|
+
_code = err.code
|
|
26
|
+
}
|
|
27
|
+
if (err && err.message) {
|
|
28
|
+
_message = err.message
|
|
29
|
+
}
|
|
30
|
+
this.message = message || _message
|
|
31
|
+
this.status = _status || _code === 'ENOENT' ? 404 : 500
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
require('util').inherits(module.exports, Error)
|