solid-server 5.7.11 → 5.8.0
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/.github/workflows/ci.yml +18 -5
- package/README.md +6 -5
- package/bin/lib/options.js +6 -0
- package/bin/solid +1 -1
- package/bin/solid.js +1 -1
- package/lib/create-app.js +14 -2
- package/lib/debug.js +1 -0
- package/lib/handlers/delete.js +3 -0
- package/lib/handlers/get.js +32 -7
- package/lib/handlers/notify.js +130 -0
- package/lib/handlers/patch.js +5 -1
- package/lib/handlers/post.js +5 -0
- package/lib/handlers/put.js +4 -1
- package/lib/ldp-middleware.js +9 -1
- package/lib/ldp.js +9 -11
- package/lib/rdf-notification-template.js +66 -0
- package/package.json +18 -11
package/.github/workflows/ci.yml
CHANGED
|
@@ -17,17 +17,30 @@ jobs:
|
|
|
17
17
|
|
|
18
18
|
strategy:
|
|
19
19
|
matrix:
|
|
20
|
-
node-version: [
|
|
20
|
+
node-version: [ '>=20.17.0' ]
|
|
21
21
|
os: [ubuntu-latest]
|
|
22
22
|
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
# extract repository name
|
|
27
|
+
- if: github.event_name == 'pull_request'
|
|
28
|
+
run: echo "REPO_NAME=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV
|
|
29
|
+
|
|
30
|
+
- if: github.event_name != 'pull_request'
|
|
31
|
+
run: echo "REPO_NAME=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
|
|
32
|
+
|
|
25
33
|
# extract branch name
|
|
26
34
|
- if: github.event_name == 'pull_request'
|
|
27
35
|
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
|
|
36
|
+
|
|
28
37
|
- if: github.event_name != 'pull_request'
|
|
29
38
|
run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
|
|
30
39
|
|
|
40
|
+
# print repository name
|
|
41
|
+
- name: Get repository name
|
|
42
|
+
run: echo 'The repository name is' $REPO_NAME
|
|
43
|
+
|
|
31
44
|
# print branch name
|
|
32
45
|
- name: Get branch name
|
|
33
46
|
run: echo 'The branch name is' $BRANCH_NAME
|
|
@@ -40,12 +53,12 @@ jobs:
|
|
|
40
53
|
# test code
|
|
41
54
|
- run: npm run standard
|
|
42
55
|
- run: npm run validate
|
|
43
|
-
- run: npm run
|
|
56
|
+
- run: npm run c8
|
|
44
57
|
# Test global install of the package
|
|
45
58
|
- run: npm pack .
|
|
46
59
|
- run: npm install -g solid-server-*.tgz
|
|
47
60
|
# Run the Solid test-suite
|
|
48
|
-
- run: bash test/surface/run-solid-test-suite.sh $BRANCH_NAME
|
|
61
|
+
- run: bash test/surface/run-solid-test-suite.sh $BRANCH_NAME $REPO_NAME
|
|
49
62
|
|
|
50
63
|
# TODO: The pipeline should automate publication to npm, so that the docker build gets the correct version
|
|
51
64
|
# This job will only dockerize solid-server@latest / solid-server@<tag-name> from npmjs.com!
|
|
@@ -56,7 +69,7 @@ jobs:
|
|
|
56
69
|
runs-on: ubuntu-latest
|
|
57
70
|
steps:
|
|
58
71
|
|
|
59
|
-
- uses: actions/checkout@
|
|
72
|
+
- uses: actions/checkout@v4
|
|
60
73
|
|
|
61
74
|
- uses: olegtarasov/get-tag@v2.1
|
|
62
75
|
id: tagName
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ Your users will have a dedicated folder under `./data` at `./data/<username>.<yo
|
|
|
138
138
|
> To use Gmail you may need to configure ["Allow Less Secure Apps"](https://www.google.com/settings/security/lesssecureapps) in your Gmail account unless you are using 2FA in which case you would have to create an [Application Specific](https://security.google.com/settings/security/apppasswords) password. You also may need to unlock your account with ["Allow access to your Google account"](https://accounts.google.com/DisplayUnlockCaptcha) to use SMTP.
|
|
139
139
|
|
|
140
140
|
also add to `config.json`
|
|
141
|
-
```
|
|
141
|
+
```
|
|
142
142
|
"useEmail": true,
|
|
143
143
|
"emailHost": "smtp.gmail.com",
|
|
144
144
|
"emailPort": "465",
|
|
@@ -206,6 +206,7 @@ $ solid start --help
|
|
|
206
206
|
--multiuser Enable multi-user mode
|
|
207
207
|
--idp [value] Obsolete; use --multiuser
|
|
208
208
|
--no-live Disable live support through WebSockets
|
|
209
|
+
--no-prep Disable Per Resource Events
|
|
209
210
|
--proxy [value] Obsolete; use --corsProxy
|
|
210
211
|
--cors-proxy [value] Serve the CORS proxy on this path
|
|
211
212
|
--suppress-data-browser Suppress provision of a data browser
|
|
@@ -271,7 +272,7 @@ docker run -p 8443:8443 --name solid node-solid-server
|
|
|
271
272
|
|
|
272
273
|
|
|
273
274
|
This will enable you to login to solid on https://localhost:8443 and then create a new account
|
|
274
|
-
but not yet use that account. After a new account is made you will need to create an entry for
|
|
275
|
+
but not yet use that account. After a new account is made you will need to create an entry for
|
|
275
276
|
it in your local (/etc/)hosts file in line with the account and subdomain, i.e. --
|
|
276
277
|
|
|
277
278
|
```pre
|
|
@@ -280,16 +281,16 @@ it in your local (/etc/)hosts file in line with the account and subdomain, i.e.
|
|
|
280
281
|
|
|
281
282
|
You can modify the config within the docker container as follows:
|
|
282
283
|
|
|
283
|
-
- Copy the `config.json` to the current directory with:
|
|
284
|
+
- Copy the `config.json` to the current directory with:
|
|
284
285
|
```bash
|
|
285
286
|
docker cp solid:/usr/src/app/config.json .
|
|
286
287
|
```
|
|
287
288
|
- Edit the `config.json` file
|
|
288
|
-
- Copy the file back with
|
|
289
|
+
- Copy the file back with
|
|
289
290
|
```bash
|
|
290
291
|
docker cp config.json solid:/usr/src/app/
|
|
291
292
|
```
|
|
292
|
-
- Restart the server with
|
|
293
|
+
- Restart the server with
|
|
293
294
|
```bash
|
|
294
295
|
docker restart solid
|
|
295
296
|
```
|
package/bin/lib/options.js
CHANGED
|
@@ -143,6 +143,12 @@ module.exports = [
|
|
|
143
143
|
flag: true,
|
|
144
144
|
default: false
|
|
145
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: 'no-prep',
|
|
148
|
+
help: 'Disable Per Resource Events',
|
|
149
|
+
flag: true,
|
|
150
|
+
default: false
|
|
151
|
+
},
|
|
146
152
|
// {
|
|
147
153
|
// full: 'default-app',
|
|
148
154
|
// help: 'URI to use as a default app for resources (default: https://linkeddata.github.io/warp/#/list/)'
|
package/bin/solid
CHANGED
package/bin/solid.js
CHANGED
package/lib/create-app.js
CHANGED
|
@@ -28,6 +28,11 @@ const ResourceMapper = require('./resource-mapper')
|
|
|
28
28
|
const aclCheck = require('@solid/acl-check')
|
|
29
29
|
const { version } = require('../package.json')
|
|
30
30
|
|
|
31
|
+
const acceptEvents = require('express-accept-events').default
|
|
32
|
+
const events = require('express-negotiate-events').default
|
|
33
|
+
const eventID = require('express-prep/event-id').default
|
|
34
|
+
const prep = require('express-prep').default
|
|
35
|
+
|
|
31
36
|
const corsSettings = cors({
|
|
32
37
|
methods: [
|
|
33
38
|
'OPTIONS', 'HEAD', 'GET', 'PATCH', 'POST', 'PUT', 'DELETE'
|
|
@@ -61,6 +66,12 @@ function createApp (argv = {}) {
|
|
|
61
66
|
|
|
62
67
|
const app = express()
|
|
63
68
|
|
|
69
|
+
// Add PREP support
|
|
70
|
+
if (argv.prep) {
|
|
71
|
+
app.use(eventID)
|
|
72
|
+
app.use(acceptEvents, events, prep)
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
initAppLocals(app, argv, ldp)
|
|
65
76
|
initHeaders(app)
|
|
66
77
|
initViews(app, configPath)
|
|
@@ -115,7 +126,7 @@ function createApp (argv = {}) {
|
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
// Attach the LDP middleware
|
|
118
|
-
app.use('/', LdpMiddleware(corsSettings))
|
|
129
|
+
app.use('/', LdpMiddleware(corsSettings, argv.prep))
|
|
119
130
|
|
|
120
131
|
// https://stackoverflow.com/questions/51741383/nodejs-express-return-405-for-un-supported-method
|
|
121
132
|
app.use(function (req, res, next) {
|
|
@@ -168,6 +179,7 @@ function initAppLocals (app, argv, ldp) {
|
|
|
168
179
|
app.locals.enforceToc = argv.enforceToc
|
|
169
180
|
app.locals.tocUri = argv.tocUri
|
|
170
181
|
app.locals.disablePasswordChecks = argv.disablePasswordChecks
|
|
182
|
+
app.locals.prep = argv.prep
|
|
171
183
|
|
|
172
184
|
if (argv.email && argv.email.host) {
|
|
173
185
|
app.locals.emailService = new EmailService(argv.templates.email, argv.email)
|
|
@@ -287,7 +299,7 @@ function initWebId (argv, app, ldp) {
|
|
|
287
299
|
initAuthentication(app, argv)
|
|
288
300
|
|
|
289
301
|
if (argv.multiuser) {
|
|
290
|
-
app.use(vhost('*', LdpMiddleware(corsSettings)))
|
|
302
|
+
app.use(vhost('*', LdpMiddleware(corsSettings, argv.prep)))
|
|
291
303
|
}
|
|
292
304
|
}
|
|
293
305
|
|
package/lib/debug.js
CHANGED
package/lib/handlers/delete.js
CHANGED
|
@@ -6,9 +6,12 @@ async function handler (req, res, next) {
|
|
|
6
6
|
debug('DELETE -- Request on' + req.originalUrl)
|
|
7
7
|
|
|
8
8
|
const ldp = req.app.locals.ldp
|
|
9
|
+
const prep = req.app.locals.prep
|
|
9
10
|
try {
|
|
10
11
|
await ldp.delete(req)
|
|
11
12
|
debug('DELETE -- Ok.')
|
|
13
|
+
// Add event-id for notifications
|
|
14
|
+
prep && res.setHeader('Event-ID', res.setEventID())
|
|
12
15
|
res.sendStatus(200)
|
|
13
16
|
next()
|
|
14
17
|
} catch (err) {
|
package/lib/handlers/get.js
CHANGED
|
@@ -17,9 +17,13 @@ const translate = require('../utils.js').translate
|
|
|
17
17
|
const error = require('../http-error')
|
|
18
18
|
|
|
19
19
|
const RDFs = require('../ldp').mimeTypesAsArray()
|
|
20
|
+
const isRdf = require('../ldp').mimeTypeIsRdf
|
|
21
|
+
|
|
22
|
+
const prepConfig = 'accept=("message/rfc822" "application/ld+json" "text/turtle")'
|
|
20
23
|
|
|
21
24
|
async function handler (req, res, next) {
|
|
22
25
|
const ldp = req.app.locals.ldp
|
|
26
|
+
const prep = req.app.locals.prep
|
|
23
27
|
const includeBody = req.method === 'GET'
|
|
24
28
|
const negotiator = new Negotiator(req)
|
|
25
29
|
const baseUri = ldp.resourceMapper.resolveUrl(req.hostname, req.path)
|
|
@@ -103,7 +107,7 @@ async function handler (req, res, next) {
|
|
|
103
107
|
debug(' sending data browser file: ' + dataBrowserPath)
|
|
104
108
|
res.sendFile(dataBrowserPath)
|
|
105
109
|
return
|
|
106
|
-
} else if (stream) {
|
|
110
|
+
} else if (stream) { // EXIT text/html
|
|
107
111
|
res.setHeader('Content-Type', contentType)
|
|
108
112
|
return stream.pipe(res)
|
|
109
113
|
}
|
|
@@ -111,14 +115,27 @@ async function handler (req, res, next) {
|
|
|
111
115
|
|
|
112
116
|
// If request accepts the content-type we found
|
|
113
117
|
if (stream && negotiator.mediaType([contentType])) {
|
|
114
|
-
|
|
118
|
+
let headers = {
|
|
119
|
+
'Content-Type': contentType
|
|
120
|
+
}
|
|
115
121
|
if (contentRange) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
headers = {
|
|
123
|
+
...headers,
|
|
124
|
+
'Content-Range': contentRange,
|
|
125
|
+
'Accept-Ranges': 'bytes',
|
|
126
|
+
'Content-Length': chunksize
|
|
127
|
+
}
|
|
128
|
+
res.statusCode = 206
|
|
121
129
|
}
|
|
130
|
+
|
|
131
|
+
if (prep & isRdf(contentType) && !res.sendEvents({
|
|
132
|
+
config: { prep: prepConfig },
|
|
133
|
+
body: stream,
|
|
134
|
+
isBodyStream: true,
|
|
135
|
+
headers
|
|
136
|
+
})) return
|
|
137
|
+
res.set(headers)
|
|
138
|
+
return stream.pipe(res)
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
// If it is not in our RDFs we can't even translate,
|
|
@@ -130,6 +147,14 @@ async function handler (req, res, next) {
|
|
|
130
147
|
// Translate from the contentType found to the possibleRDFType desired
|
|
131
148
|
const data = await translate(stream, baseUri, contentType, possibleRDFType)
|
|
132
149
|
debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType)
|
|
150
|
+
const headers = {
|
|
151
|
+
'Content-Type': possibleRDFType
|
|
152
|
+
}
|
|
153
|
+
if (prep && isRdf(contentType) && !res.sendEvents({
|
|
154
|
+
config: { prep: prepConfig },
|
|
155
|
+
body: data,
|
|
156
|
+
headers
|
|
157
|
+
})) return
|
|
133
158
|
res.setHeader('Content-Type', possibleRDFType)
|
|
134
159
|
res.send(data)
|
|
135
160
|
return next()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
module.exports = handler
|
|
2
|
+
|
|
3
|
+
const libPath = require('path/posix')
|
|
4
|
+
|
|
5
|
+
const headerTemplate = require('express-prep/templates').header
|
|
6
|
+
const solidRDFTemplate = require('../rdf-notification-template')
|
|
7
|
+
const debug = require('../debug').prep
|
|
8
|
+
|
|
9
|
+
const ALLOWED_RDF_MIME_TYPES = [
|
|
10
|
+
'application/ld+json',
|
|
11
|
+
'application/activity+json',
|
|
12
|
+
'text/turtle'
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function getParent (path) {
|
|
16
|
+
if (path === '' || path === '/') return
|
|
17
|
+
const parent = libPath.dirname(path)
|
|
18
|
+
return parent === '/' ? '/' : `${parent}/`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getActivity (method, path) {
|
|
22
|
+
if (method === 'DELETE') {
|
|
23
|
+
return 'Delete'
|
|
24
|
+
}
|
|
25
|
+
if (method === 'POST' && path.endsWith('/')) {
|
|
26
|
+
return 'Add'
|
|
27
|
+
}
|
|
28
|
+
return 'Update'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getParentActivity (method, status) {
|
|
32
|
+
if (method === 'DELETE') {
|
|
33
|
+
return 'Remove'
|
|
34
|
+
}
|
|
35
|
+
if (status === 201) {
|
|
36
|
+
return 'Add'
|
|
37
|
+
}
|
|
38
|
+
return 'Update'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handler (req, res, next) {
|
|
42
|
+
const { trigger, defaultNotification } = res.events.prep
|
|
43
|
+
|
|
44
|
+
const { method, path } = req
|
|
45
|
+
const { statusCode } = res
|
|
46
|
+
const eventID = res.getHeader('event-id')
|
|
47
|
+
const fullUrl = new URL(path, `${req.protocol}://${req.hostname}/`)
|
|
48
|
+
|
|
49
|
+
// Date is a hack since node does not seem to provide access to send date.
|
|
50
|
+
// Date needs to be shared with parent notification
|
|
51
|
+
const eventDate = res._header.match(/^Date: (.*?)$/m)?.[1] ||
|
|
52
|
+
new Date().toUTCString()
|
|
53
|
+
|
|
54
|
+
// If the resource itself newly created,
|
|
55
|
+
// it could not have been subscribed for notifications already
|
|
56
|
+
if (!((method === 'PUT' || method === 'PATCH') && statusCode === 201)) {
|
|
57
|
+
try {
|
|
58
|
+
trigger({
|
|
59
|
+
generateNotification (
|
|
60
|
+
negotiatedFields
|
|
61
|
+
) {
|
|
62
|
+
const mediaType = negotiatedFields['content-type']
|
|
63
|
+
const activity = getActivity(method, path)
|
|
64
|
+
const target = activity === 'Add'
|
|
65
|
+
? res.getHeader('location')
|
|
66
|
+
: undefined
|
|
67
|
+
if (ALLOWED_RDF_MIME_TYPES.includes(mediaType?.[0])) {
|
|
68
|
+
return `${headerTemplate(negotiatedFields)}\r\n${solidRDFTemplate({
|
|
69
|
+
activity,
|
|
70
|
+
eventID,
|
|
71
|
+
object: String(fullUrl),
|
|
72
|
+
target,
|
|
73
|
+
date: eventDate,
|
|
74
|
+
// We use eTag as a proxy for state for now
|
|
75
|
+
state: res.getHeader('ETag'),
|
|
76
|
+
mediaType
|
|
77
|
+
})}`
|
|
78
|
+
} else {
|
|
79
|
+
return defaultNotification()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
} catch (error) {
|
|
84
|
+
debug(`Failed to trigger notification on route ${fullUrl}`)
|
|
85
|
+
// No special handling is necessary since the resource mutation was
|
|
86
|
+
// already successful. The purpose of this block is to prevent Express
|
|
87
|
+
// from triggering error handling middleware when notifications fail.
|
|
88
|
+
// An error notification might be sent in the future.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Write a notification to parent container
|
|
93
|
+
// POST in Solid creates a child resource
|
|
94
|
+
const parent = getParent(path)
|
|
95
|
+
if (parent && method !== 'POST') {
|
|
96
|
+
const parentID = res.setEventID(parent)
|
|
97
|
+
const parentUrl = new URL(parent, fullUrl)
|
|
98
|
+
try {
|
|
99
|
+
trigger({
|
|
100
|
+
path: parent,
|
|
101
|
+
generateNotification (
|
|
102
|
+
negotiatedFields
|
|
103
|
+
) {
|
|
104
|
+
const mediaType = negotiatedFields['content-type']
|
|
105
|
+
const activity = getParentActivity(method, statusCode)
|
|
106
|
+
const target = activity !== 'Update' ? String(fullUrl) : undefined
|
|
107
|
+
if (ALLOWED_RDF_MIME_TYPES.includes(mediaType?.[0])) {
|
|
108
|
+
return `${headerTemplate(negotiatedFields)}\r\n${solidRDFTemplate({
|
|
109
|
+
activity,
|
|
110
|
+
eventID: parentID,
|
|
111
|
+
date: eventDate,
|
|
112
|
+
object: String(parentUrl),
|
|
113
|
+
target,
|
|
114
|
+
eTag: undefined,
|
|
115
|
+
mediaType
|
|
116
|
+
})}`
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
} catch (error) {
|
|
121
|
+
debug(`Failed to trigger notification on parent route ${parentUrl}`)
|
|
122
|
+
// No special handling is necessary since the resource mutation was
|
|
123
|
+
// already successful. The purpose of this block is to prevent Express
|
|
124
|
+
// from triggering error handling middleware when notifications fail.
|
|
125
|
+
// An error notification might be sent in the future.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
next()
|
|
130
|
+
}
|
package/lib/handlers/patch.js
CHANGED
|
@@ -39,6 +39,7 @@ function contentForNew (contentType) {
|
|
|
39
39
|
// Handles a PATCH request
|
|
40
40
|
async function patchHandler (req, res, next) {
|
|
41
41
|
debug(`PATCH -- ${req.originalUrl}`)
|
|
42
|
+
const prep = req.app.locals.prep
|
|
42
43
|
try {
|
|
43
44
|
// Obtain details of the target resource
|
|
44
45
|
const ldp = req.app.locals.ldp
|
|
@@ -91,7 +92,10 @@ async function patchHandler (req, res, next) {
|
|
|
91
92
|
return writeGraph(graph, resource, ldp.resourceMapper.resolveFilePath(req.hostname), ldp.serverUri)
|
|
92
93
|
})
|
|
93
94
|
|
|
94
|
-
//
|
|
95
|
+
// Add event-id for notifications
|
|
96
|
+
prep && res.setHeader('Event-ID', res.setEventID())
|
|
97
|
+
// Send the status and result to the client
|
|
98
|
+
res.status(resourceExists ? 200 : 201)
|
|
95
99
|
res.send(result)
|
|
96
100
|
} catch (err) {
|
|
97
101
|
return next(err)
|
package/lib/handlers/post.js
CHANGED
|
@@ -11,6 +11,7 @@ const getContentType = require('../utils').getContentType
|
|
|
11
11
|
|
|
12
12
|
async function handler (req, res, next) {
|
|
13
13
|
const ldp = req.app.locals.ldp
|
|
14
|
+
const prep = req.app.locals.prep
|
|
14
15
|
const contentType = getContentType(req.headers)
|
|
15
16
|
debug('content-type is ', contentType)
|
|
16
17
|
// Handle SPARQL(-update?) query
|
|
@@ -72,6 +73,8 @@ async function handler (req, res, next) {
|
|
|
72
73
|
// Handled by backpressure of streams!
|
|
73
74
|
busboy.on('finish', function () {
|
|
74
75
|
debug('Done storing files')
|
|
76
|
+
// Add event-id for notifications
|
|
77
|
+
prep && res.setHeader('Event-ID', res.setEventID())
|
|
75
78
|
res.sendStatus(200)
|
|
76
79
|
next()
|
|
77
80
|
})
|
|
@@ -91,6 +94,8 @@ async function handler (req, res, next) {
|
|
|
91
94
|
debug('File stored in ' + resourcePath)
|
|
92
95
|
header.addLinks(res, links)
|
|
93
96
|
res.set('Location', resourcePath)
|
|
97
|
+
// Add event-id for notifications
|
|
98
|
+
prep && res.setHeader('Event-ID', res.setEventID())
|
|
94
99
|
res.sendStatus(201)
|
|
95
100
|
next()
|
|
96
101
|
},
|
package/lib/handlers/put.js
CHANGED
|
@@ -59,6 +59,7 @@ async function checkPermission (request, resourceExists) {
|
|
|
59
59
|
// TODO could be renamed as putResource (it now covers container and non-container)
|
|
60
60
|
async function putStream (req, res, next, stream = req) {
|
|
61
61
|
const ldp = req.app.locals.ldp
|
|
62
|
+
const prep = req.app.locals.prep
|
|
62
63
|
// try {
|
|
63
64
|
// Obtain details of the target resource
|
|
64
65
|
let resourceExists = true
|
|
@@ -77,7 +78,9 @@ async function putStream (req, res, next, stream = req) {
|
|
|
77
78
|
// Fails with Append on existing resource
|
|
78
79
|
if (!req.originalUrl.endsWith('.acl')) await checkPermission(req, resourceExists)
|
|
79
80
|
await ldp.put(req, stream, getContentType(req.headers))
|
|
80
|
-
|
|
81
|
+
// Add event-id for notifications
|
|
82
|
+
prep && res.setHeader('Event-ID', res.setEventID())
|
|
83
|
+
res.sendStatus(resourceExists ? 204 : 201)
|
|
81
84
|
return next()
|
|
82
85
|
} catch (err) {
|
|
83
86
|
err.message = 'Can\'t write file/folder: ' + err.message
|
package/lib/ldp-middleware.js
CHANGED
|
@@ -10,8 +10,9 @@ const del = require('./handlers/delete')
|
|
|
10
10
|
const patch = require('./handlers/patch')
|
|
11
11
|
const index = require('./handlers/index')
|
|
12
12
|
const copy = require('./handlers/copy')
|
|
13
|
+
const notify = require('./handlers/notify')
|
|
13
14
|
|
|
14
|
-
function LdpMiddleware (corsSettings) {
|
|
15
|
+
function LdpMiddleware (corsSettings, prep) {
|
|
15
16
|
const router = express.Router('/')
|
|
16
17
|
|
|
17
18
|
// Add Link headers
|
|
@@ -28,5 +29,12 @@ function LdpMiddleware (corsSettings) {
|
|
|
28
29
|
router.put('/*', allow('Append'), put)
|
|
29
30
|
router.delete('/*', allow('Write'), del)
|
|
30
31
|
|
|
32
|
+
if (prep) {
|
|
33
|
+
router.post('/*', notify)
|
|
34
|
+
router.patch('/*', notify)
|
|
35
|
+
router.put('/*', notify)
|
|
36
|
+
router.delete('/*', notify)
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
return router
|
|
32
40
|
}
|
package/lib/ldp.js
CHANGED
|
@@ -242,16 +242,15 @@ class LDP {
|
|
|
242
242
|
if (isOverQuota) {
|
|
243
243
|
throw error(413, 'User has exceeded their storage quota')
|
|
244
244
|
}
|
|
245
|
-
// Set url using folder/.meta
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
const { path } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true })
|
|
245
|
+
// Set url using folder/.meta
|
|
246
|
+
let { path } = await this.resourceMapper.mapUrlToFile({
|
|
247
|
+
url,
|
|
248
|
+
contentType,
|
|
249
|
+
createIfNotExists: true,
|
|
250
|
+
searchIndex: false
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (container) { path += suffixMeta }
|
|
255
254
|
// debug.handlers(container + ' item ' + (url.url || url) + ' ' + contentType + ' ' + path)
|
|
256
255
|
// check if file exists, and in that case that it has the same extension
|
|
257
256
|
if (!container) { await this.checkFileExtension(url, path) }
|
|
@@ -264,7 +263,6 @@ class LDP {
|
|
|
264
263
|
// clearAclCache()
|
|
265
264
|
}
|
|
266
265
|
// Directory created, now write the file
|
|
267
|
-
if (container) return
|
|
268
266
|
return withLock(path, () => new Promise((resolve, reject) => {
|
|
269
267
|
// HACK: the middleware in webid-oidc.js uses body-parser, thus ending the stream of data
|
|
270
268
|
// for JSON bodies. So, the stream needs to be reset
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const CONTEXT_ACTIVITYSTREAMS = 'https://www.w3.org/ns/activitystreams'
|
|
2
|
+
const CONTEXT_NOTIFICATION = 'https://www.w3.org/ns/solid/notification/v1'
|
|
3
|
+
const CONTEXT_XML_SCHEMA = 'http://www.w3.org/2001/XMLSchema'
|
|
4
|
+
|
|
5
|
+
function generateJSONNotification ({
|
|
6
|
+
activity: type,
|
|
7
|
+
eventId: id,
|
|
8
|
+
date: published,
|
|
9
|
+
object,
|
|
10
|
+
target,
|
|
11
|
+
state = undefined
|
|
12
|
+
}) {
|
|
13
|
+
return {
|
|
14
|
+
published,
|
|
15
|
+
type,
|
|
16
|
+
id,
|
|
17
|
+
object,
|
|
18
|
+
...(type === 'Add') && { target },
|
|
19
|
+
...(type === 'Remove') && { origin: target },
|
|
20
|
+
...(state) && { state }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateTurtleNotification ({
|
|
25
|
+
activity,
|
|
26
|
+
eventId,
|
|
27
|
+
date,
|
|
28
|
+
object,
|
|
29
|
+
target,
|
|
30
|
+
state = undefined
|
|
31
|
+
}) {
|
|
32
|
+
const stateLine = `\n notify:state "${state}" ;`
|
|
33
|
+
|
|
34
|
+
return `@prefix as: <${CONTEXT_ACTIVITYSTREAMS}#> .
|
|
35
|
+
@prefix notify: <${CONTEXT_NOTIFICATION}#> .
|
|
36
|
+
@prefix xsd: <${CONTEXT_XML_SCHEMA}#> .
|
|
37
|
+
|
|
38
|
+
<${eventId}> a as:${activity} ;${state && stateLine}
|
|
39
|
+
as:object ${object} ;
|
|
40
|
+
as:published "${date}"^^xsd:dateTime .`.replaceAll('\n', '\r\n')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function serializeToJSONLD (notification, isActivityStreams = false) {
|
|
44
|
+
notification['@context'] = [CONTEXT_NOTIFICATION]
|
|
45
|
+
if (!isActivityStreams) {
|
|
46
|
+
notification['@context'].unshift(CONTEXT_ACTIVITYSTREAMS)
|
|
47
|
+
}
|
|
48
|
+
return JSON.stringify(notification, null, 2)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rdfTemplate (props) {
|
|
52
|
+
const { mediaType } = props
|
|
53
|
+
if (mediaType[0] === 'application/activity+json' || (mediaType[0] === 'application/ld+json' && mediaType[1].get('profile')?.toLowerCase() === 'https://www.w3.org/ns/activitystreams')) {
|
|
54
|
+
return serializeToJSONLD(generateJSONNotification(props), true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (mediaType[0] === 'application/ld+json') {
|
|
58
|
+
return serializeToJSONLD(generateJSONNotification(props))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (mediaType[0] === 'text/turtle') {
|
|
62
|
+
return generateTurtleNotification(props)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = rdfTemplate
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-server",
|
|
3
3
|
"description": "Solid server on top of the file-system",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.8.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Tim Berners-Lee",
|
|
7
7
|
"email": "timbl@w3.org"
|
|
@@ -74,7 +74,10 @@
|
|
|
74
74
|
"cors": "^2.8.5",
|
|
75
75
|
"debug": "^4.3.4",
|
|
76
76
|
"express": "^4.18.3",
|
|
77
|
+
"express-accept-events": "^0.3.0",
|
|
77
78
|
"express-handlebars": "^5.3.5",
|
|
79
|
+
"express-negotiate-events": "^0.3.0",
|
|
80
|
+
"express-prep": "^0.6.3",
|
|
78
81
|
"express-session": "^1.18.0",
|
|
79
82
|
"extend": "^3.0.2",
|
|
80
83
|
"from2": "^2.3.0",
|
|
@@ -114,7 +117,9 @@
|
|
|
114
117
|
"vhost": "^3.0.2"
|
|
115
118
|
},
|
|
116
119
|
"devDependencies": {
|
|
120
|
+
"@cxres/structured-headers": "^2.0.0-alpha.1-nesting.0",
|
|
117
121
|
"@solid/solid-auth-oidc": "0.3.0",
|
|
122
|
+
"c8": "^10.1.2",
|
|
118
123
|
"chai": "^4.4.1",
|
|
119
124
|
"chai-as-promised": "7.1.1",
|
|
120
125
|
"cross-env": "7.0.3",
|
|
@@ -124,8 +129,8 @@
|
|
|
124
129
|
"mocha": "^10.3.0",
|
|
125
130
|
"nock": "^13.5.4",
|
|
126
131
|
"node-mocks-http": "^1.14.1",
|
|
127
|
-
"nyc": "15.1.0",
|
|
128
132
|
"pre-commit": "1.2.2",
|
|
133
|
+
"prep-fetch": "^0.1.0",
|
|
129
134
|
"randombytes": "2.1.0",
|
|
130
135
|
"sinon": "12.0.1",
|
|
131
136
|
"sinon-chai": "3.7.0",
|
|
@@ -141,12 +146,12 @@
|
|
|
141
146
|
"main": "index.js",
|
|
142
147
|
"scripts": {
|
|
143
148
|
"build": "echo nothing to build",
|
|
144
|
-
"solid": "node ./bin/solid",
|
|
145
|
-
"standard": "standard
|
|
149
|
+
"solid": "node --experimental-require-module ./bin/solid",
|
|
150
|
+
"standard": "standard \"{bin,examples,lib,test}/**/*.js\"",
|
|
146
151
|
"validate": "node ./test/validate-turtle.js",
|
|
147
|
-
"
|
|
148
|
-
"mocha": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/ test/unit/",
|
|
149
|
-
"mocha-integration": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/http-test.js",
|
|
152
|
+
"c8": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 c8 --reporter=text-summary mocha -n experimental-require-module --recursive test/integration/ test/unit/",
|
|
153
|
+
"mocha": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha -n experimental-require-module --recursive test/integration/ test/unit/",
|
|
154
|
+
"mocha-integration": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha -n experimental-require-module --recursive test/integration/http-test.js",
|
|
150
155
|
"mocha-account-creation-oidc": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/account-creation-oidc-test.js",
|
|
151
156
|
"mocha-account-manager": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/account-manager-test.js",
|
|
152
157
|
"mocha-account-template": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/account-template-test.js",
|
|
@@ -156,11 +161,11 @@
|
|
|
156
161
|
"mocha-ldp": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/ldp-test.js",
|
|
157
162
|
"prepublishOnly": "npm test",
|
|
158
163
|
"postpublish": "git push --follow-tags",
|
|
159
|
-
"test": "npm run standard && npm run validate && npm run
|
|
164
|
+
"test": "npm run standard && npm run validate && npm run c8",
|
|
160
165
|
"clean": "rimraf config/templates config/views",
|
|
161
166
|
"reset": "rimraf .db data && npm run clean"
|
|
162
167
|
},
|
|
163
|
-
"
|
|
168
|
+
"c8": {
|
|
164
169
|
"reporter": [
|
|
165
170
|
"html",
|
|
166
171
|
"text-summary"
|
|
@@ -174,13 +179,15 @@
|
|
|
174
179
|
"before",
|
|
175
180
|
"beforeEach",
|
|
176
181
|
"describe",
|
|
177
|
-
"it"
|
|
182
|
+
"it",
|
|
183
|
+
"fetch",
|
|
184
|
+
"AbortController"
|
|
178
185
|
]
|
|
179
186
|
},
|
|
180
187
|
"bin": {
|
|
181
188
|
"solid": "bin/solid"
|
|
182
189
|
},
|
|
183
190
|
"engines": {
|
|
184
|
-
"node": ">=
|
|
191
|
+
"node": ">=20.17.0 <21 || >=22.8.0"
|
|
185
192
|
}
|
|
186
193
|
}
|