mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/next-major
This commit is contained in:
commit
4f17f2baac
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -14,7 +14,7 @@ updates:
|
||||
interval: "daily"
|
||||
time: "03:35"
|
||||
timezone: "Europe/Brussels"
|
||||
target-branch: "versions/6.0.0"
|
||||
target-branch: "versions/next-major"
|
||||
ignore:
|
||||
# Ignore minor and patch version updates
|
||||
- dependency-name: "*"
|
||||
|
2
.github/workflows/cth-test.yml
vendored
2
.github/workflows/cth-test.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
||||
with:
|
||||
node-version: 16.x
|
||||
- name: Check out the project
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
- name: Install dependencies and run build scripts
|
||||
|
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
tags: ${{ steps.meta-main.outputs.tags || steps.meta-version.outputs.tags }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')
|
||||
name: Docker meta edge and version tag
|
||||
id: meta-main
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
|
6
.github/workflows/mkdocs.yml
vendored
6
.github/workflows/mkdocs.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
outputs:
|
||||
major: ${{ steps.tagged_version.outputs.major || steps.current_version.outputs.major }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- uses: actions/checkout@v3.5.3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: mkdocs-prep
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- uses: actions/checkout@v3.5.3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
@ -63,7 +63,7 @@ jobs:
|
||||
needs: [mkdocs-prep, mkdocs]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- uses: actions/checkout@v3.5.3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
|
10
.github/workflows/npm-test.yml
vendored
10
.github/workflows/npm-test.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
- uses: actions/checkout@v3.5.3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
@ -38,7 +38,7 @@ jobs:
|
||||
- name: Ensure line endings are consistent
|
||||
run: git config --global core.autocrlf input
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Install dependencies and run build scripts
|
||||
run: npm ci
|
||||
- name: Type-check tests
|
||||
@ -81,7 +81,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Install dependencies and run build scripts
|
||||
run: npm ci
|
||||
- name: Run integration tests
|
||||
@ -105,7 +105,7 @@ jobs:
|
||||
- name: Ensure line endings are consistent
|
||||
run: git config --global core.autocrlf input
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Install dependencies and run build scripts
|
||||
run: npm ci
|
||||
- name: Run integration tests
|
||||
@ -127,7 +127,7 @@ jobs:
|
||||
with:
|
||||
node-version: '16.x'
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Install dependencies and run build scripts
|
||||
run: npm ci
|
||||
- name: Run deploy tests
|
||||
|
2
.github/workflows/schedule.yml
vendored
2
.github/workflows/schedule.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
matrix:
|
||||
branch:
|
||||
- 'main'
|
||||
- 'versions/6.0.0'
|
||||
- 'versions/next-major'
|
||||
uses: ./.github/workflows/cth-test.yml
|
||||
with:
|
||||
branch: ${{ matrix.branch }}
|
||||
|
@ -3,6 +3,14 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [6.0.1](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v6.0.0...v6.0.1) (2023-06-15)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Use correct type for Webhook notifications ([c0a881b](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/c0a881b9809d3a551c4cdf63bbd89ce57f3fff8d))
|
||||
* Make root storage subject of storage description ([9584ab7](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/9584ab7549ecf7ab20fe1e6db28f3c900d9a5392))
|
||||
* Prevent illegal file paths from being generated ([fdee4b3](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/fdee4b334fa456746e9d2097284321a6c1fa2362))
|
||||
|
||||
## [6.0.0](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v6.0.0-alpha.0...v6.0.0) (2023-05-02)
|
||||
|
||||
### Features
|
||||
|
@ -1,3 +1,9 @@
|
||||
# Code of conduct
|
||||
# Code of Conduct
|
||||
|
||||
We follow and adhere to the Solid [Code of Conduct](https://github.com/solid/process/blob/main/code-of-conduct.md).
|
||||
For our Code of Conduct, we follow and adhere to the Solid [Code of Conduct](https://github.com/solid/process/blob/main/code-of-conduct.md),
|
||||
but with a different Committee, which should be contacted in the case of violations.
|
||||
|
||||
The Committee consists of the following people:
|
||||
|
||||
* Joachim Van Herwegen <joachim.vanherwegen@ugent.be>
|
||||
* Ruben Verborgh <ruben.verborgh@ugent.be>
|
||||
|
@ -12,6 +12,7 @@
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:Middleware" },
|
||||
{
|
||||
"@id": "urn:solid-server:default:BaseHttpHandler",
|
||||
"@type": "WaterfallHandler",
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:StaticAssetHandler" },
|
||||
|
@ -11,6 +11,7 @@
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:Middleware" },
|
||||
{
|
||||
"@id": "urn:solid-server:default:BaseHttpHandler",
|
||||
"@type": "WaterfallHandler",
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:StaticAssetHandler" },
|
||||
|
@ -2,10 +2,10 @@
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Handles the generation and serialization of notifications for WebHookChannel2023.",
|
||||
"comment": "Handles the generation and serialization of notifications for WebhookChannel2023.",
|
||||
"@id": "urn:solid-server:default:WebHookNotificationHandler",
|
||||
"@type": "TypedNotificationHandler",
|
||||
"type": "http://www.w3.org/ns/solid/notifications#WebHookChannel2023",
|
||||
"type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023",
|
||||
"source": {
|
||||
"@type": "ComposedNotificationHandler",
|
||||
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
||||
@ -14,7 +14,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "Emits serialized notifications through HTTP requests to the WebHook.",
|
||||
"comment": "Emits serialized notifications through HTTP requests to the Webhook.",
|
||||
"@id": "urn:solid-server:default:WebHookEmitter",
|
||||
"@type": "WebHookEmitter",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
|
@ -5,7 +5,7 @@
|
||||
"@id": "urn:solid-server:default:WebHookRoute",
|
||||
"@type": "RelativePathInteractionRoute",
|
||||
"base": { "@id": "urn:solid-server:default:NotificationRoute" },
|
||||
"relativePath": "/WebHookChannel2023/"
|
||||
"relativePath": "/WebhookChannel2023/"
|
||||
},
|
||||
{
|
||||
"@id": "urn:solid-server:default:WebHookWebIdRoute",
|
||||
@ -15,11 +15,11 @@
|
||||
},
|
||||
|
||||
{
|
||||
"comment": "Handles the WebHookChannel2023 WebID.",
|
||||
"comment": "Handles the WebhookChannel2023 WebID.",
|
||||
"@id": "urn:solid-server:default:WebHookWebId",
|
||||
"@type": "OperationRouterHandler",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"allowedPathNames": [ "/WebHookChannel2023/webId$" ],
|
||||
"allowedPathNames": [ "/WebhookChannel2023/webId$" ],
|
||||
"handler": {
|
||||
"@type": "WebHookWebId",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||
|
@ -2,12 +2,12 @@
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Handles the subscriptions targeting a WebHookChannel2023.",
|
||||
"comment": "Handles the subscriptions targeting a WebhookChannel2023.",
|
||||
"@id": "urn:solid-server:default:WebHookRouter",
|
||||
"@type": "OperationRouterHandler",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||
"allowedMethods": [ "HEAD", "GET", "POST" ],
|
||||
"allowedPathNames": [ "/WebHookChannel2023/$" ],
|
||||
"allowedPathNames": [ "/WebhookChannel2023/$" ],
|
||||
"handler": {
|
||||
"@id": "urn:solid-server:default:WebHookSubscriber",
|
||||
"@type": "NotificationSubscriber",
|
||||
@ -20,7 +20,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "Contains all the metadata relevant for a WebHookChannel2023.",
|
||||
"comment": "Contains all the metadata relevant for a WebhookChannel2023.",
|
||||
"@id": "urn:solid-server:default:WebHookChannel2023Type",
|
||||
"@type": "WebhookChannel2023Type",
|
||||
"route": { "@id": "urn:solid-server:default:WebHookRoute" },
|
||||
|
@ -38,6 +38,7 @@
|
||||
"comment": "A server that stores its resources on disk while enforcing quota."
|
||||
},
|
||||
{
|
||||
"comment": "Sets the maximum size of a single pod to 7KB.",
|
||||
"@id": "urn:solid-server:default:QuotaStrategy",
|
||||
"@type": "PodQuotaStrategy",
|
||||
"limit_amount": 7000,
|
||||
|
@ -2,13 +2,17 @@
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Converts many RDF serialization to Quad objects.",
|
||||
"comment": "Converts many RDF serialization to Quad objects. Caching notification related contexts to prevent too many requests.",
|
||||
"@id": "urn:solid-server:default:RdfToQuadConverter",
|
||||
"@type": "RdfToQuadConverter",
|
||||
"contexts": [
|
||||
{
|
||||
"RdfToQuadConverter:_contexts_key": "https://www.w3.org/ns/solid/notification/v1",
|
||||
"RdfToQuadConverter:_contexts_value": "@css:templates/contexts/notification.jsonld"
|
||||
},
|
||||
{
|
||||
"RdfToQuadConverter:_contexts_key": "https://www.w3.org/ns/activitystreams",
|
||||
"RdfToQuadConverter:_contexts_value": "@css:templates/contexts/activitystreams.jsonld"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
# Releasing a new version
|
||||
# Releasing a new major version
|
||||
|
||||
This is only relevant if you are a developer with push access responsible for doing a new release.
|
||||
|
||||
Steps to follow:
|
||||
|
||||
* Merge `main` into `versions/x.0.0`.
|
||||
* Merge `main` into `versions/next-major`.
|
||||
* Verify if there are issues when upgrading an existing installation to the new version.
|
||||
* Can the data still be accessed?
|
||||
* Does authentication still work?
|
||||
@ -15,7 +15,7 @@ Steps to follow:
|
||||
* Automatically updates Components.js references to the new version.
|
||||
Committed with `chore(release): Update configs to vx.0.0`.
|
||||
* Updates the `package.json`, and generates the new entries in `CHANGELOG.md`.
|
||||
Commited with `chore(release): Release version vx.0.0 of the npm package`
|
||||
Commits with `chore(release): Release version vx.0.0 of the npm package`
|
||||
* Optionally run `npx commit-and-tag-version -r major --dry-run` to preview the commands that will be run
|
||||
and the changes to `CHANGELOG.md`.
|
||||
* The `postrelease` script will now prompt you to manually edit the `CHANGELOG.md`.
|
||||
@ -24,19 +24,25 @@ Steps to follow:
|
||||
Documentation can be removed.
|
||||
* Press any key in your terminal when your changes are ready.
|
||||
* The `postrelease` script will amend the release commit, create an annotated tag and push changes to origin.
|
||||
* Merge `versions/x.0.0` into `main` and push.
|
||||
* Merge `versions/next-major` into `main` and push.
|
||||
* Do a GitHub release.
|
||||
* `npm publish`
|
||||
* Check if there is a `next` tag that needs to be replaced.
|
||||
* `npm dist-tag add @solid/community-server@x.0.0 next`
|
||||
* Rename the `versions/x.0.0` branch to the next version.
|
||||
* Update `.github/workflows/schedule.yml` and `.github/dependabot.yml` to point at the new branch.
|
||||
* Potentially upgrade dependent repositories:
|
||||
* Recipes at <https://github.com/CommunitySolidServer/recipes/>
|
||||
* Tutorials at <https://github.com/CommunitySolidServer/tutorials/>
|
||||
* Generator at <https://github.com/CommunitySolidServer/configuration-generator/>
|
||||
* Hello world component at <https://github.com/CommunitySolidServer/hello-world-component/>
|
||||
|
||||
Changes when doing a pre-release of a major version:
|
||||
## Changes when doing a pre-release
|
||||
|
||||
* Version with `npm run release -- -r major --prerelease alpha`
|
||||
* Do not merge `versions/x.0.0` into `main`.
|
||||
* Do not merge `versions/next-major` into `main`.
|
||||
* Publish with `npm publish --tag next`.
|
||||
* Do not update the branch or anything related.
|
||||
|
||||
## Changes when doing a minor release
|
||||
|
||||
* Version with `npm run release -- -r minor`
|
||||
* Do not merge `versions/next-major` into `main`.
|
||||
|
@ -27,7 +27,7 @@ Doing a GET to `http://localhost:3000/.well-known/solid` then gives the followin
|
||||
<http://localhost:3000/.well-known/solid>
|
||||
a <http://www.w3.org/ns/pim/space#Storage> ;
|
||||
notify:subscription <http://localhost:3000/.notifications/WebSocketChannel2023/> ,
|
||||
<http://localhost:3000/.notifications/WebHookChannel2023/> .
|
||||
<http://localhost:3000/.notifications/WebhookChannel2023/> .
|
||||
<http://localhost:3000/.notifications/WebSocketChannel2023/>
|
||||
notify:channelType notify:WebSocketChannel2023 ;
|
||||
notify:feature notify:accept ,
|
||||
@ -35,8 +35,8 @@ Doing a GET to `http://localhost:3000/.well-known/solid` then gives the followin
|
||||
notify:rate ,
|
||||
notify:startAt ,
|
||||
notify:state .
|
||||
<http://localhost:3000/.notifications/WebSocketChannel2023/>
|
||||
notify:channelType notify:WebHookChannel2023;
|
||||
<http://localhost:3000/.notifications/WebhookChannel2023/>
|
||||
notify:channelType notify:WebhookChannel2023;
|
||||
notify:feature notify:accept ,
|
||||
notify:endAt ,
|
||||
notify:rate ,
|
||||
@ -61,7 +61,7 @@ Requests without `Read` permission will be rejected.
|
||||
|
||||
There are currently up to two supported ways to get notifications in CSS, depending on your configuration:
|
||||
the notification channel types [`WebSocketChannel2023`](https://solid.github.io/notifications/websocket-channel-2023);
|
||||
and [`WebHookChannel2023`](https://solid.github.io/notifications/webhook-channel-2023).
|
||||
and [`WebhookChannel2023`](https://solid.github.io/notifications/webhook-channel-2023).
|
||||
|
||||
### WebSockets
|
||||
|
||||
@ -98,30 +98,30 @@ const ws = new WebSocket(receiveFrom);
|
||||
ws.on('message', (notification) => console.log(notification));
|
||||
```
|
||||
|
||||
### WebHooks
|
||||
### Webhooks
|
||||
|
||||
Similar to the WebSocket subscription, below is sample JSON-LD
|
||||
that would be sent to `http://localhost:3000/.notifications/WebHookChannel2023/`:
|
||||
that would be sent to `http://localhost:3000/.notifications/WebhookChannel2023/`:
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": [ "https://www.w3.org/ns/solid/notification/v1" ],
|
||||
"type": "http://www.w3.org/ns/solid/notifications#WebHookChannel2023",
|
||||
"type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023",
|
||||
"topic": "http://localhost:3000/foo",
|
||||
"sendTo": "https://example.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
Note that this document has an additional `sendTo` field.
|
||||
This is the WebHook URL of your server, the URL to which you want the notifications to be sent.
|
||||
This is the Webhook URL of your server, the URL to which you want the notifications to be sent.
|
||||
|
||||
The response would then be something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": [ "https://www.w3.org/ns/solid/notification/v1" ],
|
||||
"id": "http://localhost:3000/.notifications/WebHookChannel2023/eeaf2c17-699a-4e53-8355-e91d13807e5f",
|
||||
"type": "http://www.w3.org/ns/solid/notifications#WebHookChannel2023",
|
||||
"id": "http://localhost:3000/.notifications/WebhookChannel2023/eeaf2c17-699a-4e53-8355-e91d13807e5f",
|
||||
"type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023",
|
||||
"topic": "http://localhost:3000/foo",
|
||||
"sendTo": "https://example.com/webhook"
|
||||
}
|
||||
|
@ -39,7 +39,8 @@ module.exports = {
|
||||
'js',
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
setupFilesAfterEnv: [ 'jest-rdf', '<rootDir>/test/util/SetupTests.ts' ],
|
||||
globalSetup: '<rootDir>/test/util/SetupTests.ts',
|
||||
setupFilesAfterEnv: [ 'jest-rdf' ],
|
||||
collectCoverage: false,
|
||||
// See https://github.com/matthieubosquet/ts-dpop/issues/13
|
||||
moduleNameMapper: {
|
||||
|
16
package-lock.json
generated
16
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@solid/community-server",
|
||||
"version": "6.0.0",
|
||||
"version": "6.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@solid/community-server",
|
||||
"version": "6.0.0",
|
||||
"version": "6.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@comunica/context-entries": "^2.6.8",
|
||||
@ -15179,9 +15179,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -27505,9 +27505,9 @@
|
||||
}
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true
|
||||
},
|
||||
"wordwrap": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@solid/community-server",
|
||||
"version": "6.0.0",
|
||||
"version": "6.0.1",
|
||||
"description": "Community Solid Server: an open and modular implementation of the Solid specifications",
|
||||
"keywords": [
|
||||
"solid",
|
||||
|
@ -50,10 +50,14 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
|
||||
// POST is only allowed on containers.
|
||||
// Metadata only has the resource URI in case it has resource metadata.
|
||||
if (this.isPostAllowed(metadata)) {
|
||||
if (!this.isPostAllowed(metadata)) {
|
||||
allowedMethods.delete('POST');
|
||||
}
|
||||
|
||||
if (!this.isPutAllowed(metadata)) {
|
||||
allowedMethods.delete('PUT');
|
||||
}
|
||||
|
||||
if (!this.isDeleteAllowed(metadata)) {
|
||||
allowedMethods.delete('DELETE');
|
||||
}
|
||||
@ -76,7 +80,14 @@ export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
* otherwise it is just a blank node.
|
||||
*/
|
||||
private isPostAllowed(metadata: RepresentationMetadata): boolean {
|
||||
return metadata.has(RDF.terms.type, LDP.terms.Resource) && !isContainerPath(metadata.identifier.value);
|
||||
return !metadata.has(RDF.terms.type, LDP.terms.Resource) || isContainerPath(metadata.identifier.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT is not allowed on existing containers.
|
||||
*/
|
||||
private isPutAllowed(metadata: RepresentationMetadata): boolean {
|
||||
return !metadata.has(RDF.terms.type, LDP.terms.Resource) || !isContainerPath(metadata.identifier.value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { OkResponseDescription } from '../../http/output/response/OkResponseDescription';
|
||||
import type { ResponseDescription } from '../../http/output/response/ResponseDescription';
|
||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import type { ResourceStore } from '../../storage/ResourceStore';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
|
||||
@ -32,7 +33,7 @@ export class StorageDescriptionHandler extends OperationHttpHandler {
|
||||
if (method !== 'GET') {
|
||||
throw new MethodNotAllowedHttpError([ method ], `Only GET requests can target the storage description.`);
|
||||
}
|
||||
const container = { path: ensureTrailingSlash(target.path.slice(0, -this.path.length)) };
|
||||
const container = this.getStorageIdentifier(target);
|
||||
const representation = await this.store.getRepresentation(container, {});
|
||||
representation.data.destroy();
|
||||
if (!representation.metadata.has(RDF.terms.type, PIM.terms.Storage)) {
|
||||
@ -43,10 +44,17 @@ export class StorageDescriptionHandler extends OperationHttpHandler {
|
||||
}
|
||||
|
||||
public async handle({ operation: { target }}: OperationHttpHandlerInput): Promise<ResponseDescription> {
|
||||
const quads = await this.describer.handle(target);
|
||||
const quads = await this.describer.handle(this.getStorageIdentifier(target));
|
||||
|
||||
const representation = new BasicRepresentation(quads, INTERNAL_QUADS);
|
||||
|
||||
return new OkResponseDescription(representation.metadata, representation.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the identifier of the root storage based on the identifier of the root storage description resource.
|
||||
*/
|
||||
protected getStorageIdentifier(descriptionIdentifier: ResourceIdentifier): ResourceIdentifier {
|
||||
return { path: ensureTrailingSlash(descriptionIdentifier.path.slice(0, -this.path.length)) };
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export interface WebhookChannel2023 extends NotificationChannel {
|
||||
/**
|
||||
* The "WebHookChannel2023" type.
|
||||
*/
|
||||
type: typeof NOTIFY.WebHookChannel2023;
|
||||
type: typeof NOTIFY.WebhookChannel2023;
|
||||
/**
|
||||
* Where the notifications have to be sent.
|
||||
*/
|
||||
@ -22,7 +22,7 @@ export interface WebhookChannel2023 extends NotificationChannel {
|
||||
}
|
||||
|
||||
export function isWebHook2023Channel(channel: NotificationChannel): channel is WebhookChannel2023 {
|
||||
return channel.type === NOTIFY.WebHookChannel2023;
|
||||
return channel.type === NOTIFY.WebhookChannel2023;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,7 +47,7 @@ export class WebhookChannel2023Type extends BaseChannelType {
|
||||
*/
|
||||
public constructor(route: InteractionRoute, webIdRoute: InteractionRoute, stateHandler: StateHandler,
|
||||
features?: string[]) {
|
||||
super(NOTIFY.terms.WebHookChannel2023,
|
||||
super(NOTIFY.terms.WebhookChannel2023,
|
||||
route,
|
||||
features,
|
||||
[{ path: NOTIFY.sendTo, minCount: 1, maxCount: 1 }]);
|
||||
@ -62,7 +62,7 @@ export class WebhookChannel2023Type extends BaseChannelType {
|
||||
|
||||
return {
|
||||
...channel,
|
||||
type: NOTIFY.WebHookChannel2023,
|
||||
type: NOTIFY.WebhookChannel2023,
|
||||
sendTo: sendTo.value,
|
||||
};
|
||||
}
|
||||
|
@ -140,6 +140,9 @@ const mediaRange = new RegExp(`${tchar.source}+/${tchar.source}+`, 'u');
|
||||
* Replaces all double quoted strings in the input string with `"0"`, `"1"`, etc.
|
||||
* @param input - The Accept header string.
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown if invalid characters are detected in a quoted string.
|
||||
*
|
||||
* @returns The transformed string and a map with keys `"0"`, etc. and values the original string that was there.
|
||||
*/
|
||||
export function transformQuotedStrings(input: string): { result: string; replacements: Record<string, string> } {
|
||||
@ -163,6 +166,8 @@ export function transformQuotedStrings(input: string): { result: string; replace
|
||||
* Splits the input string on commas, trims all parts and filters out empty ones.
|
||||
*
|
||||
* @param input - Input header string.
|
||||
*
|
||||
* @returns An array of trimmed strings.
|
||||
*/
|
||||
export function splitAndClean(input: string): string[] {
|
||||
return input.split(',')
|
||||
@ -175,44 +180,67 @@ export function splitAndClean(input: string): string[] {
|
||||
*
|
||||
* @param qvalue - Input qvalue string (so "q=....").
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown on invalid syntax.
|
||||
* @returns true if q value is valid, false otherwise.
|
||||
*/
|
||||
function testQValue(qvalue: string): void {
|
||||
if (!/^(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue)) {
|
||||
logger.warn(`Invalid q value: ${qvalue}`);
|
||||
throw new BadRequestHttpError(
|
||||
`Invalid q value: ${qvalue} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`,
|
||||
);
|
||||
function isValidQValue(qvalue: string): boolean {
|
||||
return /^(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a qvalue to a number.
|
||||
* Returns 1 if the value is not a valid number or 1 if it is more than 1.
|
||||
* Returns 0 if the value is negative.
|
||||
* Otherwise, the parsed value is returned.
|
||||
*
|
||||
* @param qvalue - Value to convert.
|
||||
*/
|
||||
function parseQValue(qvalue: string): number {
|
||||
const result = Number(qvalue);
|
||||
if (Number.isNaN(result) || result >= 1) {
|
||||
return 1;
|
||||
}
|
||||
if (result < 0) {
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning to indicate there was an invalid value.
|
||||
* Throws a {@link BadRequestHttpError} in case `strict` is `true`.
|
||||
*
|
||||
* @param message - Message to log and potentially put in the error.
|
||||
* @param strict - `true` if an error needs to be thrown.
|
||||
*/
|
||||
function handleInvalidValue(message: string, strict: boolean): void | never {
|
||||
logger.warn(message);
|
||||
if (strict) {
|
||||
throw new BadRequestHttpError(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of split parameters and checks their validity.
|
||||
* Parses a list of split parameters and checks their validity. Parameters with invalid
|
||||
* syntax are ignored and not returned.
|
||||
*
|
||||
* @param parameters - A list of split parameters (token [ "=" ( token / quoted-string ) ])
|
||||
* @param replacements - The double quoted strings that need to be replaced.
|
||||
*
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown on invalid parameter syntax.
|
||||
* @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`.
|
||||
*
|
||||
* @returns An array of name/value objects corresponding to the parameters.
|
||||
*/
|
||||
export function parseParameters(parameters: string[], replacements: Record<string, string>):
|
||||
export function parseParameters(parameters: string[], replacements: Record<string, string>, strict = false):
|
||||
{ name: string; value: string }[] {
|
||||
return parameters.map((param): { name: string; value: string } => {
|
||||
return parameters.reduce<{ name: string; value: string }[]>((acc, param): { name: string; value: string }[] => {
|
||||
const [ name, rawValue ] = param.split('=').map((str): string => str.trim());
|
||||
|
||||
// Test replaced string for easier check
|
||||
// parameter = token "=" ( token / quoted-string )
|
||||
// second part is optional for certain parameters
|
||||
if (!(token.test(name) && (!rawValue || /^"\d+"$/u.test(rawValue) || token.test(rawValue)))) {
|
||||
logger.warn(`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue}`);
|
||||
throw new BadRequestHttpError(
|
||||
`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` +
|
||||
`does not match (token ( "=" ( token / quoted-string ))?). `,
|
||||
);
|
||||
handleInvalidValue(`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` +
|
||||
`does not match (token ( "=" ( token / quoted-string ))?). `, strict);
|
||||
return acc;
|
||||
}
|
||||
|
||||
let value = rawValue;
|
||||
@ -220,8 +248,9 @@ export function parseParameters(parameters: string[], replacements: Record<strin
|
||||
value = replacements[rawValue];
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
});
|
||||
acc.push({ name, value });
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,24 +258,24 @@ export function parseParameters(parameters: string[], replacements: Record<strin
|
||||
* For every parameter value that is a double quoted string,
|
||||
* we check if it is a key in the replacements map.
|
||||
* If yes the value from the map gets inserted instead.
|
||||
* Invalid q values and parameter values are ignored and not returned.
|
||||
*
|
||||
* @param part - A string corresponding to a media range and its corresponding parameters.
|
||||
* @param replacements - The double quoted strings that need to be replaced.
|
||||
* @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`.
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown on invalid type, qvalue or parameter syntax.
|
||||
*
|
||||
* @returns {@link Accept} object corresponding to the header string.
|
||||
* @returns {@link Accept | undefined} object corresponding to the header string, or
|
||||
* undefined if an invalid type or sub-type is detected.
|
||||
*/
|
||||
function parseAcceptPart(part: string, replacements: Record<string, string>): Accept {
|
||||
function parseAcceptPart(part: string, replacements: Record<string, string>, strict: boolean): Accept | undefined {
|
||||
const [ range, ...parameters ] = part.split(';').map((param): string => param.trim());
|
||||
|
||||
// No reason to test differently for * since we don't check if the type exists
|
||||
if (!mediaRange.test(range)) {
|
||||
logger.warn(`Invalid Accept range: ${range}`);
|
||||
throw new BadRequestHttpError(
|
||||
`Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`,
|
||||
handleInvalidValue(
|
||||
`Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`, strict,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let weight = 1;
|
||||
@ -258,13 +287,16 @@ function parseAcceptPart(part: string, replacements: Record<string, string>): Ac
|
||||
if (name === 'q') {
|
||||
// Extension parameters appear after the q value
|
||||
map = extensionParams;
|
||||
testQValue(value);
|
||||
weight = Number.parseFloat(value);
|
||||
if (!isValidQValue(value)) {
|
||||
handleInvalidValue(`Invalid q value for range ${range}: ${value
|
||||
} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict);
|
||||
}
|
||||
weight = parseQValue(value);
|
||||
} else {
|
||||
if (!value && map !== extensionParams) {
|
||||
logger.warn(`Invalid Accept parameter ${name}`);
|
||||
throw new BadRequestHttpError(`Invalid Accept parameter ${name}: ` +
|
||||
`Accept parameter values are not optional when preceding the q value`);
|
||||
handleInvalidValue(`Invalid Accept parameter ${name}: ` +
|
||||
`Accept parameter values are not optional when preceding the q value`, strict);
|
||||
return;
|
||||
}
|
||||
map[name] = value || '';
|
||||
}
|
||||
@ -282,14 +314,13 @@ function parseAcceptPart(part: string, replacements: Record<string, string>): Ac
|
||||
|
||||
/**
|
||||
* Parses an Accept-* header where each part is only a value and a weight, so roughly /.*(q=.*)?/ separated by commas.
|
||||
* The returned weights default to 1 if no q value is found or the q value is invalid.
|
||||
* @param input - Input header string.
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown on invalid qvalue syntax.
|
||||
* @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`.
|
||||
*
|
||||
* @returns An array of ranges and weights.
|
||||
*/
|
||||
function parseNoParameters(input: string): AcceptHeader[] {
|
||||
function parseNoParameters(input: string, strict = false): AcceptHeader[] {
|
||||
const parts = splitAndClean(input);
|
||||
|
||||
return parts.map((part): AcceptHeader => {
|
||||
@ -297,12 +328,15 @@ function parseNoParameters(input: string): AcceptHeader[] {
|
||||
const result = { range, weight: 1 };
|
||||
if (qvalue) {
|
||||
if (!qvalue.startsWith('q=')) {
|
||||
logger.warn(`Only q parameters are allowed in ${input}`);
|
||||
throw new BadRequestHttpError(`Only q parameters are allowed in ${input}`);
|
||||
handleInvalidValue(`Only q parameters are allowed in ${input}`, strict);
|
||||
return result;
|
||||
}
|
||||
const val = qvalue.slice(2);
|
||||
testQValue(val);
|
||||
result.weight = Number.parseFloat(val);
|
||||
if (!isValidQValue(val)) {
|
||||
handleInvalidValue(`Invalid q value for range ${range}: ${val
|
||||
} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict);
|
||||
}
|
||||
result.weight = parseQValue(val);
|
||||
}
|
||||
return result;
|
||||
}).sort((left, right): number => right.weight - left.weight);
|
||||
@ -314,17 +348,25 @@ function parseNoParameters(input: string): AcceptHeader[] {
|
||||
* Parses an Accept header string.
|
||||
*
|
||||
* @param input - The Accept header string.
|
||||
* @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`.
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown on invalid header syntax.
|
||||
*
|
||||
* @returns An array of {@link Accept} objects, sorted by weight.
|
||||
* @returns An array of {@link Accept} objects, sorted by weight. Accept parts
|
||||
* with invalid syntax are ignored and removed from the returned array.
|
||||
*/
|
||||
export function parseAccept(input: string): Accept[] {
|
||||
export function parseAccept(input: string, strict = false): Accept[] {
|
||||
// Quoted strings could prevent split from having correct results
|
||||
const { result, replacements } = transformQuotedStrings(input);
|
||||
|
||||
return splitAndClean(result)
|
||||
.map((part): Accept => parseAcceptPart(part, replacements))
|
||||
.reduce<Accept[]>((acc, part): Accept[] => {
|
||||
const partOrUndef = parseAcceptPart(part, replacements, strict);
|
||||
|
||||
if (partOrUndef !== undefined) {
|
||||
acc.push(partOrUndef);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((left, right): number => right.weight - left.weight);
|
||||
}
|
||||
|
||||
@ -332,70 +374,65 @@ export function parseAccept(input: string): Accept[] {
|
||||
* Parses an Accept-Charset header string.
|
||||
*
|
||||
* @param input - The Accept-Charset header string.
|
||||
* @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`.
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown on invalid header syntax.
|
||||
*
|
||||
* @returns An array of {@link AcceptCharset} objects, sorted by weight.
|
||||
* @returns An array of {@link AcceptCharset} objects, sorted by weight. Invalid ranges
|
||||
* are ignored and not returned.
|
||||
*/
|
||||
export function parseAcceptCharset(input: string): AcceptCharset[] {
|
||||
export function parseAcceptCharset(input: string, strict = false): AcceptCharset[] {
|
||||
const results = parseNoParameters(input);
|
||||
results.forEach((result): void => {
|
||||
return results.filter((result): boolean => {
|
||||
if (!token.test(result.range)) {
|
||||
logger.warn(`Invalid Accept-Charset range: ${result.range}`);
|
||||
throw new BadRequestHttpError(
|
||||
`Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`,
|
||||
handleInvalidValue(
|
||||
`Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`, strict,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Accept-Encoding header string.
|
||||
*
|
||||
* @param input - The Accept-Encoding header string.
|
||||
* @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`.
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown on invalid header syntax.
|
||||
*
|
||||
* @returns An array of {@link AcceptEncoding} objects, sorted by weight.
|
||||
* @returns An array of {@link AcceptEncoding} objects, sorted by weight. Invalid ranges
|
||||
* are ignored and not returned.
|
||||
*/
|
||||
export function parseAcceptEncoding(input: string): AcceptEncoding[] {
|
||||
export function parseAcceptEncoding(input: string, strict = false): AcceptEncoding[] {
|
||||
const results = parseNoParameters(input);
|
||||
results.forEach((result): void => {
|
||||
return results.filter((result): boolean => {
|
||||
if (!token.test(result.range)) {
|
||||
logger.warn(`Invalid Accept-Encoding range: ${result.range}`);
|
||||
throw new BadRequestHttpError(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`);
|
||||
handleInvalidValue(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`, strict);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Accept-Language header string.
|
||||
*
|
||||
* @param input - The Accept-Language header string.
|
||||
* @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`.
|
||||
*
|
||||
* @throws {@link BadRequestHttpError}
|
||||
* Thrown on invalid header syntax.
|
||||
*
|
||||
* @returns An array of {@link AcceptLanguage} objects, sorted by weight.
|
||||
* @returns An array of {@link AcceptLanguage} objects, sorted by weight. Invalid ranges
|
||||
* are ignored and not returned.
|
||||
*/
|
||||
export function parseAcceptLanguage(input: string): AcceptLanguage[] {
|
||||
export function parseAcceptLanguage(input: string, strict = false): AcceptLanguage[] {
|
||||
const results = parseNoParameters(input);
|
||||
results.forEach((result): void => {
|
||||
return results.filter((result): boolean => {
|
||||
// (1*8ALPHA *("-" 1*8alphanum)) / "*"
|
||||
if (result.range !== '*' && !/^[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*$/u.test(result.range)) {
|
||||
logger.warn(
|
||||
`Invalid Accept-Language range: ${result.range}`,
|
||||
);
|
||||
throw new BadRequestHttpError(
|
||||
`Invalid Accept-Language range: ${result.range} does not match ((1*8ALPHA *("-" 1*8alphanum)) / "*")`,
|
||||
handleInvalidValue(
|
||||
`Invalid Accept-Language range: ${result.range} does not match ((1*8ALPHA *("-" 1*8alphanum)) / "*")`, strict,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
@ -405,24 +442,21 @@ const rfc1123Date = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|
|
||||
* Parses an Accept-DateTime header string.
|
||||
*
|
||||
* @param input - The Accept-DateTime header string.
|
||||
* @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`.
|
||||
*
|
||||
* @returns An array with a single {@link AcceptDatetime} object.
|
||||
* @returns An array with a single {@link AcceptDatetime} object,
|
||||
* or an empty array if a range in an invalid format is detected.
|
||||
*/
|
||||
export function parseAcceptDateTime(input: string): AcceptDatetime[] {
|
||||
const results: AcceptDatetime[] = [];
|
||||
export function parseAcceptDateTime(input: string, strict = false): AcceptDatetime[] {
|
||||
const range = input.trim();
|
||||
if (range) {
|
||||
if (!rfc1123Date.test(range)) {
|
||||
logger.warn(
|
||||
`Invalid Accept-DateTime range: ${range}`,
|
||||
);
|
||||
throw new BadRequestHttpError(
|
||||
`Invalid Accept-DateTime range: ${range} does not match the RFC1123 format`,
|
||||
);
|
||||
}
|
||||
results.push({ range, weight: 1 });
|
||||
if (!range) {
|
||||
return [];
|
||||
}
|
||||
return results;
|
||||
if (!rfc1123Date.test(range)) {
|
||||
handleInvalidValue(`Invalid Accept-DateTime range: ${range} does not match the RFC1123 format`, strict);
|
||||
return [];
|
||||
}
|
||||
return [{ range, weight: 1 }];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -155,16 +155,31 @@ export function toCanonicalUriPath(path: string): string {
|
||||
encodeURIComponent(decodeURIComponent(part)));
|
||||
}
|
||||
|
||||
// Characters not allowed in a Windows file path
|
||||
const forbiddenSymbols = {
|
||||
'<': '%3C',
|
||||
'>': '%3E',
|
||||
':': '%3A',
|
||||
'"': '%22',
|
||||
'|': '%7C',
|
||||
'?': '%3F',
|
||||
// `*` does not get converted by `encodeUriComponent`
|
||||
'*': '%2A',
|
||||
} as const;
|
||||
const forbiddenRegex = new RegExp(`[${Object.keys(forbiddenSymbols).join('')}]`, 'ug');
|
||||
/**
|
||||
* This function is used when converting a URI to a file path. Decodes all components of a URI path,
|
||||
* with the exception of encoded slash characters, as this would lead to unexpected file locations
|
||||
* being targeted (resulting in erroneous behaviour of the file based backend).
|
||||
* Characters that would result in an illegal file path remain percent encoded.
|
||||
*
|
||||
* @param path - The path to decode the URI path components of.
|
||||
* @returns A decoded copy of the provided URI path (ignoring encoded slash characters).
|
||||
*/
|
||||
export function decodeUriPathComponents(path: string): string {
|
||||
return transformPathComponents(path, decodeURIComponent);
|
||||
return transformPathComponents(path, (part): string => decodeURIComponent(part)
|
||||
// The characters replaced below result in illegal Windows file paths so need to be encoded
|
||||
.replace(forbiddenRegex, (val): string => forbiddenSymbols[val as keyof typeof forbiddenSymbols]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -210,7 +210,7 @@ export const NOTIFY = createVocabulary('http://www.w3.org/ns/solid/notifications
|
||||
'topic',
|
||||
'webhookAuth',
|
||||
|
||||
'WebHookChannel2023',
|
||||
'WebhookChannel2023',
|
||||
'WebSocketChannel2023',
|
||||
);
|
||||
|
||||
|
379
templates/contexts/activitystreams.jsonld
Normal file
379
templates/contexts/activitystreams.jsonld
Normal file
@ -0,0 +1,379 @@
|
||||
{
|
||||
"@context": {
|
||||
"@vocab": "_:",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||
"as": "https://www.w3.org/ns/activitystreams#",
|
||||
"ldp": "http://www.w3.org/ns/ldp#",
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||
"id": "@id",
|
||||
"type": "@type",
|
||||
"Accept": "as:Accept",
|
||||
"Activity": "as:Activity",
|
||||
"IntransitiveActivity": "as:IntransitiveActivity",
|
||||
"Add": "as:Add",
|
||||
"Announce": "as:Announce",
|
||||
"Application": "as:Application",
|
||||
"Arrive": "as:Arrive",
|
||||
"Article": "as:Article",
|
||||
"Audio": "as:Audio",
|
||||
"Block": "as:Block",
|
||||
"Collection": "as:Collection",
|
||||
"CollectionPage": "as:CollectionPage",
|
||||
"Relationship": "as:Relationship",
|
||||
"Create": "as:Create",
|
||||
"Delete": "as:Delete",
|
||||
"Dislike": "as:Dislike",
|
||||
"Document": "as:Document",
|
||||
"Event": "as:Event",
|
||||
"Follow": "as:Follow",
|
||||
"Flag": "as:Flag",
|
||||
"Group": "as:Group",
|
||||
"Ignore": "as:Ignore",
|
||||
"Image": "as:Image",
|
||||
"Invite": "as:Invite",
|
||||
"Join": "as:Join",
|
||||
"Leave": "as:Leave",
|
||||
"Like": "as:Like",
|
||||
"Link": "as:Link",
|
||||
"Mention": "as:Mention",
|
||||
"Note": "as:Note",
|
||||
"Object": "as:Object",
|
||||
"Offer": "as:Offer",
|
||||
"OrderedCollection": "as:OrderedCollection",
|
||||
"OrderedCollectionPage": "as:OrderedCollectionPage",
|
||||
"Organization": "as:Organization",
|
||||
"Page": "as:Page",
|
||||
"Person": "as:Person",
|
||||
"Place": "as:Place",
|
||||
"Profile": "as:Profile",
|
||||
"Question": "as:Question",
|
||||
"Reject": "as:Reject",
|
||||
"Remove": "as:Remove",
|
||||
"Service": "as:Service",
|
||||
"TentativeAccept": "as:TentativeAccept",
|
||||
"TentativeReject": "as:TentativeReject",
|
||||
"Tombstone": "as:Tombstone",
|
||||
"Undo": "as:Undo",
|
||||
"Update": "as:Update",
|
||||
"Video": "as:Video",
|
||||
"View": "as:View",
|
||||
"Listen": "as:Listen",
|
||||
"Read": "as:Read",
|
||||
"Move": "as:Move",
|
||||
"Travel": "as:Travel",
|
||||
"IsFollowing": "as:IsFollowing",
|
||||
"IsFollowedBy": "as:IsFollowedBy",
|
||||
"IsContact": "as:IsContact",
|
||||
"IsMember": "as:IsMember",
|
||||
"subject": {
|
||||
"@id": "as:subject",
|
||||
"@type": "@id"
|
||||
},
|
||||
"relationship": {
|
||||
"@id": "as:relationship",
|
||||
"@type": "@id"
|
||||
},
|
||||
"actor": {
|
||||
"@id": "as:actor",
|
||||
"@type": "@id"
|
||||
},
|
||||
"attributedTo": {
|
||||
"@id": "as:attributedTo",
|
||||
"@type": "@id"
|
||||
},
|
||||
"attachment": {
|
||||
"@id": "as:attachment",
|
||||
"@type": "@id"
|
||||
},
|
||||
"bcc": {
|
||||
"@id": "as:bcc",
|
||||
"@type": "@id"
|
||||
},
|
||||
"bto": {
|
||||
"@id": "as:bto",
|
||||
"@type": "@id"
|
||||
},
|
||||
"cc": {
|
||||
"@id": "as:cc",
|
||||
"@type": "@id"
|
||||
},
|
||||
"context": {
|
||||
"@id": "as:context",
|
||||
"@type": "@id"
|
||||
},
|
||||
"current": {
|
||||
"@id": "as:current",
|
||||
"@type": "@id"
|
||||
},
|
||||
"first": {
|
||||
"@id": "as:first",
|
||||
"@type": "@id"
|
||||
},
|
||||
"generator": {
|
||||
"@id": "as:generator",
|
||||
"@type": "@id"
|
||||
},
|
||||
"icon": {
|
||||
"@id": "as:icon",
|
||||
"@type": "@id"
|
||||
},
|
||||
"image": {
|
||||
"@id": "as:image",
|
||||
"@type": "@id"
|
||||
},
|
||||
"inReplyTo": {
|
||||
"@id": "as:inReplyTo",
|
||||
"@type": "@id"
|
||||
},
|
||||
"items": {
|
||||
"@id": "as:items",
|
||||
"@type": "@id"
|
||||
},
|
||||
"instrument": {
|
||||
"@id": "as:instrument",
|
||||
"@type": "@id"
|
||||
},
|
||||
"orderedItems": {
|
||||
"@id": "as:items",
|
||||
"@type": "@id",
|
||||
"@container": "@list"
|
||||
},
|
||||
"last": {
|
||||
"@id": "as:last",
|
||||
"@type": "@id"
|
||||
},
|
||||
"location": {
|
||||
"@id": "as:location",
|
||||
"@type": "@id"
|
||||
},
|
||||
"next": {
|
||||
"@id": "as:next",
|
||||
"@type": "@id"
|
||||
},
|
||||
"object": {
|
||||
"@id": "as:object",
|
||||
"@type": "@id"
|
||||
},
|
||||
"oneOf": {
|
||||
"@id": "as:oneOf",
|
||||
"@type": "@id"
|
||||
},
|
||||
"anyOf": {
|
||||
"@id": "as:anyOf",
|
||||
"@type": "@id"
|
||||
},
|
||||
"closed": {
|
||||
"@id": "as:closed",
|
||||
"@type": "xsd:dateTime"
|
||||
},
|
||||
"origin": {
|
||||
"@id": "as:origin",
|
||||
"@type": "@id"
|
||||
},
|
||||
"accuracy": {
|
||||
"@id": "as:accuracy",
|
||||
"@type": "xsd:float"
|
||||
},
|
||||
"prev": {
|
||||
"@id": "as:prev",
|
||||
"@type": "@id"
|
||||
},
|
||||
"preview": {
|
||||
"@id": "as:preview",
|
||||
"@type": "@id"
|
||||
},
|
||||
"replies": {
|
||||
"@id": "as:replies",
|
||||
"@type": "@id"
|
||||
},
|
||||
"result": {
|
||||
"@id": "as:result",
|
||||
"@type": "@id"
|
||||
},
|
||||
"audience": {
|
||||
"@id": "as:audience",
|
||||
"@type": "@id"
|
||||
},
|
||||
"partOf": {
|
||||
"@id": "as:partOf",
|
||||
"@type": "@id"
|
||||
},
|
||||
"tag": {
|
||||
"@id": "as:tag",
|
||||
"@type": "@id"
|
||||
},
|
||||
"target": {
|
||||
"@id": "as:target",
|
||||
"@type": "@id"
|
||||
},
|
||||
"to": {
|
||||
"@id": "as:to",
|
||||
"@type": "@id"
|
||||
},
|
||||
"url": {
|
||||
"@id": "as:url",
|
||||
"@type": "@id"
|
||||
},
|
||||
"altitude": {
|
||||
"@id": "as:altitude",
|
||||
"@type": "xsd:float"
|
||||
},
|
||||
"content": "as:content",
|
||||
"contentMap": {
|
||||
"@id": "as:content",
|
||||
"@container": "@language"
|
||||
},
|
||||
"name": "as:name",
|
||||
"nameMap": {
|
||||
"@id": "as:name",
|
||||
"@container": "@language"
|
||||
},
|
||||
"duration": {
|
||||
"@id": "as:duration",
|
||||
"@type": "xsd:duration"
|
||||
},
|
||||
"endTime": {
|
||||
"@id": "as:endTime",
|
||||
"@type": "xsd:dateTime"
|
||||
},
|
||||
"height": {
|
||||
"@id": "as:height",
|
||||
"@type": "xsd:nonNegativeInteger"
|
||||
},
|
||||
"href": {
|
||||
"@id": "as:href",
|
||||
"@type": "@id"
|
||||
},
|
||||
"hreflang": "as:hreflang",
|
||||
"latitude": {
|
||||
"@id": "as:latitude",
|
||||
"@type": "xsd:float"
|
||||
},
|
||||
"longitude": {
|
||||
"@id": "as:longitude",
|
||||
"@type": "xsd:float"
|
||||
},
|
||||
"mediaType": "as:mediaType",
|
||||
"published": {
|
||||
"@id": "as:published",
|
||||
"@type": "xsd:dateTime"
|
||||
},
|
||||
"radius": {
|
||||
"@id": "as:radius",
|
||||
"@type": "xsd:float"
|
||||
},
|
||||
"rel": "as:rel",
|
||||
"startIndex": {
|
||||
"@id": "as:startIndex",
|
||||
"@type": "xsd:nonNegativeInteger"
|
||||
},
|
||||
"startTime": {
|
||||
"@id": "as:startTime",
|
||||
"@type": "xsd:dateTime"
|
||||
},
|
||||
"summary": "as:summary",
|
||||
"summaryMap": {
|
||||
"@id": "as:summary",
|
||||
"@container": "@language"
|
||||
},
|
||||
"totalItems": {
|
||||
"@id": "as:totalItems",
|
||||
"@type": "xsd:nonNegativeInteger"
|
||||
},
|
||||
"units": "as:units",
|
||||
"updated": {
|
||||
"@id": "as:updated",
|
||||
"@type": "xsd:dateTime"
|
||||
},
|
||||
"width": {
|
||||
"@id": "as:width",
|
||||
"@type": "xsd:nonNegativeInteger"
|
||||
},
|
||||
"describes": {
|
||||
"@id": "as:describes",
|
||||
"@type": "@id"
|
||||
},
|
||||
"formerType": {
|
||||
"@id": "as:formerType",
|
||||
"@type": "@id"
|
||||
},
|
||||
"deleted": {
|
||||
"@id": "as:deleted",
|
||||
"@type": "xsd:dateTime"
|
||||
},
|
||||
"inbox": {
|
||||
"@id": "ldp:inbox",
|
||||
"@type": "@id"
|
||||
},
|
||||
"outbox": {
|
||||
"@id": "as:outbox",
|
||||
"@type": "@id"
|
||||
},
|
||||
"following": {
|
||||
"@id": "as:following",
|
||||
"@type": "@id"
|
||||
},
|
||||
"followers": {
|
||||
"@id": "as:followers",
|
||||
"@type": "@id"
|
||||
},
|
||||
"streams": {
|
||||
"@id": "as:streams",
|
||||
"@type": "@id"
|
||||
},
|
||||
"preferredUsername": "as:preferredUsername",
|
||||
"endpoints": {
|
||||
"@id": "as:endpoints",
|
||||
"@type": "@id"
|
||||
},
|
||||
"uploadMedia": {
|
||||
"@id": "as:uploadMedia",
|
||||
"@type": "@id"
|
||||
},
|
||||
"proxyUrl": {
|
||||
"@id": "as:proxyUrl",
|
||||
"@type": "@id"
|
||||
},
|
||||
"liked": {
|
||||
"@id": "as:liked",
|
||||
"@type": "@id"
|
||||
},
|
||||
"oauthAuthorizationEndpoint": {
|
||||
"@id": "as:oauthAuthorizationEndpoint",
|
||||
"@type": "@id"
|
||||
},
|
||||
"oauthTokenEndpoint": {
|
||||
"@id": "as:oauthTokenEndpoint",
|
||||
"@type": "@id"
|
||||
},
|
||||
"provideClientKey": {
|
||||
"@id": "as:provideClientKey",
|
||||
"@type": "@id"
|
||||
},
|
||||
"signClientKey": {
|
||||
"@id": "as:signClientKey",
|
||||
"@type": "@id"
|
||||
},
|
||||
"sharedInbox": {
|
||||
"@id": "as:sharedInbox",
|
||||
"@type": "@id"
|
||||
},
|
||||
"Public": {
|
||||
"@id": "as:Public",
|
||||
"@type": "@id"
|
||||
},
|
||||
"source": "as:source",
|
||||
"likes": {
|
||||
"@id": "as:likes",
|
||||
"@type": "@id"
|
||||
},
|
||||
"shares": {
|
||||
"@id": "as:shares",
|
||||
"@type": "@id"
|
||||
},
|
||||
"alsoKnownAs": {
|
||||
"@id": "as:alsoKnownAs",
|
||||
"@type": "@id"
|
||||
}
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ else
|
||||
fi
|
||||
printf " - %s\n" "${CONFIG_ARRAY[@]}"
|
||||
|
||||
mkdir -p test/tmp/data
|
||||
mkdir -p test/tmp/data
|
||||
echo "$TEST_NAME - Building and installing package"
|
||||
npm pack --loglevel warn
|
||||
npm install -g solid-community-server-*.tgz --loglevel warn
|
||||
@ -72,7 +72,7 @@ run_server_with_config () {
|
||||
cat test/tmp/"$CONFIG_NAME"
|
||||
else
|
||||
echo "$TEST_NAME($CONFIG_NAME) - Attempting HTTP access to the server"
|
||||
if curl -sfkI -X GET --retry 15 --retry-connrefused --retry-delay 1 $CSS_BASE_URL > test/tmp/"$CONFIG_NAME"-curl; then
|
||||
if curl -sfkI -X GET --retry 15 --retry-connrefused --retry-delay 5 $CSS_BASE_URL > test/tmp/"$CONFIG_NAME"-curl; then
|
||||
echo "$TEST_NAME($CONFIG_NAME) - SUCCESS: server reached"
|
||||
FAILURE=0
|
||||
else
|
||||
|
@ -5,7 +5,6 @@ import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
import type { NamedNode } from 'n3';
|
||||
import { DataFactory, Parser, Store } from 'n3';
|
||||
import type { App } from '../../src/init/App';
|
||||
|
||||
import { matchesAuthorizationScheme } from '../../src/util/HeaderUtil';
|
||||
import { joinUrl, trimTrailingSlashes } from '../../src/util/PathUtil';
|
||||
import { readJsonStream } from '../../src/util/StreamUtil';
|
||||
@ -21,14 +20,13 @@ import {
|
||||
removeFolder,
|
||||
} from './Config';
|
||||
import quad = DataFactory.quad;
|
||||
import namedNode = DataFactory.namedNode;
|
||||
|
||||
const port = getPort('WebHookChannel2023');
|
||||
const baseUrl = `http://localhost:${port}/`;
|
||||
const clientPort = getPort('WebHookChannel2023-client');
|
||||
const target = `http://localhost:${clientPort}/`;
|
||||
const webId = 'http://example.com/card/#me';
|
||||
const notificationType = NOTIFY.WebHookChannel2023;
|
||||
const notificationType = NOTIFY.WebhookChannel2023;
|
||||
|
||||
const rootFilePath = getTestFolder('WebHookChannel2023');
|
||||
const stores: [string, any][] = [
|
||||
@ -37,8 +35,7 @@ const stores: [string, any][] = [
|
||||
teardown: jest.fn(),
|
||||
}],
|
||||
[ 'on-disk storage', {
|
||||
// Switch to file locker after https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1452
|
||||
configs: [ 'storage/backend/file.json', 'util/resource-locker/memory.json' ],
|
||||
configs: [ 'storage/backend/file.json', 'util/resource-locker/file.json' ],
|
||||
teardown: async(): Promise<void> => removeFolder(rootFilePath),
|
||||
}],
|
||||
];
|
||||
@ -97,9 +94,9 @@ describe.each(stores)('A server supporting WebHookChannel2023 using %s', (name,
|
||||
const quads = new Store(new Parser().parse(await response.text()));
|
||||
|
||||
// Find the notification channel for websockets
|
||||
const subscriptions = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.subscription, null);
|
||||
const subscriptions = quads.getObjects(null, NOTIFY.terms.subscription, null);
|
||||
const webhookSubscriptions = subscriptions.filter((channel): boolean => quads.has(
|
||||
quad(channel as NamedNode, NOTIFY.terms.channelType, namedNode(`${NOTIFY.namespace}WebHookChannel2023`)),
|
||||
quad(channel as NamedNode, NOTIFY.terms.channelType, NOTIFY.terms.WebhookChannel2023),
|
||||
));
|
||||
expect(webhookSubscriptions).toHaveLength(1);
|
||||
subscriptionUrl = webhookSubscriptions[0].value;
|
||||
|
@ -30,8 +30,7 @@ const stores: [string, any][] = [
|
||||
teardown: jest.fn(),
|
||||
}],
|
||||
[ 'on-disk storage', {
|
||||
// Switch to file locker after https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1452
|
||||
configs: [ 'storage/backend/file.json', 'util/resource-locker/memory.json' ],
|
||||
configs: [ 'storage/backend/file.json', 'util/resource-locker/file.json' ],
|
||||
teardown: async(): Promise<void> => removeFolder(rootFilePath),
|
||||
}],
|
||||
];
|
||||
@ -86,9 +85,9 @@ describe.each(stores)('A server supporting WebSocketChannel2023 using %s', (name
|
||||
const quads = new Store(new Parser().parse(await response.text()));
|
||||
|
||||
// Find the notification channel for websockets
|
||||
const subscriptions = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.subscription, null);
|
||||
const subscriptions = quads.getObjects(null, NOTIFY.terms.subscription, null);
|
||||
const websocketSubscriptions = subscriptions.filter((channel): boolean => quads.has(
|
||||
quad(channel as NamedNode, NOTIFY.terms.channelType, namedNode(`${NOTIFY.namespace}WebSocketChannel2023`)),
|
||||
quad(channel as NamedNode, NOTIFY.terms.channelType, NOTIFY.terms.WebSocketChannel2023),
|
||||
));
|
||||
expect(websocketSubscriptions).toHaveLength(1);
|
||||
subscriptionUrl = websocketSubscriptions[0].value;
|
||||
|
@ -49,36 +49,33 @@ describe('An AllowAcceptHeaderWriter', (): void => {
|
||||
expect(headers['accept-post']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns all methods for an empty container.', async(): Promise<void> => {
|
||||
it('returns all methods except PUT for an empty container.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: emptyContainer })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
expect(typeof headers.allow).toBe('string');
|
||||
expect(new Set((headers.allow as string).split(', ')))
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', 'DELETE' ]));
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH', 'DELETE' ]));
|
||||
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
|
||||
expect(headers['accept-put']).toBe('*/*');
|
||||
expect(headers['accept-post']).toBe('*/*');
|
||||
});
|
||||
|
||||
it('returns all methods except DELETE for a non-empty container.', async(): Promise<void> => {
|
||||
it('returns all methods except PUT/DELETE for a non-empty container.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: fullContainer })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
expect(typeof headers.allow).toBe('string');
|
||||
expect(new Set((headers.allow as string).split(', ')))
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH' ]));
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH' ]));
|
||||
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
|
||||
expect(headers['accept-put']).toBe('*/*');
|
||||
expect(headers['accept-post']).toBe('*/*');
|
||||
});
|
||||
|
||||
it('returns all methods except DELETE for a storage container.', async(): Promise<void> => {
|
||||
it('returns all methods except PUT/DELETE for a storage container.', async(): Promise<void> => {
|
||||
await expect(writer.handleSafe({ response, metadata: storageContainer })).resolves.toBeUndefined();
|
||||
const headers = response.getHeaders();
|
||||
expect(typeof headers.allow).toBe('string');
|
||||
expect(new Set((headers.allow as string).split(', ')))
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH' ]));
|
||||
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH' ]));
|
||||
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
|
||||
expect(headers['accept-put']).toBe('*/*');
|
||||
expect(headers['accept-post']).toBe('*/*');
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,24 @@
|
||||
import { PassThrough } from 'stream';
|
||||
import type { Logger } from 'winston';
|
||||
import type * as Transport from 'winston-transport';
|
||||
import { WinstonLogger } from '../../../src/logging/WinstonLogger';
|
||||
import { WinstonLoggerFactory } from '../../../src/logging/WinstonLoggerFactory';
|
||||
|
||||
const now = new Date();
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
describe('WinstonLoggerFactory', (): void => {
|
||||
let factory: WinstonLoggerFactory;
|
||||
let transport: jest.Mocked<Transport>;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
factory = new WinstonLoggerFactory('debug');
|
||||
|
||||
// Create a dummy log transport
|
||||
transport = new PassThrough({ objectMode: true }) as any;
|
||||
transport.write = jest.fn();
|
||||
transport.log = jest.fn();
|
||||
});
|
||||
|
||||
it('creates WinstonLoggers.', async(): Promise<void> => {
|
||||
@ -19,10 +31,6 @@ describe('WinstonLoggerFactory', (): void => {
|
||||
});
|
||||
|
||||
it('allows WinstonLoggers to be invoked.', async(): Promise<void> => {
|
||||
// Create a dummy log transport
|
||||
const transport: any = new PassThrough({ objectMode: true });
|
||||
transport.write = jest.fn();
|
||||
transport.log = jest.fn();
|
||||
(factory as any).createTransports = (): any => [ transport ];
|
||||
|
||||
// Create logger, and log
|
||||
@ -30,15 +38,39 @@ describe('WinstonLoggerFactory', (): void => {
|
||||
logger.log('debug', 'my message');
|
||||
|
||||
expect(transport.write).toHaveBeenCalledTimes(1);
|
||||
// Need to check level like this as it has color tags
|
||||
const { level } = transport.write.mock.calls[0][0];
|
||||
expect(transport.write).toHaveBeenCalledWith({
|
||||
label: 'MyLabel',
|
||||
level: expect.stringContaining('debug'),
|
||||
level,
|
||||
message: 'my message',
|
||||
timestamp: expect.any(String),
|
||||
metadata: expect.any(Object),
|
||||
timestamp: now.toISOString(),
|
||||
metadata: {},
|
||||
[Symbol.for('level')]: 'debug',
|
||||
[Symbol.for('splat')]: [ undefined ],
|
||||
[Symbol.for('message')]: expect.any(String),
|
||||
[Symbol.for('message')]: `${now.toISOString()} [MyLabel] {W-???} ${level}: my message`,
|
||||
});
|
||||
});
|
||||
|
||||
it('allows extra metadata when logging to indicate the thread.', async(): Promise<void> => {
|
||||
(factory as any).createTransports = (): any => [ transport ];
|
||||
|
||||
// Create logger, and log
|
||||
const logger = factory.createLogger('MyLabel');
|
||||
logger.log('debug', 'my message', { isPrimary: true, pid: 0 });
|
||||
|
||||
expect(transport.write).toHaveBeenCalledTimes(1);
|
||||
// Need to check level like this as it has color tags
|
||||
const { level } = transport.write.mock.calls[0][0];
|
||||
expect(transport.write).toHaveBeenCalledWith(expect.objectContaining({
|
||||
label: 'MyLabel',
|
||||
level,
|
||||
message: 'my message',
|
||||
timestamp: now.toISOString(),
|
||||
metadata: { isPrimary: true, pid: 0 },
|
||||
[Symbol.for('level')]: 'debug',
|
||||
[Symbol.for('splat')]: [ undefined ],
|
||||
[Symbol.for('message')]: `${now.toISOString()} [MyLabel] {Primary} ${level}: my message`,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@ -83,8 +83,8 @@ describe('A StorageDescriptionHandler', (): void => {
|
||||
expect(result.metadata?.contentType).toBe('internal/quads');
|
||||
expect(result.data).toBeDefined();
|
||||
const quads = await readableToQuads(result.data!);
|
||||
expect(quads.countQuads(operation.target.path, RDF.terms.type, PIM.terms.Storage, null)).toBe(1);
|
||||
expect(quads.countQuads('http://example.com/', RDF.terms.type, PIM.terms.Storage, null)).toBe(1);
|
||||
expect(describer.handle).toHaveBeenCalledTimes(1);
|
||||
expect(describer.handle).toHaveBeenLastCalledWith(operation.target);
|
||||
expect(describer.handle).toHaveBeenLastCalledWith({ path: 'http://example.com/' });
|
||||
});
|
||||
});
|
||||
|
@ -43,14 +43,14 @@ describe('A WebhookChannel2023Type', (): void => {
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
data = new Store();
|
||||
data.addQuad(quad(subject, RDF.terms.type, NOTIFY.terms.WebHookChannel2023));
|
||||
data.addQuad(quad(subject, RDF.terms.type, NOTIFY.terms.WebhookChannel2023));
|
||||
data.addQuad(quad(subject, NOTIFY.terms.topic, namedNode(topic)));
|
||||
data.addQuad(quad(subject, NOTIFY.terms.sendTo, namedNode(sendTo)));
|
||||
|
||||
const id = 'http://example.com/webhooks/4c9b88c1-7502-4107-bb79-2a3a590c7aa3';
|
||||
channel = {
|
||||
id,
|
||||
type: NOTIFY.WebHookChannel2023,
|
||||
type: NOTIFY.WebhookChannel2023,
|
||||
topic: 'https://storage.example/resource',
|
||||
sendTo,
|
||||
};
|
||||
@ -79,7 +79,7 @@ describe('A WebhookChannel2023Type', (): void => {
|
||||
CONTEXT_NOTIFICATION,
|
||||
],
|
||||
id: channel.id,
|
||||
type: NOTIFY.WebHookChannel2023,
|
||||
type: NOTIFY.WebhookChannel2023,
|
||||
sendTo,
|
||||
topic,
|
||||
sender: 'http://example.com/webhooks/webid',
|
||||
|
@ -45,7 +45,7 @@ describe('A WebHookEmitter', (): void => {
|
||||
const channel: WebhookChannel2023 = {
|
||||
id: 'id',
|
||||
topic: 'http://example.com/foo',
|
||||
type: NOTIFY.WebHookChannel2023,
|
||||
type: NOTIFY.WebhookChannel2023,
|
||||
sendTo: 'http://example.org/somewhere-else',
|
||||
};
|
||||
|
||||
|
@ -53,32 +53,63 @@ describe('HeaderUtil', (): void => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects Accept Headers with invalid types.', async(): Promise<void> => {
|
||||
expect((): any => parseAccept('*')).toThrow('Invalid Accept range:');
|
||||
expect((): any => parseAccept('"bad"/text')).toThrow('Invalid Accept range:');
|
||||
expect((): any => parseAccept('*/\\bad')).toThrow('Invalid Accept range:');
|
||||
expect((): any => parseAccept('*/*')).not.toThrow('Invalid Accept range:');
|
||||
it('ignores Accept Headers with invalid types.', async(): Promise<void> => {
|
||||
expect(parseAccept('*')).toEqual([]);
|
||||
expect(parseAccept('"bad"/text')).toEqual([]);
|
||||
expect(parseAccept('*/\\bad')).toEqual([]);
|
||||
expect(parseAccept('*/*')).toEqual([{
|
||||
parameters: { extension: {}, mediaType: {}}, range: '*/*', weight: 1,
|
||||
}]);
|
||||
});
|
||||
|
||||
it('rejects Accept Headers with invalid q values.', async(): Promise<void> => {
|
||||
expect((): any => parseAccept('a/b; q=text')).toThrow('Invalid q value:');
|
||||
expect((): any => parseAccept('a/b; q=0.1234')).toThrow('Invalid q value:');
|
||||
expect((): any => parseAccept('a/b; q=1.1')).toThrow('Invalid q value:');
|
||||
expect((): any => parseAccept('a/b; q=1.000')).not.toThrow();
|
||||
expect((): any => parseAccept('a/b; q=0.123')).not.toThrow();
|
||||
it('ignores the weight of Accept Headers with q values it can not parse.', async(): Promise<void> => {
|
||||
expect(parseAccept('a/b; q=text')).toEqual([{
|
||||
range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
// Invalid Q value but can be parsed
|
||||
expect(parseAccept('a/b; q=0.1234')).toEqual([{
|
||||
range: 'a/b', weight: 0.1234, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
expect(parseAccept('a/b; q=1.1')).toEqual([{
|
||||
range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
expect(parseAccept('a/b; q=1.000')).toEqual([{
|
||||
range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
expect(parseAccept('a/b; q=-5')).toEqual([{
|
||||
range: 'a/b', weight: 0, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
expect(parseAccept('a/b; q=0.123')).toEqual([{
|
||||
range: 'a/b', weight: 0.123, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
});
|
||||
|
||||
it('rejects Accept Headers with invalid parameters.', async(): Promise<void> => {
|
||||
expect((): any => parseAccept('a/b; a')).toThrow('Invalid Accept parameter');
|
||||
expect((): any => parseAccept('a/b; a=\\')).toThrow('Invalid parameter value');
|
||||
expect((): any => parseAccept('a/b; q=1 ; a=\\')).toThrow('Invalid parameter value');
|
||||
expect((): any => parseAccept('a/b; q=1 ; a')).not.toThrow('Invalid Accept parameter');
|
||||
it('ignores Accept Headers with invalid parameters.', async(): Promise<void> => {
|
||||
expect(parseAccept('a/b; a')).toEqual([{
|
||||
range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
expect(parseAccept('a/b; a=\\')).toEqual([{
|
||||
range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
expect(parseAccept('a/b; q=1 ; a=\\')).toEqual([{
|
||||
range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}},
|
||||
}]);
|
||||
expect(parseAccept('a/b; q=1 ; a')).toEqual([{
|
||||
// eslint-disable-next-line id-length
|
||||
range: 'a/b', weight: 1, parameters: { extension: { a: '' }, mediaType: {}},
|
||||
}]);
|
||||
});
|
||||
|
||||
it('rejects Accept Headers with quoted parameters.', async(): Promise<void> => {
|
||||
expect((): any => parseAccept('a/b; a="\\""')).not.toThrow();
|
||||
expect((): any => parseAccept('a/b; a="\\\u007F"')).toThrow('Invalid quoted string in header:');
|
||||
});
|
||||
|
||||
it('rejects invalid values when strict mode is enabled.', async(): Promise<void> => {
|
||||
expect((): any => parseAccept('"bad"/text', true)).toThrow(BadRequestHttpError);
|
||||
expect((): any => parseAccept('a/b; q=text', true)).toThrow(BadRequestHttpError);
|
||||
expect((): any => parseAccept('a/b; a', true)).toThrow(BadRequestHttpError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#parseCharset', (): void => {
|
||||
@ -89,10 +120,14 @@ describe('HeaderUtil', (): void => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects invalid Accept-Charset Headers.', async(): Promise<void> => {
|
||||
expect((): any => parseAcceptCharset('a/b')).toThrow('Invalid Accept-Charset range:');
|
||||
expect((): any => parseAcceptCharset('a; q=text')).toThrow('Invalid q value:');
|
||||
expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed');
|
||||
it('ignores invalid Accept-Charset Headers.', async(): Promise<void> => {
|
||||
expect(parseAcceptCharset('a/b')).toEqual([]);
|
||||
expect(parseAcceptCharset('a; q=text')).toEqual([{ range: 'a', weight: 1 }]);
|
||||
expect(parseAcceptCharset('a; c=d')).toEqual([{ range: 'a', weight: 1 }]);
|
||||
});
|
||||
|
||||
it('rejects invalid values when strict mode is enabled.', async(): Promise<void> => {
|
||||
expect((): any => parseAcceptCharset('a/b', true)).toThrow(BadRequestHttpError);
|
||||
});
|
||||
});
|
||||
|
||||
@ -109,10 +144,14 @@ describe('HeaderUtil', (): void => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects invalid Accept-Encoding Headers.', async(): Promise<void> => {
|
||||
expect((): any => parseAcceptEncoding('a/b')).toThrow('Invalid Accept-Encoding range:');
|
||||
expect((): any => parseAcceptEncoding('a; q=text')).toThrow('Invalid q value:');
|
||||
expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed');
|
||||
it('ignores invalid Accept-Encoding Headers.', async(): Promise<void> => {
|
||||
expect(parseAcceptEncoding('a/b')).toEqual([]);
|
||||
expect(parseAcceptEncoding('a; q=text')).toEqual([{ range: 'a', weight: 1 }]);
|
||||
expect(parseAcceptCharset('a; c=d')).toEqual([{ range: 'a', weight: 1 }]);
|
||||
});
|
||||
|
||||
it('rejects invalid values when strict mode is enabled.', async(): Promise<void> => {
|
||||
expect((): any => parseAcceptEncoding('a/b', true)).toThrow(BadRequestHttpError);
|
||||
});
|
||||
});
|
||||
|
||||
@ -125,16 +164,20 @@ describe('HeaderUtil', (): void => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects invalid Accept-Language Headers.', async(): Promise<void> => {
|
||||
expect((): any => parseAcceptLanguage('a/b')).toThrow('Invalid Accept-Language range:');
|
||||
expect((): any => parseAcceptLanguage('05-a')).toThrow('Invalid Accept-Language range:');
|
||||
expect((): any => parseAcceptLanguage('a--05')).toThrow('Invalid Accept-Language range:');
|
||||
expect((): any => parseAcceptLanguage('a-"a"')).toThrow('Invalid Accept-Language range:');
|
||||
expect((): any => parseAcceptLanguage('a-05')).not.toThrow('Invalid Accept-Language range:');
|
||||
expect((): any => parseAcceptLanguage('a-b-c-d')).not.toThrow('Invalid Accept-Language range:');
|
||||
it('ignores invalid Accept-Language Headers.', async(): Promise<void> => {
|
||||
expect(parseAcceptLanguage('a/b')).toEqual([]);
|
||||
expect(parseAcceptLanguage('05-a')).toEqual([]);
|
||||
expect(parseAcceptLanguage('a--05')).toEqual([]);
|
||||
expect(parseAcceptLanguage('a-"a"')).toEqual([]);
|
||||
expect(parseAcceptLanguage('a-05')).toEqual([{ range: 'a-05', weight: 1 }]);
|
||||
expect(parseAcceptLanguage('a-b-c-d')).toEqual([{ range: 'a-b-c-d', weight: 1 }]);
|
||||
|
||||
expect((): any => parseAcceptLanguage('a; q=text')).toThrow('Invalid q value:');
|
||||
expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed');
|
||||
expect(parseAcceptLanguage('a; q=text')).toEqual([{ range: 'a', weight: 1 }]);
|
||||
expect(parseAcceptCharset('a; c=d')).toEqual([{ range: 'a', weight: 1 }]);
|
||||
});
|
||||
|
||||
it('rejects invalid values when strict mode is enabled.', async(): Promise<void> => {
|
||||
expect((): any => parseAcceptLanguage('a/b', true)).toThrow(BadRequestHttpError);
|
||||
});
|
||||
});
|
||||
|
||||
@ -150,9 +193,13 @@ describe('HeaderUtil', (): void => {
|
||||
expect(parseAcceptDateTime(' ')).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects invalid Accept-DateTime Headers.', async(): Promise<void> => {
|
||||
expect((): any => parseAcceptDateTime('a/b')).toThrow('Invalid Accept-DateTime range:');
|
||||
expect((): any => parseAcceptDateTime('30 May 2007')).toThrow('Invalid Accept-DateTime range:');
|
||||
it('ignores invalid Accept-DateTime Headers.', async(): Promise<void> => {
|
||||
expect(parseAcceptDateTime('a/b')).toEqual([]);
|
||||
expect(parseAcceptDateTime('30 May 2007')).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects invalid values when strict mode is enabled.', async(): Promise<void> => {
|
||||
expect((): any => parseAcceptLanguage('a/b', true)).toThrow(BadRequestHttpError);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -96,7 +96,7 @@ describe('PathUtil', (): void => {
|
||||
|
||||
describe('#toCanonicalUriPath', (): void => {
|
||||
it('encodes only the necessary parts.', (): void => {
|
||||
expect(toCanonicalUriPath('/a%20path&/name')).toBe('/a%20path%26/name');
|
||||
expect(toCanonicalUriPath('/a%20path&*/name')).toBe('/a%20path%26*/name');
|
||||
});
|
||||
|
||||
it('leaves the query string untouched.', (): void => {
|
||||
@ -138,6 +138,11 @@ describe('PathUtil', (): void => {
|
||||
expect(decodeUriPathComponents('/a%25252Fb')).toBe('/a%25252Fb');
|
||||
expect(decodeUriPathComponents('/a%2525252Fb')).toBe('/a%2525252Fb');
|
||||
});
|
||||
|
||||
it('ensures illegal path characters are encoded.', async(): Promise<void> => {
|
||||
expect(decodeUriPathComponents('/a<path%3F%3E/*:name?abc=def&xyz'))
|
||||
.toBe('/a%3Cpath%3F%3E/%2A%3Aname?abc=def&xyz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#encodeUriPathComponents', (): void => {
|
||||
|
@ -2,7 +2,7 @@ import { fetch } from 'cross-fetch';
|
||||
|
||||
/**
|
||||
* Subscribes to a notification channel.
|
||||
* @param type - The type of the notification channel, e.g., "NOTIFY.WebHookChannel2023".
|
||||
* @param type - The type of the notification channel, e.g., "NOTIFY.WebhookChannel2023".
|
||||
* @param webId - The WebID to spoof in the authorization header. This assumes the config uses the debug auth import.
|
||||
* @param subscriptionUrl - The subscription URL to which the request needs to be sent.
|
||||
* @param topic - The topic to subscribe to.
|
||||
|
@ -1,17 +1,24 @@
|
||||
import { setGlobalLoggerFactory } from '../../src/logging/LogUtil';
|
||||
import { WinstonLoggerFactory } from '../../src/logging/WinstonLoggerFactory';
|
||||
import { getTestFolder, removeFolder } from '../integration/Config';
|
||||
|
||||
// Jest global setup requires a single function to be exported
|
||||
export default async function(): Promise<void> {
|
||||
// Set the main logger
|
||||
const level = process.env.LOGLEVEL ?? 'off';
|
||||
const loggerFactory = new WinstonLoggerFactory(level);
|
||||
setGlobalLoggerFactory(loggerFactory);
|
||||
const level = process.env.LOGLEVEL ?? 'off';
|
||||
const loggerFactory = new WinstonLoggerFactory(level);
|
||||
setGlobalLoggerFactory(loggerFactory);
|
||||
|
||||
// Also set the logger factory of transpiled JS modules
|
||||
// (which are instantiated by Components.js)
|
||||
try {
|
||||
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||
const dist = require('../../dist/logging/LogUtil');
|
||||
dist.setGlobalLoggerFactory(loggerFactory);
|
||||
} catch {
|
||||
// Ignore
|
||||
// Also set the logger factory of transpiled JS modules
|
||||
// (which are instantiated by Components.js)
|
||||
try {
|
||||
// eslint-disable-next-line global-require,@typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports
|
||||
const dist = require('../../dist/logging/LogUtil');
|
||||
dist.setGlobalLoggerFactory(loggerFactory);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Clean up the test folder to prevent issues with remaining files from previous tests
|
||||
await removeFolder(getTestFolder(''));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user