-
Notifications
You must be signed in to change notification settings - Fork 305
/
Copy pathpatch.js
235 lines (213 loc) · 8.96 KB
/
patch.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
// Express handler for LDP PATCH requests
module.exports = handler
const bodyParser = require('body-parser')
const fs = require('fs')
const debug = require('../debug').handlers
const error = require('../http-error')
const $rdf = require('rdflib')
const crypto = require('crypto')
const { overQuota, getContentType } = require('../utils')
const withLock = require('../lock')
// Patch parsers by request body content type
const PATCH_PARSERS = {
'application/sparql-update': require('./patch/sparql-update-parser.js'),
'application/sparql-update-single-match': require('./patch/sparql-update-parser.js'),
'text/n3': require('./patch/n3-patch-parser.js')
}
// use media-type as contentType for new RDF resource
const DEFAULT_FOR_NEW_CONTENT_TYPE = 'text/turtle'
function contentTypeForNew (req) {
let contentTypeForNew = DEFAULT_FOR_NEW_CONTENT_TYPE
if (req.path.endsWith('.jsonld')) contentTypeForNew = 'application/ld+json'
else if (req.path.endsWith('.n3')) contentTypeForNew = 'text/n3'
else if (req.path.endsWith('.rdf')) contentTypeForNew = 'application/rdf+xml'
return contentTypeForNew
}
function contentForNew (contentType) {
let contentForNew = ''
if (contentType.includes('ld+json')) contentForNew = JSON.stringify('{}')
else if (contentType.includes('rdf+xml')) contentForNew = '<rdf:RDF\n xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n\n</rdf:RDF>'
return contentForNew
}
// Handles a PATCH request
async function patchHandler (req, res, next) {
debug(`PATCH -- ${req.originalUrl}`)
try {
// Obtain details of the target resource
const ldp = req.app.locals.ldp
let path, contentType
let resourceExists = true
try {
// First check if the file already exists
({ path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req }))
} catch (err) {
// If the file doesn't exist, request to create one with the file media type as contentType
({ path, contentType } = await ldp.resourceMapper.mapUrlToFile(
{ url: req, createIfNotExists: true, contentType: contentTypeForNew(req) }))
// check if a folder with same name exists
try {
await ldp.checkItemName(req)
} catch (err) {
return next(err)
}
resourceExists = false
}
const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname })
const resource = { path, contentType, url }
debug('PATCH -- Target <%s> (%s)', url, contentType)
// Obtain details of the patch document
const patch = {}
patch.text = req.body ? req.body.toString() : ''
patch.uri = `${url}#patch-${hash(patch.text)}`
patch.contentType = getContentType(req.headers)
if (!patch.contentType) {
throw error(400, 'PATCH request requires a content-type via the Content-Type header')
}
debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType)
const parsePatch = PATCH_PARSERS[patch.contentType]
if (!parsePatch) {
throw error(415, `Unsupported patch content type: ${patch.contentType}`)
}
res.header('Accept-Patch', patch.contentType) // is this needed ?
// Parse the patch document and verify permissions
const patchObject = await parsePatch(url, patch.uri, patch.text)
await checkPermission(req, patchObject, resourceExists)
// Create the enclosing directory, if necessary
await ldp.createDirectory(path, req.hostname)
// Patch the graph and write it back to the file
const result = await withLock(path, async () => {
const graph = await readGraph(resource)
await applyPatch(patchObject, graph, url)
return writeGraph(graph, resource, ldp.resourceMapper.resolveFilePath(req.hostname), ldp.serverUri)
})
// Send the result to the client
res.send(result)
} catch (err) {
return next(err)
}
return next()
}
// Reads the request body and calls the actual patch handler
function handler (req, res, next) {
readEntity(req, res, () => patchHandler(req, res, next))
}
const readEntity = bodyParser.text({ type: () => true })
// Reads the RDF graph in the given resource
function readGraph (resource) {
// Read the resource's file
return new Promise((resolve, reject) =>
fs.readFile(resource.path, { encoding: 'utf8' }, function (err, fileContents) {
if (err) {
// If the file does not exist, assume empty contents
// (it will be created after a successful patch)
if (err.code === 'ENOENT') {
fileContents = contentForNew(resource.contentType)
// Fail on all other errors
} else {
return reject(error(500, `Original file read error: ${err}`))
}
}
debug('PATCH -- Read target file (%d bytes)', fileContents.length)
fileContents = resource.contentType.includes('json') ? JSON.parse(fileContents) : fileContents
resolve(fileContents)
})
)
// Parse the resource's file contents
.then((fileContents) => {
const graph = $rdf.graph()
debug('PATCH -- Reading %s with content type %s', resource.url, resource.contentType)
try {
$rdf.parse(fileContents, graph, resource.url, resource.contentType)
} catch (err) {
throw error(500, `Patch: Target ${resource.contentType} file syntax error: ${err}`)
}
debug('PATCH -- Parsed target file')
return graph
})
}
// Verifies whether the user is allowed to perform the patch on the target
async function checkPermission (request, patchObject, resourceExists) {
// If no ACL object was passed down, assume permissions are okay.
if (!request.acl) return Promise.resolve(patchObject)
// At this point, we already assume append access,
// as this can be checked upfront before parsing the patch.
// Now that we know the details of the patch,
// we might need to perform additional checks.
let modes = []
const { acl, session: { userId } } = request
// Read access is required for DELETE and WHERE.
// If we would allows users without read access,
// they could use DELETE or WHERE to trigger 200 or 409,
// and thereby guess the existence of certain triples.
// DELETE additionally requires write access.
if (patchObject.delete) {
// ACTUALLY Read not needed by solid/test-suite only Write
modes = ['Read', 'Write']
// checks = [acl.can(userId, 'Read'), acl.can(userId, 'Write')]
} else if (patchObject.where) {
modes = modes.concat(['Read'])
// checks = [acl.can(userId, 'Read')]
}
const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode, request.method, resourceExists)))
const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true)
if (!allAllowed) {
// check owner with Control
const ldp = request.app.locals.ldp
if (request.path.endsWith('.acl') && await ldp.isOwner(userId, request.hostname)) return Promise.resolve(patchObject)
const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode)))
const error = errors.filter(error => !!error)
.reduce((prevErr, err) => prevErr.status > err.status ? prevErr : err, { status: 0 })
return Promise.reject(error)
}
return Promise.resolve(patchObject)
}
// Applies the patch to the RDF graph
function applyPatch (patchObject, graph, url) {
debug('PATCH -- Applying patch')
return new Promise((resolve, reject) =>
graph.applyPatch(patchObject, graph.sym(url), (err) => {
if (err) {
const message = err.message || err // returns string at the moment
debug(`PATCH -- FAILED. Returning 409. Message: '${message}'`)
return reject(error(409, `The patch could not be applied. ${message}`))
}
resolve(graph)
})
)
}
// Writes the RDF graph to the given resource
function writeGraph (graph, resource, root, serverUri) {
debug('PATCH -- Writing patched file')
return new Promise((resolve, reject) => {
const resourceSym = graph.sym(resource.url)
function doWrite (serialized) {
// First check if we are above quota
overQuota(root, serverUri).then((isOverQuota) => {
if (isOverQuota) {
return reject(error(413,
'User has exceeded their storage quota'))
}
fs.writeFile(resource.path, serialized, { encoding: 'utf8' }, function (err) {
if (err) {
return reject(error(500, `Failed to write file after patch: ${err}`))
}
debug('PATCH -- applied successfully')
resolve('Patch applied successfully.\n')
})
}).catch(() => reject(error(500, 'Error finding user quota')))
}
if (resource.contentType === 'application/ld+json') {
$rdf.serialize(resourceSym, graph, resource.url, resource.contentType, function (err, result) {
if (err) return reject(error(500, `Failed to serialize after patch: ${err}`))
doWrite(result)
})
} else {
const serialized = $rdf.serialize(resourceSym, graph, resource.url, resource.contentType)
doWrite(serialized)
}
})
}
// Creates a hash of the given text
function hash (text) {
return crypto.createHash('md5').update(text).digest('hex')
}