UNPKG

14.9 kBPlain TextView Raw
1/**
2 * Copyright 2020 Inrupt Inc.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal in
6 * the Software without restriction, including without limitation the rights to use,
7 * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8 * Software, and to permit persons to whom the Software is furnished to do so,
9 * subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 */
21
22import { Quad, NamedNode } from "rdf-js";
23import LinkHeader from "http-link-header";
24import { dataset, DataFactory } from "./rdfjs";
25import { fetch } from "./fetcher";
26import { turtleToTriples, triplesToTurtle } from "./formats/turtle";
27import { isLocalNode, resolveIriForLocalNodes } from "./datatypes";
28import { internal_fetchResourceAcl, internal_fetchFallbackAcl } from "./acl";
29import {
30 UrlString,
31 LitDataset,
32 DatasetInfo,
33 ChangeLog,
34 hasChangelog,
35 hasDatasetInfo,
36 LocalNode,
37 unstable_Acl,
38 unstable_hasAccessibleAcl,
39 unstable_AccessModes,
40} from "./interfaces";
41
42/**
43 * Initialise a new [[LitDataset]] in memory.
44 *
45 * @returns An empty [[LitDataset]].
46 */
47export function createLitDataset(): LitDataset {
48 return dataset();
49}
50
51/**
52 * @internal
53 */
54export const defaultFetchOptions = {
55 fetch: fetch,
56};
57/**
58 * Fetch a LitDataset from the given URL. Currently requires the LitDataset to be available as [Turtle](https://www.w3.org/TR/turtle/).
59 *
60 * @param url URL to fetch a [[LitDataset]] from.
61 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
62 * @returns Promise resolving to a [[LitDataset]] containing the data at the given Resource, or rejecting if fetching it failed.
63 */
64export async function fetchLitDataset(
65 url: UrlString,
66 options: Partial<typeof defaultFetchOptions> = defaultFetchOptions
67): Promise<LitDataset & DatasetInfo> {
68 const config = {
69 ...defaultFetchOptions,
70 ...options,
71 };
72
73 const response = await config.fetch(url);
74 if (!response.ok) {
75 throw new Error(
76 `Fetching the Resource failed: ${response.status} ${response.statusText}.`
77 );
78 }
79 const data = await response.text();
80 const triples = await turtleToTriples(data, url);
81 const resource = dataset();
82 triples.forEach((triple) => resource.add(triple));
83
84 const datasetInfo = parseDatasetInfo(response);
85
86 const resourceWithDatasetInfo: LitDataset &
87 DatasetInfo = Object.assign(resource, { datasetInfo: datasetInfo });
88
89 return resourceWithDatasetInfo;
90}
91
92/**
93 * @internal
94 */
95export async function internal_fetchLitDatasetInfo(
96 url: UrlString,
97 options: Partial<typeof defaultFetchOptions> = defaultFetchOptions
98): Promise<DatasetInfo> {
99 const config = {
100 ...defaultFetchOptions,
101 ...options,
102 };
103
104 const response = await config.fetch(url, { method: "HEAD" });
105 if (!response.ok) {
106 throw new Error(
107 `Fetching the Resource metadata failed: ${response.status} ${response.statusText}.`
108 );
109 }
110
111 const datasetInfo = parseDatasetInfo(response);
112
113 return { datasetInfo: datasetInfo };
114}
115
116function parseDatasetInfo(response: Response): DatasetInfo["datasetInfo"] {
117 const datasetInfo: DatasetInfo["datasetInfo"] = {
118 fetchedFrom: response.url,
119 };
120 const linkHeader = response.headers.get("Link");
121 if (linkHeader) {
122 const parsedLinks = LinkHeader.parse(linkHeader);
123 const aclLinks = parsedLinks.get("rel", "acl");
124 if (aclLinks.length === 1) {
125 datasetInfo.unstable_aclUrl = new URL(
126 aclLinks[0].uri,
127 datasetInfo.fetchedFrom
128 ).href;
129 }
130 }
131
132 const wacAllowHeader = response.headers.get("WAC-Allow");
133 if (wacAllowHeader) {
134 datasetInfo.unstable_permissions = parseWacAllowHeader(wacAllowHeader);
135 }
136
137 return datasetInfo;
138}
139
140/**
141 * Experimental: fetch a LitDataset and its associated Access Control List.
142 *
143 * This is an experimental function that fetches both a Resource, the linked ACL Resource (if
144 * available), and the ACL that applies to it if the linked ACL Resource is not available. This can
145 * result in many HTTP requests being executed, in lieu of the Solid spec mandating servers to
146 * provide this info in a single request. Therefore, and because this function is still
147 * experimental, prefer [[fetchLitDataset]] instead.
148 *
149 * If the Resource does not advertise the ACL Resource (because the authenticated user does not have
150 * access to it), the `acl` property in the returned value will be null. `acl.resourceAcl` will be
151 * undefined if the Resource's linked ACL Resource could not be fetched (because it does not exist),
152 * and `acl.fallbackAcl` will be null if the applicable Container's ACL is not accessible to the
153 * authenticated user.
154 *
155 * @param url URL of the LitDataset to fetch.
156 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
157 * @returns A LitDataset and the ACLs that apply to it, if available to the authenticated user.
158 */
159export async function unstable_fetchLitDatasetWithAcl(
160 url: UrlString,
161 options: Partial<typeof defaultFetchOptions> = defaultFetchOptions
162): Promise<LitDataset & DatasetInfo & (unstable_Acl | { acl: null })> {
163 const litDataset = await fetchLitDataset(url, options);
164
165 if (!unstable_hasAccessibleAcl(litDataset)) {
166 return Object.assign(litDataset, { acl: null });
167 }
168
169 const [resourceAcl, fallbackAcl] = await Promise.all([
170 internal_fetchResourceAcl(litDataset, options),
171 internal_fetchFallbackAcl(litDataset, options),
172 ]);
173
174 const acl: unstable_Acl["acl"] = {
175 fallbackAcl: fallbackAcl,
176 resourceAcl: resourceAcl !== null ? resourceAcl : undefined,
177 };
178
179 return Object.assign(litDataset, { acl: acl });
180}
181
182const defaultSaveOptions = {
183 fetch: fetch,
184};
185/**
186 * Given a LitDataset, store it in a Solid Pod (overwriting the existing data at the given URL).
187 *
188 * @param url URL to save `litDataset` to.
189 * @param litDataset The [[LitDataset]] to save.
190 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
191 * @returns A Promise resolving to a [[LitDataset]] containing the stored data, or rejecting if saving it failed.
192 */
193export async function saveLitDatasetAt(
194 url: UrlString,
195 litDataset: LitDataset,
196 options: Partial<typeof defaultSaveOptions> = defaultSaveOptions
197): Promise<LitDataset & DatasetInfo & ChangeLog> {
198 const config = {
199 ...defaultSaveOptions,
200 ...options,
201 };
202
203 let requestInit: RequestInit;
204
205 if (isUpdate(litDataset, url)) {
206 const deleteStatement =
207 litDataset.changeLog.deletions.length > 0
208 ? `DELETE DATA {${(
209 await triplesToTurtle(
210 litDataset.changeLog.deletions.map(getNamedNodesForLocalNodes)
211 )
212 ).trim()}};`
213 : "";
214 const insertStatement =
215 litDataset.changeLog.additions.length > 0
216 ? `INSERT DATA {${(
217 await triplesToTurtle(
218 litDataset.changeLog.additions.map(getNamedNodesForLocalNodes)
219 )
220 ).trim()}};`
221 : "";
222
223 requestInit = {
224 method: "PATCH",
225 body: `${deleteStatement} ${insertStatement}`,
226 headers: {
227 "Content-Type": "application/sparql-update",
228 },
229 };
230 } else {
231 requestInit = {
232 method: "PUT",
233 body: await triplesToTurtle(
234 Array.from(litDataset).map(getNamedNodesForLocalNodes)
235 ),
236 headers: {
237 "Content-Type": "text/turtle",
238 "If-None-Match": "*",
239 Link: '<http://www.w3.org/ns/ldp#Resource>; rel="type"',
240 },
241 };
242 }
243
244 const response = await config.fetch(url, requestInit);
245
246 if (!response.ok) {
247 throw new Error(
248 `Storing the Resource failed: ${response.status} ${response.statusText}.`
249 );
250 }
251
252 const datasetInfo: DatasetInfo["datasetInfo"] = hasDatasetInfo(litDataset)
253 ? { ...litDataset.datasetInfo, fetchedFrom: url }
254 : { fetchedFrom: url };
255 const storedDataset: LitDataset & ChangeLog & DatasetInfo = Object.assign(
256 litDataset,
257 {
258 changeLog: { additions: [], deletions: [] },
259 datasetInfo: datasetInfo,
260 }
261 );
262
263 const storedDatasetWithResolvedIris = resolveLocalIrisInLitDataset(
264 storedDataset
265 );
266
267 return storedDatasetWithResolvedIris;
268}
269
270function isUpdate(
271 litDataset: LitDataset,
272 url: UrlString
273): litDataset is LitDataset &
274 ChangeLog &
275 DatasetInfo & { datasetInfo: { fetchedFrom: string } } {
276 return (
277 hasChangelog(litDataset) &&
278 hasDatasetInfo(litDataset) &&
279 typeof litDataset.datasetInfo.fetchedFrom === "string" &&
280 litDataset.datasetInfo.fetchedFrom === url
281 );
282}
283
284const defaultSaveInContainerOptions = {
285 fetch: fetch,
286};
287type SaveInContainerOptions = Partial<
288 typeof defaultSaveInContainerOptions & {
289 slugSuggestion: string;
290 }
291>;
292/**
293 * Given a LitDataset, store it in a Solid Pod in a new Resource inside a Container.
294 *
295 * @param containerUrl URL of the Container in which to create a new Resource.
296 * @param litDataset The [[LitDataset]] to save to a new Resource in the given Container.
297 * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters).
298 * @returns A Promise resolving to a [[LitDataset]] containing the stored data linked to the new Resource, or rejecting if saving it failed.
299 */
300export async function saveLitDatasetInContainer(
301 containerUrl: UrlString,
302 litDataset: LitDataset,
303 options: SaveInContainerOptions = defaultSaveInContainerOptions
304): Promise<LitDataset & DatasetInfo> {
305 const config = {
306 ...defaultSaveOptions,
307 ...options,
308 };
309
310 const rawTurtle = await triplesToTurtle(
311 Array.from(litDataset).map(getNamedNodesForLocalNodes)
312 );
313 const headers: RequestInit["headers"] = {
314 "Content-Type": "text/turtle",
315 Link: '<http://www.w3.org/ns/ldp#Resource>; rel="type"',
316 };
317 if (options.slugSuggestion) {
318 headers.slug = options.slugSuggestion;
319 }
320 const response = await config.fetch(containerUrl, {
321 method: "POST",
322 body: rawTurtle,
323 headers: headers,
324 });
325
326 if (!response.ok) {
327 throw new Error(
328 `Storing the Resource in the Container failed: ${response.status} ${response.statusText}.`
329 );
330 }
331
332 const locationHeader = response.headers.get("Location");
333 if (locationHeader === null) {
334 throw new Error(
335 "Could not determine the location for the newly saved LitDataset."
336 );
337 }
338
339 const resourceIri = new URL(locationHeader, new URL(containerUrl).origin)
340 .href;
341 const datasetInfo: DatasetInfo["datasetInfo"] = {
342 fetchedFrom: resourceIri,
343 };
344 const resourceWithDatasetInfo: LitDataset &
345 DatasetInfo = Object.assign(litDataset, { datasetInfo: datasetInfo });
346
347 const resourceWithResolvedIris = resolveLocalIrisInLitDataset(
348 resourceWithDatasetInfo
349 );
350
351 return resourceWithResolvedIris;
352}
353
354function getNamedNodesForLocalNodes(quad: Quad): Quad {
355 const subject = isLocalNode(quad.subject)
356 ? getNamedNodeFromLocalNode(quad.subject)
357 : quad.subject;
358 const object = isLocalNode(quad.object)
359 ? getNamedNodeFromLocalNode(quad.object)
360 : quad.object;
361
362 return {
363 ...quad,
364 subject: subject,
365 object: object,
366 };
367}
368
369function getNamedNodeFromLocalNode(localNode: LocalNode): NamedNode {
370 return DataFactory.namedNode("#" + localNode.name);
371}
372
373function resolveLocalIrisInLitDataset<Dataset extends LitDataset & DatasetInfo>(
374 litDataset: Dataset
375): Dataset {
376 const resourceIri = litDataset.datasetInfo.fetchedFrom;
377 const unresolvedQuads = Array.from(litDataset);
378
379 unresolvedQuads.forEach((unresolvedQuad) => {
380 const resolvedQuad = resolveIriForLocalNodes(unresolvedQuad, resourceIri);
381 litDataset.delete(unresolvedQuad);
382 litDataset.add(resolvedQuad);
383 });
384
385 return litDataset;
386}
387
388/**
389 * Parse a WAC-Allow header into user and public access booleans.
390 *
391 * @param wacAllowHeader A WAC-Allow header in the format `user="read append write control",public="read"`
392 * @see https://github.com/solid/solid-spec/blob/cb1373a369398d561b909009bd0e5a8c3fec953b/api-rest.md#wac-allow-headers
393 */
394function parseWacAllowHeader(wacAllowHeader: string) {
395 function parsePermissionStatement(
396 permissionStatement: string
397 ): unstable_AccessModes {
398 const permissions = permissionStatement.split(" ");
399 const writePermission = permissions.includes("write");
400 return writePermission
401 ? {
402 read: permissions.includes("read"),
403 append: true,
404 write: true,
405 control: permissions.includes("control"),
406 }
407 : {
408 read: permissions.includes("read"),
409 append: permissions.includes("append"),
410 write: false,
411 control: permissions.includes("control"),
412 };
413 }
414 function getStatementFor(header: string, scope: "user" | "public") {
415 const relevantEntries = header
416 .split(",")
417 .map((rawEntry) => rawEntry.split("="))
418 .filter((parts) => parts.length === 2 && parts[0].trim() === scope);
419
420 // There should only be one statement with the given scope:
421 if (relevantEntries.length !== 1) {
422 return "";
423 }
424 const relevantStatement = relevantEntries[0][1].trim();
425
426 // The given statement should be wrapped in double quotes to be valid:
427 if (
428 relevantStatement.charAt(0) !== '"' ||
429 relevantStatement.charAt(relevantStatement.length - 1) !== '"'
430 ) {
431 return "";
432 }
433 // Return the statment without the wrapping quotes, e.g.: read append write control
434 return relevantStatement.substring(1, relevantStatement.length - 1);
435 }
436
437 return {
438 user: parsePermissionStatement(getStatementFor(wacAllowHeader, "user")),
439 public: parsePermissionStatement(getStatementFor(wacAllowHeader, "public")),
440 };
441}