@@ -9,44 +9,50 @@ const debug = require('../debug').handlers
99const utils = require ( '../utils.js' )
1010const error = require ( '../http-error' )
1111const $rdf = require ( 'rdflib' )
12+ const crypto = require ( 'crypto' )
1213
1314const DEFAULT_TARGET_TYPE = 'text/turtle'
1415
15- // Patch handlers by request body content type
16- const PATCHERS = {
17- 'application/sparql-update' : require ( './patch/sparql-update-patcher.js' )
16+ // Patch parsers by request body content type
17+ const PATCH_PARSERS = {
18+ 'application/sparql-update' : require ( './patch/sparql-update-parser.js' ) ,
19+ 'text/n3' : require ( './patch/n3-patch-parser.js' )
1820}
1921
2022// Handles a PATCH request
2123function patchHandler ( req , res , next ) {
22- debug ( ' PATCH -- ' + req . originalUrl )
24+ debug ( ` PATCH -- ${ req . originalUrl } ` )
2325 res . header ( 'MS-Author-Via' , 'SPARQL' )
2426
25- // Obtain details of the patch document
26- const patch = {
27- text : req . body ? req . body . toString ( ) : '' ,
28- contentType : ( req . get ( 'content-type' ) || '' ) . match ( / ^ [ ^ ; \s ] * / ) [ 0 ]
29- }
30- const patchGraph = PATCHERS [ patch . contentType ]
31- if ( ! patchGraph ) {
32- return next ( error ( 415 , 'Unknown patch content type: ' + patch . contentType ) )
33- }
34- debug ( 'PATCH -- Received patch (%d bytes, %s)' , patch . text . length , patch . contentType )
35-
3627 // Obtain details of the target resource
3728 const ldp = req . app . locals . ldp
38- const root = ! ldp . idp ? ldp . root : ldp . root + req . hostname + '/'
39- const target = {
40- file : utils . uriToFilename ( req . path , root ) ,
41- uri : utils . uriAbs ( req ) + req . originalUrl
42- }
29+ const root = ! ldp . idp ? ldp . root : `${ ldp . root } ${ req . hostname } /`
30+ const target = { }
31+ target . file = utils . uriToFilename ( req . path , root )
32+ target . uri = utils . uriAbs ( req ) + req . originalUrl
4333 target . contentType = mime . lookup ( target . file ) || DEFAULT_TARGET_TYPE
4434 debug ( 'PATCH -- Target <%s> (%s)' , target . uri , target . contentType )
4535
46- // Read the RDF graph to be patched from the file
47- readGraph ( target )
36+ // Obtain details of the patch document
37+ const patch = { }
38+ patch . text = req . body ? req . body . toString ( ) : ''
39+ patch . uri = `${ target . uri } #patch-${ hash ( patch . text ) } `
40+ patch . contentType = ( req . get ( 'content-type' ) || '' ) . match ( / ^ [ ^ ; \s ] * / ) [ 0 ]
41+ debug ( 'PATCH -- Received patch (%d bytes, %s)' , patch . text . length , patch . contentType )
42+ const parsePatch = PATCH_PARSERS [ patch . contentType ]
43+ if ( ! parsePatch ) {
44+ return next ( error ( 415 , `Unsupported patch content type: ${ patch . contentType } ` ) )
45+ }
46+
47+ // Parse the target graph and the patch document,
48+ // and verify permission for performing this specific patch
49+ Promise . all ( [
50+ readGraph ( target ) ,
51+ parsePatch ( target . uri , patch . uri , patch . text )
52+ . then ( patchObject => checkPermission ( target , req , patchObject ) )
53+ ] )
4854 // Patch the graph and write it back to the file
49- . then ( graph => patchGraph ( graph , target . uri , patch . text ) )
55+ . then ( ( [ graph , patchObject ] ) => applyPatch ( patchObject , graph , target ) )
5056 . then ( graph => writeGraph ( graph , target ) )
5157 // Send the result to the client
5258 . then ( result => { res . send ( result ) } )
@@ -71,7 +77,7 @@ function readGraph (resource) {
7177 fileContents = ''
7278 // Fail on all other errors
7379 } else {
74- return reject ( error ( 500 , 'Patch: Original file read error:' + err ) )
80+ return reject ( error ( 500 , ` Original file read error: ${ err } ` ) )
7581 }
7682 }
7783 debug ( 'PATCH -- Read target file (%d bytes)' , fileContents . length )
@@ -85,25 +91,69 @@ function readGraph (resource) {
8591 try {
8692 $rdf . parse ( fileContents , graph , resource . uri , resource . contentType )
8793 } catch ( err ) {
88- throw error ( 500 , ' Patch: Target ' + resource . contentType + ' file syntax error:' + err )
94+ throw error ( 500 , ` Patch: Target ${ resource . contentType } file syntax error: ${ err } ` )
8995 }
9096 debug ( 'PATCH -- Parsed target file' )
9197 return graph
9298 } )
9399}
94100
101+ // Verifies whether the user is allowed to perform the patch on the target
102+ function checkPermission ( target , request , patchObject ) {
103+ // If no ACL object was passed down, assume permissions are okay.
104+ if ( ! request . acl ) return Promise . resolve ( patchObject )
105+ // At this point, we already assume append access,
106+ // as this can be checked upfront before parsing the patch.
107+ // Now that we know the details of the patch,
108+ // we might need to perform additional checks.
109+ let checks = [ ]
110+ const { acl, session : { userId } } = request
111+ // Read access is required for DELETE and WHERE.
112+ // If we would allows users without read access,
113+ // they could use DELETE or WHERE to trigger 200 or 409,
114+ // and thereby guess the existence of certain triples.
115+ // DELETE additionally requires write access.
116+ if ( patchObject . delete ) {
117+ checks = [ acl . can ( userId , 'Read' ) , acl . can ( userId , 'Write' ) ]
118+ } else if ( patchObject . where ) {
119+ checks = [ acl . can ( userId , 'Read' ) ]
120+ }
121+ return Promise . all ( checks ) . then ( ( ) => patchObject )
122+ }
123+
124+ // Applies the patch to the RDF graph
125+ function applyPatch ( patchObject , graph , target ) {
126+ debug ( 'PATCH -- Applying patch' )
127+ return new Promise ( ( resolve , reject ) =>
128+ graph . applyPatch ( patchObject , graph . sym ( target . uri ) , ( err ) => {
129+ if ( err ) {
130+ const message = err . message || err // returns string at the moment
131+ debug ( `PATCH -- FAILED. Returning 409. Message: '${ message } '` )
132+ return reject ( error ( 409 , `The patch could not be applied. ${ message } ` ) )
133+ }
134+ resolve ( graph )
135+ } )
136+ )
137+ }
138+
95139// Writes the RDF graph to the given resource
96140function writeGraph ( graph , resource ) {
141+ debug ( 'PATCH -- Writing patched file' )
97142 return new Promise ( ( resolve , reject ) => {
98143 const resourceSym = graph . sym ( resource . uri )
99144 const serialized = $rdf . serialize ( resourceSym , graph , resource . uri , resource . contentType )
100145
101146 fs . writeFile ( resource . file , serialized , { encoding : 'utf8' } , function ( err ) {
102147 if ( err ) {
103- return reject ( error ( 500 , ' Failed to write file back after patch: ' + err ) )
148+ return reject ( error ( 500 , ` Failed to write file after patch: ${ err } ` ) )
104149 }
105- debug ( 'PATCH -- applied OK (sync) ' )
106- resolve ( 'Patch applied OK \n' )
150+ debug ( 'PATCH -- applied successfully ' )
151+ resolve ( 'Patch applied successfully. \n' )
107152 } )
108153 } )
109154}
155+
156+ // Creates a hash of the given text
157+ function hash ( text ) {
158+ return crypto . createHash ( 'md5' ) . update ( text ) . digest ( 'hex' )
159+ }
0 commit comments