solid-server 5.7.10 → 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/common/js/index-buttons.js +2 -2
- 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 +8 -19
- package/lib/models/account-manager.js +0 -2
- package/lib/rdf-notification-template.js +66 -0
- package/lib/utils.js +1 -1
- package/package.json +26 -12
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
|
@@ -30,7 +30,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
30
30
|
// HIDE LOGIN BUTTON, ADD REGISTER BUTTON
|
|
31
31
|
else {
|
|
32
32
|
let loginArea = document.getElementById('loginStatusArea');
|
|
33
|
-
let html = `<input type="button" onclick="window.location.href='/register'" value="Register to get a Pod" class="register-button">`
|
|
33
|
+
let html = `<input type="button" onclick="window.location.href='/register'" value="Register to get a Pod" class="register-button" style="padding: 1em; border-radius:0.2em; font-size: 100%;margin: 0.75em 0 0.75em 0.5em !important; padding: 0.5em !important;background-color: #efe;">`
|
|
34
34
|
let span = document.createElement("span")
|
|
35
35
|
span.innerHTML = html
|
|
36
36
|
loginArea.appendChild(span);
|
|
@@ -41,4 +41,4 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
41
41
|
signUpButton.style.display = "none";
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
})
|
|
44
|
+
})
|
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
|
@@ -103,7 +103,6 @@ class LDP {
|
|
|
103
103
|
|
|
104
104
|
async listContainer (container, reqUri, containerData, hostname) {
|
|
105
105
|
const resourceGraph = $rdf.graph()
|
|
106
|
-
|
|
107
106
|
try {
|
|
108
107
|
$rdf.parse(containerData, resourceGraph, reqUri, 'text/turtle')
|
|
109
108
|
} catch (err) {
|
|
@@ -220,19 +219,16 @@ class LDP {
|
|
|
220
219
|
|
|
221
220
|
async put (url, stream, contentType) {
|
|
222
221
|
const container = (url.url || url).endsWith('/')
|
|
223
|
-
|
|
224
222
|
// PUT without content type is forbidden, unless PUTting container
|
|
225
223
|
if (!contentType && !container) {
|
|
226
224
|
throw error(400,
|
|
227
225
|
'PUT request requires a content-type via the Content-Type header')
|
|
228
226
|
}
|
|
229
|
-
|
|
230
227
|
// reject resource with percent-encoded $ extension
|
|
231
228
|
const dollarExtensionRegex = /%(?:24)\.[^%(?:24)]*$/ // /\$\.[^$]*$/
|
|
232
229
|
if ((url.url || url).match(dollarExtensionRegex)) {
|
|
233
230
|
throw error(400, 'Resource with a $.ext is not allowed by the server')
|
|
234
231
|
}
|
|
235
|
-
|
|
236
232
|
// First check if we are above quota
|
|
237
233
|
let isOverQuota
|
|
238
234
|
// Someone had a reason to make url actually a req sometimes but not
|
|
@@ -246,34 +242,27 @@ class LDP {
|
|
|
246
242
|
if (isOverQuota) {
|
|
247
243
|
throw error(413, 'User has exceeded their storage quota')
|
|
248
244
|
}
|
|
245
|
+
// Set url using folder/.meta
|
|
246
|
+
let { path } = await this.resourceMapper.mapUrlToFile({
|
|
247
|
+
url,
|
|
248
|
+
contentType,
|
|
249
|
+
createIfNotExists: true,
|
|
250
|
+
searchIndex: false
|
|
251
|
+
})
|
|
249
252
|
|
|
250
|
-
|
|
251
|
-
if (container) {
|
|
252
|
-
if (typeof url !== 'string') {
|
|
253
|
-
url.url = url.url + suffixMeta
|
|
254
|
-
} else {
|
|
255
|
-
url = url + suffixMeta
|
|
256
|
-
}
|
|
257
|
-
contentType = 'text/turtle'
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const { path } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true })
|
|
253
|
+
if (container) { path += suffixMeta }
|
|
261
254
|
// debug.handlers(container + ' item ' + (url.url || url) + ' ' + contentType + ' ' + path)
|
|
262
255
|
// check if file exists, and in that case that it has the same extension
|
|
263
256
|
if (!container) { await this.checkFileExtension(url, path) }
|
|
264
|
-
|
|
265
257
|
// Create the enclosing directory, if necessary, do not create pubsub if PUT create container
|
|
266
258
|
await this.createDirectory(path, hostname, !container)
|
|
267
|
-
|
|
268
259
|
// clear cache
|
|
269
260
|
if (path.endsWith(this.suffixAcl)) {
|
|
270
261
|
const { url: aclUrl } = await this.resourceMapper.mapFileToUrl({ path, hostname })
|
|
271
262
|
clearAclCache(aclUrl)
|
|
272
263
|
// clearAclCache()
|
|
273
264
|
}
|
|
274
|
-
|
|
275
265
|
// Directory created, now write the file
|
|
276
|
-
if (container) return
|
|
277
266
|
return withLock(path, () => new Promise((resolve, reject) => {
|
|
278
267
|
// HACK: the middleware in webid-oidc.js uses body-parser, thus ending the stream of data
|
|
279
268
|
// for JSON bodies. So, the stream needs to be reset
|
|
@@ -89,12 +89,10 @@ class AccountManager {
|
|
|
89
89
|
try {
|
|
90
90
|
accountUri = this.accountUriFor(accountName)
|
|
91
91
|
accountUri = url.parse(accountUri).hostname
|
|
92
|
-
|
|
93
92
|
cardPath = url.resolve('/', this.pathCard)
|
|
94
93
|
} catch (err) {
|
|
95
94
|
return Promise.reject(err)
|
|
96
95
|
}
|
|
97
|
-
|
|
98
96
|
return this.accountUriExists(accountUri, cardPath)
|
|
99
97
|
}
|
|
100
98
|
|
|
@@ -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/lib/utils.js
CHANGED
|
@@ -201,7 +201,7 @@ async function getQuota (root, serverUri) {
|
|
|
201
201
|
return Infinity
|
|
202
202
|
}
|
|
203
203
|
const graph = $rdf.graph()
|
|
204
|
-
const storageUri = serverUri + '/'
|
|
204
|
+
const storageUri = serverUri.endsWith('/') ? serverUri : serverUri + '/'
|
|
205
205
|
try {
|
|
206
206
|
$rdf.parse(prefs, graph, storageUri, 'text/turtle')
|
|
207
207
|
} catch (error) {
|
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",
|
|
@@ -89,7 +92,7 @@
|
|
|
89
92
|
"ip-range-check": "0.2.0",
|
|
90
93
|
"is-ip": "^3.1.0",
|
|
91
94
|
"li": "^1.3.0",
|
|
92
|
-
"mashlib": "^1.8.
|
|
95
|
+
"mashlib": "^1.8.11",
|
|
93
96
|
"mime-types": "^2.1.35",
|
|
94
97
|
"negotiator": "^0.6.3",
|
|
95
98
|
"node-fetch": "^2.7.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,19 +146,26 @@
|
|
|
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",
|
|
155
|
+
"mocha-account-creation-oidc": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/account-creation-oidc-test.js",
|
|
156
|
+
"mocha-account-manager": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/account-manager-test.js",
|
|
157
|
+
"mocha-account-template": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/account-template-test.js",
|
|
158
|
+
"mocha-acl-oidc": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/acl-oidc-test.js",
|
|
159
|
+
"mocha-authentication-oidc": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/authentication-oidc-test.js",
|
|
160
|
+
"mocha-header": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/header-test.js",
|
|
161
|
+
"mocha-ldp": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha --recursive test/integration/ldp-test.js",
|
|
150
162
|
"prepublishOnly": "npm test",
|
|
151
163
|
"postpublish": "git push --follow-tags",
|
|
152
|
-
"test": "npm run standard && npm run validate && npm run
|
|
164
|
+
"test": "npm run standard && npm run validate && npm run c8",
|
|
153
165
|
"clean": "rimraf config/templates config/views",
|
|
154
166
|
"reset": "rimraf .db data && npm run clean"
|
|
155
167
|
},
|
|
156
|
-
"
|
|
168
|
+
"c8": {
|
|
157
169
|
"reporter": [
|
|
158
170
|
"html",
|
|
159
171
|
"text-summary"
|
|
@@ -167,13 +179,15 @@
|
|
|
167
179
|
"before",
|
|
168
180
|
"beforeEach",
|
|
169
181
|
"describe",
|
|
170
|
-
"it"
|
|
182
|
+
"it",
|
|
183
|
+
"fetch",
|
|
184
|
+
"AbortController"
|
|
171
185
|
]
|
|
172
186
|
},
|
|
173
187
|
"bin": {
|
|
174
188
|
"solid": "bin/solid"
|
|
175
189
|
},
|
|
176
190
|
"engines": {
|
|
177
|
-
"node": ">=
|
|
191
|
+
"node": ">=20.17.0 <21 || >=22.8.0"
|
|
178
192
|
}
|
|
179
193
|
}
|