├── .babelrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── build.sh ├── config.js ├── demo ├── demo.css └── index.html ├── docs └── installing.md ├── package.json ├── src ├── app-registry.js ├── identity.js ├── index.js ├── meta.js ├── registry.js ├── solid │ ├── app-registration.js │ ├── index-registration.js │ └── profile.js ├── status.js ├── type-registry.js └── util │ ├── graph-util.js │ ├── rdf-parser.js │ └── web-util.js ├── test ├── integration │ ├── index.html │ ├── solid-profile-test.js │ └── web-client-test.js ├── resources │ ├── app-registry-listed.js │ ├── app-registry-unlisted.js │ ├── profile-extended.js │ ├── profile-minimal.js │ ├── profile-private.js │ ├── type-index-listed.js │ └── type-index-unlisted.js └── unit │ ├── app-registry-test.js │ ├── identity-test.js │ ├── registry-test.js │ ├── solid-client-test.js │ ├── solid-profile-test.js │ ├── type-registry-test.js │ └── vocab-test.js ├── vendor ├── qunit-1.21.0.css └── qunit-1.21.0.js ├── webpack-no-rdflib.config.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tests/ 3 | vendor/ 4 | dist/ 5 | lib/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.0" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ##### Version 0.23.7 2 | - Update solid-web-client to 0.2.0 3 | 4 | ##### Version 0.23.1 5 | - Use newest rdflib v0.13.0 6 | 7 | ##### Version 0.23.0 8 | 9 | - `getProfile()` now handles 30x redirects 10 | - Fix response.url and Content-Type handling in `solid-web-client` 11 | 12 | ##### Version 0.22.5 13 | 14 | - Misc. fixes and refactoring 15 | 16 | ##### Version 0.21.0 17 | - Massive refactoring, extracting authentication, permissions and web client 18 | into standalone repositories 19 | ([`solid-auth-tls`](https://github.com/solid/solid-auth-tls), 20 | [`solid-web-client`](https://github.com/solid/solid-web-client), 21 | [`solid-permissions`](https://github.com/solid/solid-permissions) and 22 | [`solid-namespace`](https://github.com/solid/solid-namespace)) 23 | - **(breaking change)** Deprecated `solid.web.getParsedGraph()`. Use 24 | `solid.web.get().then(response => { return response.parsedGraph() })` instead. 25 | - Added `profile.registerApp()` functionality (adds an app entry to the App 26 | Registry), and `profile.appsForType()` (queries for app registry entries for 27 | a given type). 28 | 29 | ##### Version 0.20.0 30 | - Added `initTypeRegistry()` and `initAppRegistry()` functionality 31 | - (**breaking change**) distribute two bundles as minified UMD modules. One 32 | includes `rdflib` in the bundle, and one does not. Clients using the bundles 33 | without a module bundler (e.g. referencing the library in ` 24 | 25 | 26 | 29 | 30 | 31 | ``` 32 | 33 | Or, using a module loader: 34 | 35 | ```js 36 | var solid = require('solid-client') 37 | ``` 38 | 39 | See the [installation docs](docs/installing.md) for more installation examples. 40 | 41 | Take a look at the **[solid-client demo 42 | page](https://solid.github.io/solid-client/demo/)** (source located in 43 | `demo/index.html`) for usage examples. 44 | 45 | ## Tutorials 46 | 47 | If you would like to learn how to build Solid apps using solid-client, please see: 48 | 49 | * [solid introduction tutorial](https://github.com/solid/solid-tutorial-intro) 50 | * [pastebin example tutorial](https://github.com/solid/solid-tutorial-pastebin) 51 | * [using rdflib.js tutorial](https://github.com/solid/solid-tutorial-rdflib.js) 52 | 53 | ## Developing solid-client 54 | 55 | **Node version:** 6.0+. 56 | 57 | Install dev dependencies: 58 | 59 | ``` 60 | npm install 61 | ``` 62 | 63 | Building (uses Browserify, builds to `solid-client.js` and `dist/solid-client-no-rdflib.js`): 64 | 65 | ``` 66 | npm run build 67 | ``` 68 | 69 | ## Testing 70 | 71 | To run the unit tests: 72 | 73 | ``` 74 | npm test 75 | ``` 76 | 77 | This runs the [Tape](https://github.com/substack/tape) unit test suite. 78 | 79 | ## Releases 80 | 81 | The following steps specify how to release solid-client: 82 | 83 | Make sure you're at the HEAD of `master`. 84 | 85 | ```shell 86 | $ git checkout master && git pull 87 | ``` 88 | 89 | Run `npm version` to bump the package version via git commit and git 90 | tags. 91 | 92 | ```shell 93 | # refer to http://semver.org/ for which of (major, minor, patch) to use 94 | $ npm version [major|minor|patch] 95 | ``` 96 | 97 | Next, push the commit and tags: 98 | 99 | ```shell 100 | $ git push --follow-tags 101 | ``` 102 | 103 | Finally release the package to `npmjs`. 104 | 105 | ```shell 106 | $ npm publish 107 | ``` 108 | 109 | ## Logging In and User Profiles 110 | 111 | Before doing any sort of [reading or 112 | writing](https://github.com/solid/solid-spec#reading-and-writing-resources) of 113 | Solid resources, your app will likely need to authenticate a user and load their 114 | profile, so let's start with those sections. 115 | 116 | ### Authentication 117 | 118 | Solid currently uses [WebID-TLS](https://github.com/solid/solid-spec#webid-tls) 119 | for authentication, which relies on a web browser's built-in key store to manage 120 | certificates and prompt the user to select the correct certificate when 121 | accessing a server. 122 | 123 | Solid servers must always return a Solid-specific HTTP header called `User`, 124 | which contains the [WebID](https://github.com/solid/solid-spec#identity) that 125 | the user used to access this particular server. An empty header usually means 126 | that the user is not authenticated. 127 | 128 | #### Detecting the Current Logged-in User 129 | 130 | Most of the WebID-TLS authentication process takes place before a web 131 | page gets fully loaded and the javascript code has had a chance to run. 132 | Since client-side Javascript code does *not* have access to most HTTP headers 133 | (including the `User` header) of the page on which it runs, how does an app 134 | discover if there is an already authenticated user that is accessing it? 135 | 136 | The current best practice answer is -- the app should do an Ajax/XHR HEAD 137 | request to the relevant resource: 138 | 139 | 1. either to the *current page* if it's a standalone app, or 140 | 2. to the requested resource (if it's an app that's acting as a viewer or 141 | editor, and requires a resource URI as a parameter) 142 | 143 | For the first case (standalone apps), solid-client provides a convenience 144 | `solid.currentUser()` method (which does a HEAD request to the current page in 145 | the background). Usage: 146 | 147 | ```js 148 | solid.currentUser() 149 | .then(function (currentWebId) { 150 | if (currentWebId) { 151 | console.log('Current WebID: %s', currentWebId) 152 | } else { 153 | console.log('You are not logged in') 154 | } 155 | }) 156 | ``` 157 | 158 | For the second case (apps that are wrapping a resource as viewers or editors), 159 | client apps can just use a `solid.login(targetUrl)` function to return the 160 | current user's WebID. And if users are unable to log in, prompt the user 161 | to create an account with `solid.signup()`. 162 | 163 | #### Login example 164 | 165 | Here is a typical example of authenticating a user and returning their WebID. 166 | The following `login` function, specific to your application, wraps the 167 | `solid.login()` function. If the promise is resolved, then an application 168 | will do something with the `webId` value. Otherwise, if the promise is rejected, 169 | the application may choose to display an error message. 170 | 171 | HTML: 172 | 173 | ```html 174 | Login 175 | ``` 176 | 177 | Javascript: 178 | 179 | ```javascript 180 | var solid = require('solid') 181 | var login = function() { 182 | // Get the current user 183 | solid.login().then(function (webId){ 184 | // authentication succeeded; do something with the WebID string 185 | console.log(webId) 186 | }).catch(function (err) { 187 | // authentication failed; display some error message 188 | console.log(err) 189 | }) 190 | } 191 | ``` 192 | 193 | ### Signup example 194 | 195 | The `signup` function is very similar to the `login` function, wrapping the 196 | `solid.signup()` function. It results in either a WebID or an error message 197 | being returned. 198 | 199 | HTML: 200 | 201 | ```html 202 | Sign up 203 | ``` 204 | 205 | Javascript: 206 | 207 | ```javascript 208 | var solid = require('solid') 209 | // Signup for a WebID 210 | var signup = function() { 211 | solid.signup() 212 | .then(function (webId) { 213 | // authentication succeeded; do something with the WebID string 214 | console.log(webId) 215 | }) 216 | .catch(function (err) { 217 | // authentication failed; display some error message 218 | console.log(err) 219 | }) 220 | } 221 | ``` 222 | 223 | ### User Profiles 224 | 225 | Once you have a user's WebID (say, from a `login()` call), it's often useful 226 | to load the user profile: 227 | 228 | ```javascript 229 | var profile = solid.login() 230 | .then(function (webId) { 231 | // have the webId, now load the profile 232 | return solid.getProfile(webId) 233 | }) 234 | ``` 235 | 236 | The call to `getProfile(url)` loads the full [extended 237 | profile](https://github.com/solid/solid-spec/blob/master/solid-webid-profiles.md#extended-profile): 238 | the profile document itself, any `sameAs` and `seeAlso` links it finds there, 239 | as well as the Preferences file. 240 | 241 | Once a profile is loaded, you can access the values of the profile's pre-defined 242 | fields, or look for predicates in the profile's parsed graph using 243 | `profile.find()` and `profile.findAll()`: 244 | 245 | ```js 246 | var ns = solid.vocab 247 | solid.login() 248 | .then(function (webId) { 249 | return solid.getProfile(webId) 250 | }) 251 | .then(function (profile) { 252 | profile.name // -> 'Alice' 253 | profile.picture // -> 'https://example.com/profile/icon.png' 254 | profile.find(ldp.solid('inbox')) // -> 'https://example.com/inbox/' 255 | profile.findAll(ns.owl('sameAs')) // -> [ url1, url2 ] 256 | }) 257 | .catch(function (err) { 258 | console.log('Error accessing profile: ' + err) 259 | }) 260 | ``` 261 | 262 | #### Profile App Registry 263 | 264 | The profile provides an interface to the user's App Registry. 265 | 266 | ```js 267 | var ns = solid.vocab 268 | solid.login() 269 | .then(function (webId) { 270 | return solid.getProfile(webId) 271 | }) 272 | .then(function (profile) { 273 | return profile.loadAppRegistry() 274 | }) 275 | .then(function (profile) { 276 | // The profile has been updated, app registry loaded. Now you can register 277 | // apps with is. 278 | var options = { 279 | name: 'Contact Manager', 280 | shortdesc: 'A reference contact manager', 281 | redirectTemplateUri: 'https://solid.github.io/contacts/?uri={uri}' 282 | } 283 | var typesForApp = [ ns.vcard('AddressBook') ] 284 | var isListed = true 285 | var app = new AppRegistration(options, typesForApp, isListed) 286 | return profile.registerApp(app) 287 | }) 288 | .then(function (profile) { 289 | // The app entry was created. You can now query the registry for it 290 | return profile.appsForType(ns.vcard('AddressBook')) 291 | }) 292 | .then(function (registrationResults) { 293 | var app = registrationResults[0] 294 | app.name // -> 'Contact Manager' 295 | app.shortdesc // -> ... 296 | app.redirectTemplateUri 297 | }) 298 | ``` 299 | 300 | #### User Type Registry Index 301 | 302 | If your application needs to do data discovery, it can also call 303 | `loadTypeRegistry()` after loading the profile: 304 | 305 | ```javascript 306 | var profile = solid.login() 307 | .then(function (webId) { 308 | return solid.getProfile(webId) 309 | }) 310 | .then(function (profile) { 311 | return profile.loadTypeRegistry() 312 | }) 313 | ``` 314 | 315 | Now, both listed and unlisted type indexes are loaded, and you can look up 316 | where the user keeps various types. 317 | 318 | ```js 319 | var ns = solid.vocab 320 | // .. load profile and load type registry 321 | 322 | var addressBookRegistrations = solid.getProfile(webId) 323 | .then(function (profile) { 324 | return profile.loadTypeRegistry() 325 | }) 326 | .then(function (profile) { 327 | return profile.typeRegistryForClass(ns.vcard('AddressBook')) 328 | }) 329 | /* 330 | --> 331 | [ 332 | an IndexRegistration( 333 | locationUri: 'https://localhost:8443/public-contacts/AddressBook.ttl', 334 | locationType: 'instance', 335 | isListed: true 336 | ), 337 | an IndexRegistration( 338 | locationUri: 'https://localhost:8443/personal-address-books/', 339 | locationType: 'container', 340 | isListed: false 341 | ) 342 | ] 343 | */ 344 | ``` 345 | 346 | You can then load the resources from the returned locations, as usual. 347 | 348 | ```js 349 | addressBookRegistrations.forEach(function (registration) { 350 | if (registration.isInstance()) { 351 | // This is an instance (an individual resource) 352 | } else if (registration.isContainer()) { 353 | // This is a container with many address books 354 | } 355 | }) 356 | ``` 357 | 358 | #### Registering (and un-registering) Types in the Type Registry 359 | 360 | To register an RDF Class with a user's Type Registry (listed or unlisted), 361 | use `profile.registerType()`: 362 | 363 | ```js 364 | var ns = solid.vocab 365 | // .. load profile 366 | 367 | var classToRegister = vocab.sioc('Post') 368 | var locationToRegister = 'https://localhost:8443/new-posts-container/' 369 | var isListed = true 370 | profile.registerType(classToRegister, locationToRegister, 'container', isListed) 371 | .then(function (profile) { 372 | // Now the type is registered, and the profile's type registry is refreshed 373 | // querying the registry now will include the new container 374 | profile.typeRegistryForClass(ns.sioc('Post')) 375 | }) 376 | 377 | // To remove the same class from registry: 378 | var classToRemove = ns.sioc('Post') 379 | profile.unregisterType(classToRemove, isListed) 380 | .then(function (profile) { 381 | // Type is removed 382 | profile.typeRegistryForClass(ns.sioc('Post')) // --> [] 383 | }) 384 | ``` 385 | 386 | ## Web operations 387 | 388 | solid-client uses a mix of [LDP](http://www.w3.org/TR/ldp/) and Solid-specific 389 | functions to manipulate Web resources. Please see the 390 | [Solid spec](https://github.com/solid/solid-spec) for more details. 391 | 392 | ### Getting information about a resource 393 | 394 | Sometimes an application may need to get some useful meta data about a resource. 395 | For instance, it may want to find out where the ACL resource is. Clients should 396 | take notice to the fact that the `solid.web.head()` function will always 397 | successfully complete, even for resources that don't exists, since that is 398 | considered useful information. For instance, clients can use the 399 | `solidResponse.xhr.status` value will indicate whether the resource exists or 400 | not. 401 | 402 | Here, for example, we can find out where the corresponding ACL resource is for 403 | our new blog post `hello-world`. 404 | 405 | ```javascript 406 | var solid = require('solid') 407 | var url = 'https://example.org/blog/hello-world' 408 | solid.web.head(url).then( 409 | function(solidResponse) { 410 | console.log(solidResponse.acl) // the ACL uri 411 | if (!solidResponse.exists()) { 412 | console.log("This resource doesn't exist") 413 | } else if (solidResponse.xhr.status === 403) { 414 | if (solidResponse.isLoggedIn()) { 415 | console.log("You don't have access to the resource") 416 | } else { 417 | console.log("Please authenticate") 418 | } 419 | } 420 | } 421 | ) 422 | ``` 423 | 424 | The `SolidResponse` object returned by most `solid.web` calls, including 425 | `head()`, contains the following properties: 426 | 427 | * `url` - the URL of the resource // `https://example.org/blog/hello-world` 428 | * `acl` - the URL of the corresponding .acl resource // 429 | `https://example.org/blog/hello-world.acl` 430 | * `meta` - the URL of the corresponding .meta resource // 431 | `https://example.org/blog/hello-world.meta` 432 | * `types` - An array of LDP types for the resource, if applicable. For example: 433 | `[ 'http://www.w3.org/ns/ldp#LDPResource', 434 | 'http://www.w3.org/ns/ldp#Resource' ]` 435 | * `user` - the WebID of the authenticated user (if authenticated) // 436 | `https://user.example.org/profile#me` 437 | * `websocket` - the URI of the corresponding websocket instance // 438 | `wss://example.org/blog/hello-world` 439 | * `method` - the HTTP verb (`get`, `put`, etc) of the original request that 440 | resulted in this response. 441 | * `xhr` - the raw XMLHttpRequest object (e.g. xhr.status) 442 | 443 | The response object also has some convenience methods: 444 | 445 | * `contentType()` - returns the MIME type of the resource 446 | * `isContainer()` - determines whether the resource is a Container or a regular 447 | resource 448 | 449 | ### Fetching a Resource 450 | 451 | Assuming that a resource or a container exists (see 452 | [creating resources](#creating-a-resource) and 453 | [creating containers](#creating-a-solid-container) below), you can retrieve 454 | it using `web.get()`: 455 | 456 | ```js 457 | solid.web.get(url) 458 | .then(function (response) { 459 | if (response.isContainer()) { 460 | // This is an instance of SolidContainer, see Listing Containers below 461 | for (resourceUrl in response.resources) { 462 | // iterate over resources 463 | } 464 | for (subcontainerUrl in response.containers) { 465 | // iterate over sub-containers 466 | } 467 | } else { 468 | // Regular resource 469 | console.log('Raw resource: %s', response.raw()) 470 | 471 | // You can access the parsed graph (parsed by RDFLib.js): 472 | var parsedGraph = response.parsedGraph() 473 | } 474 | }) 475 | .catch(function (err) { 476 | console.log(err) // error object 477 | // ... 478 | }) 479 | ``` 480 | 481 | #### Fetching a Parsed Graph 482 | 483 | Once a resource is retrieved, we can access it as a parsed graph (here, parsed 484 | by `rdflib.js`). This graph can then be queried. 485 | 486 | ```js 487 | var solid = require('solid') 488 | var vocab = solid.vocab 489 | 490 | var url = 'https://example.org/blog/hello-world' 491 | 492 | solid.web.get(url) 493 | .then(function(response) { 494 | var graph = response.parsedGraph() 495 | // Print all statements matching resources of type foaf:Post 496 | console.log(graph.statementsMatching(undefined, vocab.rdf('type'), 497 | vocab.sioc('Post'))) 498 | }) 499 | .catch(function(err) { 500 | console.log(err) // error object 501 | }) 502 | ``` 503 | 504 | ### Creating a Solid Container 505 | 506 | The Solid client offers a function called `solid.web.createContainer()`, 507 | which is used to create containers. The 508 | function accepts the following parameters: 509 | 510 | * `parentDir` (string) - the URL of the parent container in which the new 511 | resource/container will be created. 512 | * `containerName` (string) (optional) - the value for the `Slug` header -- i.e. the name 513 | of the new resource to be created; this value is optional. 514 | * `options` (object) - Optional hashmap of request options 515 | * `data` (string) - Optional RDF data serialized as `text/turtle`; can also be an empty 516 | string if no data will be sent. 517 | 518 | In the example below we are also sending some meta data (semantics) about the 519 | container, setting its type to `sioc:Blog`. 520 | 521 | ```javascript 522 | // Assumes you've loaded rdflib.js and solid-client, see Dependences above 523 | var solid = require('solid') 524 | var parentUrl = 'https://example.org/' 525 | var containerName = 'blog' 526 | var options = {} 527 | var data = '<#this> .' 528 | 529 | solid.web.createContainer(parentUrl, containerName, options, data).then( 530 | function(solidResponse) { 531 | console.log(solidResponse) 532 | // The resulting object has several useful properties. 533 | // See lib/solid/response.js for details 534 | // solidResponse.url - value of the Location header 535 | // solidResponse.acl - url of acl resource 536 | // solidResponse.meta - url of meta resource 537 | } 538 | ).catch(function(err){ 539 | console.log(err) // error object 540 | }) 541 | ``` 542 | 543 | Note that the `options` and `data` parameters are optional, and you can simply 544 | do `solid.web.createContainer(url, name)`. 545 | 546 | ### Listing a Solid Container 547 | 548 | To list the contents of a Solid container, just use `solid.web.get()`. 549 | This returns a promise that resolves to a `SolidContainer` instance, 550 | which will contain various useful properties: 551 | 552 | - A short name (`.name`) and absolute URI (`.uri`) 553 | - A `.parsedGraph` property for further RDF queries 554 | - A parsed list of links to all the contents (both containers and resources) 555 | (`.contentsUris`) 556 | - A list of RDF types to which the container belongs (`.types`) 557 | - A hashmap of all sub-Containers within this container, keyed by absolute uri 558 | (`.containers`) 559 | - A hashmap of all non-container Resources within this container, also keyed by 560 | absolute uri. (`.resources`) 561 | 562 | Containers also have several convenience methods: 563 | 564 | - `container.isEmpty()` will return `true` when there are no sub-containers or 565 | resources inside it 566 | - `container.findByType(rdfClass)` will return an array of resources or 567 | containers that have the given `rdfClass` in their `.types` array 568 | 569 | For example: 570 | 571 | ```js 572 | var container = solid.web.get('/settings/') 573 | .then(function (container) { 574 | console.log(container) 575 | // See below 576 | }) 577 | 578 | // container is an instance of SolidContainer (see lib/solid/container.js) 579 | container.uri // -> 'https://localhost:8443/settings/' 580 | container.name // -> 'settings' 581 | container.isEmpty() // -> false 582 | container.types // -> 583 | [ 584 | 'http://www.w3.org/ns/ldp#BasicContainer', 585 | 'http://www.w3.org/ns/ldp#Container' 586 | ] 587 | container.contentsUris // -> 588 | [ 589 | 'https://localhost:8443/settings/prefs.ttl', 590 | 'https://localhost:8443/settings/privateTypeIndex.ttl', 591 | 'https://localhost:8443/settings/testcontainer/' 592 | ] 593 | 594 | var subContainer = 595 | container.containers['https://localhost:8443/settings/testcontainer/'] 596 | subContainer.name // -> 'testcontainer' 597 | subContainer.types // -> 598 | [ 599 | 'http://www.w3.org/ns/ldp#BasicContainer', 600 | 'http://www.w3.org/ns/ldp#Container', 601 | 'http://www.w3.org/ns/ldp#Resource' 602 | ] 603 | 604 | var resource = 605 | container.resources['https://localhost:8443/settings/privateTypeIndex.ttl'] 606 | // resource - SolidResource instance 607 | resource.name // -> 'privateTypeIndex.ttl' 608 | resource.types // -> 609 | [ 610 | 'http://www.w3.org/ns/ldp#Resource', 611 | 'http://www.w3.org/ns/solid/terms#TypeIndex', 612 | 'http://www.w3.org/ns/solid/terms#UnlistedDocument' 613 | ] 614 | resource.isType('http://www.w3.org/ns/solid/terms#TypeIndex') // -> true 615 | 616 | container.findByType('http://www.w3.org/ns/ldp#Resource') // -> 617 | [ 618 | // a SolidContainer('testcontainer'), 619 | // a SolidResource('privateTypeIndex.ttl'), 620 | // a SolidResource('prefs.ttl') 621 | ] 622 | ``` 623 | 624 | ### Creating a resource 625 | 626 | Creating a regular LDP resource is done using the `web.post()` method. 627 | 628 | In this example we will create the resource `hello-world` under the newly 629 | created `blog/` container. 630 | 631 | ```javascript 632 | var solid = require('solid') 633 | var parentDir = 'https://example.org/blog/' 634 | var slug = 'hello-world' 635 | var data = ` 636 | <> a ; 637 | "First post" ; 638 | "Hello world! This is my first post" . 639 | ` 640 | 641 | solid.web.post(parentDir, data, slug) 642 | .then(function (response) { 643 | console.log(response.url) // URL of the newly created resource 644 | }) 645 | .catch(function (err){ 646 | console.log(err) // error object 647 | }) 648 | ``` 649 | 650 | ### Updating a resource 651 | 652 | Sometimes we need to update a resource after making a small change. For 653 | instance, we sometimes need to delete a triple, or update the value of an object 654 | (technically by replacing the triple with a new one). Luckily, Solid allows us 655 | to use the `HTTP PATCH` operation to do very small changes. 656 | 657 | Let's try to change the value of the title in our first post. To do so, we need 658 | to indicate which triple we want to replace, and then the triple that will 659 | replace it. 660 | 661 | Let's create the statements and serialize them to Turtle before patching the 662 | blog post resource: 663 | 664 | ```js 665 | var rdf = require('rdflib') 666 | var url = 'https://example.org/blog/hello-world' 667 | var vocab = ns.vocab 668 | 669 | var oldTitleTriple = rdf.triple(rdf.namedNode(url), ns.dct('title'), 670 | rdf.literal("First post")).toCanonical() 671 | 672 | var newTitleTriple = rdf.triple(rdf.namedNode(url), ns.dct('title'), 673 | rdf.literal("Hello")).toCanonical() 674 | ``` 675 | 676 | Now we can actually patch the resource. The `solid.web.patch()` function (also 677 | aliased to `solid.web.update()`) takes three arguments: 678 | 679 | * `url` (string) - the URL of the resource to be overwritten. 680 | * `toDel` (array) - an array of statements to be deleted, serialized as Turtle. 681 | * `toIns` (array) - an array of statements to be inserted, serialized as Turtle. 682 | 683 | ```javascript 684 | var solid = require('solid') 685 | var toDel = [ oldTitleTriple ] 686 | var toIns = [ newTitleTriple ] 687 | solid.web.patch(url, toDel, toIns) 688 | .then(function (response){ 689 | console.log(response.xhr.status) // HTTP 200 (OK) 690 | }) 691 | .catch(function(err) { 692 | console.log(err) // error object 693 | }) 694 | ``` 695 | 696 | ### Replacing a resource 697 | 698 | We can also completely replace (overwrite) existing resources with new content, 699 | using the client's `solid.web.put()` function (also aliased to `replace()`). The 700 | function accepts the following parameters: 701 | 702 | * `url` (string) - the URL of the resource to be overwritten. 703 | * `data` (string) - RDF data serialized as `text/turtle`; can also be an empty 704 | string if no data will be sent. 705 | * `mime` (string) (optional) - the mime type for this resource; this value is 706 | optional and defaults to `text/turtle`. 707 | 708 | Here is an example where we try to overwrite the existing resource 709 | `hello-world`, giving it a bogus type - `http://example.org/#Post`. 710 | 711 | ```javascript 712 | var solid = require('solid') 713 | var url = 'https://example.org/blog/hello-world' 714 | var data = '<> .' 715 | 716 | solid.web.put(url, data) 717 | .then(function (response) { 718 | console.log(response.xhr.status) // HTTP 200 (OK) 719 | }) 720 | .catch(function(err) { 721 | console.log(err) // error object 722 | }) 723 | ``` 724 | 725 | ### Deleting a resource 726 | 727 | Delete an RDF resource from the Web. For example, we can delete the blog post 728 | `hello-world` we created earlier, using the `solid.web.del()` function. 729 | 730 | **NOTE:** while this function can also be used to delete containers, it will 731 | only work for empty containers. For now, app developers should make sure to 732 | empty a container by recursively calling this function on its contents. 733 | 734 | ```javascript 735 | var solid = require('solid') 736 | var url = 'https://example.org/blog/hello-world' 737 | 738 | solid.web.del(url) 739 | .then(function (response) { 740 | console.log(response) 741 | }).catch(function (err) { 742 | console.log(err) // error object 743 | }) 744 | ``` 745 | 746 | ### Managing Resource Permissions 747 | 748 | Each Solid resource has a set of permissions that determine which user 749 | (identified by their WebID) has read and write access to it, called an 750 | *ACL resource*. 751 | (See the [`web-access-control-spec` repo](https://github.com/solid/web-access-control-spec) 752 | for the exact details.) 753 | 754 | solid-client has a set of convenience methods to help developers manage those 755 | permissions. 756 | 757 | #### Reading Permissions 758 | 759 | To load the corresponding ACL resource, for a given file: 760 | 761 | ```js 762 | var solid = require('solid') 763 | var resourceUrl = 'https://example.org/blog/hello-world' 764 | 765 | solid.getPermissions(resourceUrl) 766 | .then(function (permissionSet) { 767 | // Now the permission set, parsed from `hello-world.acl` is loaded, 768 | // and you can iterate over the individual authorizations 769 | permissionSet.forEach(function (auth) { 770 | if (auth.isAgent()) { 771 | console.log('agent webId: ' + auth.agent) 772 | } else if (auth.isPublic()) { 773 | // this permission is for everyone (acl:agentClass foaf:Agent) 774 | } else if (auth.isGroup()) { 775 | console.log('agentClass webId: ' + auth.group) 776 | } 777 | // You can also use auth.webId() for all cases: 778 | console.log('agent/group webId: ' + auth.webId()) 779 | // You can check what sort of access modes are granted: 780 | auth.allowsRead() // -> true if the authorization contains acl:Read mode 781 | auth.allowsWrite() 782 | auth.allowsAppend() 783 | auth.allowsControl() 784 | // Check to see if this Authorization is inherited (`acl:default`) 785 | auth.isInherited() // -> false for a resource, usually true for container 786 | // Check to see if access is allowed from a given Origin 787 | auth.allowsOrigin('https://example.com') 788 | }) 789 | }) 790 | ``` 791 | 792 | **Note:** You can read the permissions for a given resource *only* if you have 793 | `acl:Control` access mode for that resource. (You also need that access mode to 794 | edit those permissions, as well.) 795 | 796 | You can also access individual authorizations from a resource set: 797 | 798 | ```js 799 | solid.getPermissions(resourceUrl) 800 | .then(function (permissionSet) { 801 | var auth = permissionSet.permissionFor(bobWebId) 802 | auth.webId() // -> bob's web id 803 | auth.allowsRead() // -> true if bob has acl:Read permission 804 | auth.allModes() // -> array of access modes granted 805 | auth.allOrigins() // -> array of allowed origin URLs 806 | // If this is for the root container's ACL, you can also load a user's 807 | // emails using the `mailTo` property. (Unofficial functionality) 808 | auth.mailTo // -> ['bob@example.com', 'bob@gmail.com'] 809 | }) 810 | ``` 811 | 812 | #### Editing Permissions 813 | 814 | To manage the set of permissions for a given resource (provided the current 815 | user has `acl:Control` access mode granted to them for that resource), use 816 | the convenience methods provided by `PermissionSet`. 817 | 818 | The example below adds 3 different permissions: 819 | 820 | 1. Allows Alice to Read, Write and Control the resource 821 | 2. Allows Public Read access (that's the `solid.acl.EVERYONE`) 822 | 3. Grants Bob Write access (in addition to the Read access he inherits from 823 | the above permission, since he's a member of the Public). 824 | Also, this Write access is only allowed from a particular *origin*. 825 | 826 | ```js 827 | var solid = require('solid') 828 | var resourceUrl = 'https://example.org/blog/hello-world' 829 | var aliceWebId = 'https://alice.example.org/profile/card#me' 830 | var bobWebId = 'https://bob.example.org/profile/card#me' 831 | var allowedOrigin = 'https://example.org' 832 | 833 | solid.getPermissions(resourceUrl) 834 | .then(function (permissionSet) { 835 | return permissionSet 836 | .addPermission(aliceWebId, [solid.acl.READ, solid.acl.WRITE, 837 | solid.acl.CONTROL]) 838 | .addPermission(solid.acl.EVERYONE, solid.acl.READ) 839 | // see also .addGroupPermission() 840 | .addPermission(bobWebId, solid.acl.WRITE, allowedOrigin) 841 | .save() 842 | }) 843 | .then(function (response) { 844 | console.log('Permissions saved successfully') 845 | }) 846 | .catch(function (err) { 847 | console.log('Error saving permissions') 848 | }) 849 | ``` 850 | 851 | To *delete* all permissions associated with a resource, use 852 | `clearPermissions()`. Keep in mind that permissions are inherited from a 853 | resource's parent container, and if you delete an individual ACL resource, 854 | this simply means that the permissions reset to that of the upstream container. 855 | You can also clear the ACLs of the container, all the way up to the root storage 856 | container's ACL, which cannot be deleted. Refer to the 857 | [ACL Inheritance Algorithm](https://github.com/solid/web-access-control-spec#acl-inheritance-algorithm) 858 | section of the spec. 859 | 860 | ```js 861 | // If you have an existing PermissionSet as a result of `getPermissions()`: 862 | solid.getPermissions('https://www.example.com/file1') 863 | .then(function (permissionSet) { 864 | return permissionSet.clear() // deletes the file1.acl resource 865 | }) 866 | // Otherwise, use the helper function 867 | // solid.clearPermissions(resourceUrl) instead 868 | solid.clearPermissions('https://www.example.com/file1') 869 | .then(function (response) { 870 | // file1.acl is now deleted 871 | }) 872 | ``` 873 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Build the dist file based on individual modules 3 | # TODO: minify dist file 4 | 5 | npm run build 6 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides a simple configuration object for Solid web client and other 4 | * modules. 5 | * @module config 6 | */ 7 | module.exports = { 8 | /** 9 | * Default authentication endpoint 10 | */ 11 | authEndpoint: 'https://databox.me/', 12 | 13 | /** 14 | * Default signup endpoints (list of identity providers) 15 | */ 16 | signupEndpoint: 'https://solid.github.io/solid-idps/', 17 | 18 | /** 19 | * Default height of the Signup popup window, in pixels 20 | */ 21 | signupWindowHeight: 600, 22 | 23 | /** 24 | * Default width of the Signup popup window, in pixels 25 | */ 26 | signupWindowWidth: 1024 27 | } 28 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-bottom: 3em; 3 | } 4 | .demo-section { 5 | margin-top: 3em; 6 | border-top: 1px solid gray; 7 | } 8 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | solid.js Test Page 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

solid.js Demo Page

21 |

Note: Open up your browser's Developer Tools, as the demo makes use 22 | of console.log() events.

23 |
24 |
25 |

Login

26 |
27 |
28 | 29 | Detecting current user... 30 |
31 |

Log in using WebID+TLS:

32 |
33 | 34 | 35 |
36 | 37 | 39 |
40 |
41 |
42 |

Load Profile

43 |

Enter a WebID Profile URI (yours or someone else's).

44 |
45 |
46 |
47 |
48 | 49 | 51 |
52 | 53 | 55 |
56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 | 93 |

 94 |       
95 |
96 |
97 |
98 | 99 |
100 |
101 | 102 |
103 |
104 |
105 |
106 | 107 |
108 |
109 | 110 |
111 |
112 |
113 |
114 |
Type Registry
115 | 118 |
119 |
120 |
121 |
122 |

Read/Write/Delete

123 |
124 |
125 |
126 |
127 | 128 | 130 |
131 | 132 | 134 |
135 |
136 |
137 |
138 |
139 | 140 |
141 |
142 | 143 |
144 |
145 |
146 |
147 |

ACL

148 |
149 |
150 |
151 |
152 | 153 | 155 | 156 | 158 | 159 | 161 | 162 | 164 | 165 | 167 | 168 | 170 |
171 | 172 | 174 | 176 |
177 |
178 |
179 |
180 |
181 | 182 |
183 |
184 | 185 |
186 |
187 |
188 |
189 | 380 | 381 | 382 | -------------------------------------------------------------------------------- /docs/installing.md: -------------------------------------------------------------------------------- 1 | # Installing 2 | 3 | The solid client library can be installed in several ways. It's designed to be 4 | run in Node.js and the browser. There are three primary distributions: 5 | 6 | 1. The CommonJS module named `'solid-client'` 7 | 2. The bundled and minified `solid-client.min.js` 8 | 3. The bundled and minified `solid-client-no-rdflib.min.js` which does not 9 | include the (current) hard dependency on 10 | [`rdflib.js`](https://github.com/linkeddata/rdflib.js). This bundle is for 11 | developers that want to include `rdflib.js` themselves. 12 | 13 | ## Node.js 14 | 15 | Simply install solid-client through node and then `require('solid-client')` 16 | within your code! 17 | 18 | ```sh 19 | $ npm install solid-client --save 20 | ``` 21 | 22 | ```js 23 | var solid = require('solid-client') 24 | ``` 25 | 26 | ## Browser 27 | 28 | We offer a few ways to install the solid client. 29 | 30 | ### CDN 31 | 32 | If you don't need a module system, the simplest way to use the solid client in 33 | an app is to add the `solid-client.min.js` bundle to your page. 34 | 35 | ```html 36 | 37 | ``` 38 | 39 | If you're using the `solid-client-no-rdflib.min.js` bundle, you'll need to 40 | manually include its dependency on `rdflib.js`. 41 | 42 | ```html 43 | 44 | 45 | ``` 46 | 47 | ### browserify 48 | 49 | ```sh 50 | $ npm install solid-client --save 51 | ``` 52 | 53 | ```js 54 | var solid = require('solid-client') 55 | ``` 56 | 57 | ### webpack 58 | 59 | Using solid-client with webpack requires some configuration. You'll need the 60 | json-loader for webpack and will need to exclude the xhr2 and xmlhttprequest 61 | modules from the build. 62 | 63 | First install solid-client and json-loader: 64 | 65 | ```sh 66 | $ npm install solid-client --save 67 | $ npm install json-loader --save-dev 68 | ``` 69 | 70 | Then add the JSON loader and declare the xhr2 and xmlhttprequest externals in 71 | `webpack.config.js`: 72 | 73 | ```js 74 | module.exports = { 75 | // ... 76 | module: { 77 | loaders: [ 78 | { 79 | test: /\.json$/, 80 | loader: 'json' 81 | } 82 | ] 83 | }, 84 | externals: { 85 | xhr2: 'XMLHttpRequest', 86 | xmlhttprequest: 'XMLHttpRequest' 87 | }, 88 | // ... 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-client", 3 | "version": "0.24.3", 4 | "description": "Common library for writing Solid read-write-web applications", 5 | "main": "./lib/index.js", 6 | "files": [ 7 | "config.js", 8 | "lib", 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build-lib": "babel src -d lib", 13 | "build-full": "webpack --progress --config webpack.config.js --output-filename solid-client-full.js", 14 | "build-with-rdflib": "webpack --progress --colors --optimize-minimize --optimize-occurrence-order --optimize-dedupe --config webpack.config.js", 15 | "build-without-rdflib": "webpack --progress --colors --optimize-minimize --optimize-occurrence-order --optimize-dedupe --config webpack-no-rdflib.config.js", 16 | "build-qunit-resources": "npm run clean && mkdir -p dist/resources && npm run build-full && browserify -r ./test/resources/profile-minimal.js:test-minimal-profile -o dist/resources/test-minimal-profile.js && browserify -r ./test/resources/profile-private.js:test-minimal-prefs -o dist/resources/test-minimal-prefs.js", 17 | "build": "npm run clean && mkdir dist && npm run build-lib && npm run build-with-rdflib && npm run build-without-rdflib", 18 | "clean": "rm -rf dist/", 19 | "standard": "standard src/*", 20 | "tape": "tape test/unit/*.js", 21 | "test": "npm run standard && npm run tape", 22 | "qunit": "npm run build-qunit-resources && open test/integration/index.html", 23 | "preversion": "npm test", 24 | "postversion": "git push --follow-tags", 25 | "prepublish": "npm run test && npm run build" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/solid/solid-client" 30 | }, 31 | "keywords": [ 32 | "solid", 33 | "decentralized", 34 | "web", 35 | "rdf", 36 | "ldp", 37 | "linked", 38 | "data" 39 | ], 40 | "author": "Andrei Sambra ", 41 | "maintainers": [ 42 | { 43 | "name": "Dmitri Zagidulin", 44 | "url": "https://github.com/dmitrizagidulin/" 45 | } 46 | ], 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/solid/solid-client/issues" 50 | }, 51 | "homepage": "https://github.com/solid/solid-client", 52 | "dependencies": { 53 | "rdflib": "^0.13.0", 54 | "shorthash": "0.0.2", 55 | "solid-auth-oidc": "^0.1.2", 56 | "solid-auth-tls": "0.0.4", 57 | "solid-namespace": "^0.1.0", 58 | "solid-permissions": "^0.5.1", 59 | "solid-web-client": "^0.3.2" 60 | }, 61 | "devDependencies": { 62 | "babel-cli": "^6.18.0", 63 | "babel-loader": "^6.2.10", 64 | "babel-preset-es2015": "^6.18.0", 65 | "json-loader": "^0.5.4", 66 | "nock": "^9.0.2", 67 | "qunit": "^0.9.0", 68 | "sinon": "^2.1.0", 69 | "standard": "^5.4.1", 70 | "tape": "^4.4.0", 71 | "webpack": "^1.13.1" 72 | }, 73 | "standard": { 74 | "globals": [ 75 | "$rdf", 76 | "SolidClient", 77 | "tabulator", 78 | "QUnit" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app-registry.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides Solid helper functions involved with initializing, reading and 4 | * writing the App Registry resources. 5 | * @module app-registry 6 | */ 7 | 8 | module.exports.addToAppRegistry = addToAppRegistry 9 | module.exports.blankPrivateAppRegistry = blankPrivateAppRegistry 10 | module.exports.blankPublicAppRegistry = blankPublicAppRegistry 11 | module.exports.initAppRegistryPrivate = initAppRegistryPrivate 12 | module.exports.initAppRegistryPublic = initAppRegistryPublic 13 | module.exports.loadAppRegistry = loadAppRegistry 14 | module.exports.appsForType = appsForType 15 | module.exports.registerApp = registerApp 16 | module.exports.registrationsFromGraph = registrationsFromGraph 17 | 18 | var AppRegistration = require('./solid/app-registration') 19 | var graphUtil = require('./util/graph-util.js') 20 | var util = require('./util/web-util.js') 21 | var vocab = require('solid-namespace') 22 | var webUtil = require('./util/web-util.js') 23 | 24 | /** 25 | * Adds an RDF class to a user's app registry, and returns the 26 | * profile (with the appropriate registry graph updated). 27 | * Called by `registerApp()`, which does all the argument validation. 28 | * @method addToAppRegistry 29 | * @param profile {SolidProfile} 30 | * @param app {AppRegistration} 31 | * @param webClient {SolidWebClient} 32 | * @return {Promise} Returns updated profile 33 | */ 34 | function addToAppRegistry (profile, app, webClient) { 35 | // TODO: Check to see if a registry entry for this type already exists. 36 | var registryUri 37 | var registryGraph 38 | if (app.isListed) { 39 | registryUri = profile.appRegistryListed.uri 40 | registryGraph = profile.appRegistryListed.graph 41 | } else { 42 | registryUri = profile.appRegistryUnlisted.uri 43 | registryGraph = profile.appRegistryUnlisted.graph 44 | } 45 | if (!registryUri) { 46 | throw new Error('Cannot register app, registry URL missing') 47 | } 48 | var rdf = profile.rdf 49 | // triples to delete (none for the moment) 50 | var toDel = [] 51 | // Create the list of triples to add in the PATCH operation 52 | var toAdd = app.rdfStatements(rdf) 53 | return webClient.patch(registryUri, toDel, toAdd) 54 | .then(function (response) { 55 | // Update the profile object with the new registry without reloading 56 | var newRegistration = graphUtil.graphFromStatements(toAdd, rdf) 57 | if (registryGraph) { 58 | graphUtil.appendGraph(registryGraph, newRegistration) 59 | } else { 60 | profile[app.isListed ? 'appRegistryListed' : 'appRegistryUnlisted'].graph = newRegistration 61 | } 62 | return profile 63 | }) 64 | } 65 | 66 | /** 67 | * Returns a list of registry entries for a profile and a given RDF Class. 68 | * @method appsForType 69 | * @param profile {SolidProfile} 70 | * @param type {NamedNode} RDF Class 71 | * @param rdf {RDF} RDF Library 72 | * @return {Array} 73 | */ 74 | function appsForType (profile, type, rdf) { 75 | var registrations = [] 76 | return registrations 77 | .concat( 78 | // Public/listed registrations 79 | registrationsFromGraph(profile.appRegistryListed.graph, type, rdf) 80 | ) 81 | .concat( 82 | // Private/unlisted registrations 83 | registrationsFromGraph(profile.appRegistryUnlisted.graph, type, rdf) 84 | ) 85 | } 86 | 87 | /** 88 | * Returns a blank private app registry option. 89 | * For use with `initAppRegistry()`. 90 | * @method blankPrivateAppRegistry 91 | * @private 92 | * @return {Object} Blank app registry object 93 | */ 94 | function blankPrivateAppRegistry (rdf) { 95 | var ns = vocab(rdf) 96 | var thisDoc = rdf.namedNode('') 97 | var registryStatements = [ 98 | rdf.triple(thisDoc, ns.rdf('type'), ns.solid('AppRegistry')), 99 | rdf.triple(thisDoc, ns.rdf('type'), ns.solid('UnlistedDocument')) 100 | ] 101 | var registry = { 102 | data: graphUtil.serializeStatements(registryStatements), 103 | graph: graphUtil.graphFromStatements(registryStatements, rdf), 104 | slug: 'privateAppRegistry.ttl', 105 | uri: null // actual url not yet known 106 | } 107 | return registry 108 | } 109 | 110 | /** 111 | * Returns a blank public app registry option. 112 | * For use with `initAppRegistry()`. 113 | * @method blankPublicAppRegistry 114 | * @private 115 | * @return {Object} Blank app registry object 116 | */ 117 | function blankPublicAppRegistry (rdf) { 118 | var ns = vocab(rdf) 119 | var thisDoc = rdf.namedNode('') 120 | var registryStatements = [ 121 | rdf.triple(thisDoc, ns.rdf('type'), ns.solid('AppRegistry')), 122 | rdf.triple(thisDoc, ns.rdf('type'), ns.solid('ListedDocument')) 123 | ] 124 | var registry = { 125 | data: graphUtil.serializeStatements(registryStatements), 126 | graph: graphUtil.graphFromStatements(registryStatements, rdf), 127 | slug: 'publicAppRegistry.ttl', 128 | uri: null // actual url not yet known 129 | } 130 | return registry 131 | } 132 | 133 | /** 134 | * Initializes the private App Registry resource, updates 135 | * the profile with the initialized registry, and returns the updated profile. 136 | * @method initAppRegistryPrivate 137 | * @param profile {SolidProfile} User's WebID profile 138 | * @param [options={}] Options hashmap (see solid.web.solidRequest() 139 | * function docs) 140 | * @return {Promise} Resolves with the updated profile instance. 141 | */ 142 | function initAppRegistryPrivate (profile, webClient, options) { 143 | options = options || {} 144 | var rdf = profile.rdf 145 | var ns = vocab(rdf) 146 | var registryContainerUri = profile.appRegistryDefaultContainer() 147 | var webId = rdf.namedNode(profile.webId) 148 | var registry = blankPrivateAppRegistry(rdf) 149 | // First, create the private App Registry resource 150 | return webClient.post(registryContainerUri, registry.data, 151 | registry.slug) 152 | .catch(function (err) { 153 | throw new Error('Could not create private registry document:', err) 154 | }) 155 | .then(function (response) { 156 | // Private registry resource created. 157 | // Update the private profile (preferences) to link to it. 158 | registry.uri = util.absoluteUrl(webUtil.hostname(registryContainerUri), 159 | response.url) 160 | var toAdd = [ 161 | rdf.triple(webId, ns.solid('privateAppRegistry'), 162 | rdf.namedNode(registry.uri)) 163 | ] 164 | var toDel = [] 165 | // Note: this PATCH will actually create a private profile if it doesn't 166 | // already exist. 167 | return webClient.patch(profile.privateProfileUri(), toDel, toAdd, 168 | options) 169 | }) 170 | .catch(function (err) { 171 | throw new Error('Could not update profile with private registry:' + err) 172 | }) 173 | .then(function (response) { 174 | // Profile successfully patched with a link to the created private registry 175 | // It's safe to update this instance of profile 176 | profile.appRegistryUnlisted = registry 177 | // Finally, return the updated profile with registry loaded 178 | return profile 179 | }) 180 | } 181 | 182 | /** 183 | * Initializes the public App Registry resource, updates 184 | * the profile with the initialized registry, and returns the updated profile. 185 | * @method initAppRegistryPublic 186 | * @param profile {SolidProfile} User's WebID profile 187 | * @param [options] Options hashmap (see solid.web.solidRequest() function docs) 188 | * @return {Promise} Resolves with the updated profile instance. 189 | */ 190 | function initAppRegistryPublic (profile, webClient, options) { 191 | options = options || {} 192 | var rdf = profile.rdf 193 | var ns = vocab(rdf) 194 | var registryContainerUri = profile.appRegistryDefaultContainer() 195 | var webId = rdf.namedNode(profile.webId) 196 | var registry = blankPublicAppRegistry(rdf) 197 | // First, create the public registry Registry resource 198 | return webClient.post(registryContainerUri, registry.data, 199 | registry.slug) 200 | .catch(function (err) { 201 | throw new Error('Could not create public registry document:', err) 202 | }) 203 | .then(function (response) { 204 | // Public registry resource created. Update the profile to link to it. 205 | registry.uri = util.absoluteUrl(webUtil.hostname(registryContainerUri), 206 | response.url) 207 | var toAdd = [ 208 | rdf.triple(webId, ns.solid('publicAppRegistry'), 209 | rdf.namedNode(registry.uri)) 210 | ] 211 | var toDel = [] 212 | return webClient.patch(profile.webId, toDel, toAdd, options) 213 | }) 214 | .catch(function (err) { 215 | throw new Error('Could not update profile with public registry:', err) 216 | }) 217 | .then(function (response) { 218 | // Profile successfully patched with a link to the created public registry 219 | // It's safe to update this instance of profile 220 | profile.appRegistryListed = registry 221 | // Finally, return the updated profile with registry loaded 222 | return profile 223 | }) 224 | } 225 | 226 | /** 227 | * Loads the public and private app registry resources, adds them 228 | * to the profile, and returns the profile. 229 | * Called by the profile.loadAppRegistry() alias method. 230 | * Usage: 231 | * 232 | * ``` 233 | * var profile = solid.getProfile(url, options) 234 | * .then(function (profile) { 235 | * return profile.loadAppRegistry(options) 236 | * }) 237 | * ``` 238 | * @method loadAppRegistry 239 | * @param profile {SolidProfile} 240 | * @param webClient {SolidWebClient} 241 | * @param [options={}] Options hashmap (see solid.web.solidRequest() 242 | * function docs) 243 | * @return {Promise} 244 | */ 245 | function loadAppRegistry (profile, webClient, options) { 246 | options = options || {} 247 | options.headers = options.headers || {} 248 | // Politely ask for Turtle format 249 | if (!options.headers['Accept']) { 250 | options.headers['Accept'] = 'text/turtle' 251 | } 252 | // load public and private registry resources 253 | var links = [] 254 | if (profile.appRegistryListed.uri) { 255 | links.push(profile.appRegistryListed.uri) 256 | } 257 | if (profile.appRegistryUnlisted.uri) { 258 | links.push(profile.appRegistryUnlisted.uri) 259 | } 260 | return webClient.loadParsedGraphs(links, options) 261 | .then(function (loadedGraphs) { 262 | loadedGraphs.forEach(function (graph) { 263 | // For each registry resource loaded, add it to `profile.appRegistryListed` 264 | // or `profile.appRegistryUnlisted` as appropriate 265 | if (graph && graph.value) { 266 | profile.addAppRegistry(graph.value, graph.uri) 267 | } 268 | }) 269 | return profile 270 | }) 271 | } 272 | 273 | /** 274 | * Registers a given entry in the app registry. 275 | * @method registerApp 276 | * @param profile {SolidProfile} 277 | * @param app {AppRegistration} 278 | * @param webClient {SolidWebClient} 279 | * @return {Promise} Returns updated profile. 280 | */ 281 | function registerApp (profile, app, webClient) { 282 | if (!profile) { 283 | throw new Error('No profile provided') 284 | } 285 | if (!profile.isLoaded) { 286 | throw new Error('Profile is not loaded') 287 | } 288 | if (!app || !app.isValid()) { 289 | throw new Error('Invalid app registration') 290 | } 291 | // make sure app registry is loaded 292 | return loadAppRegistry(profile, webClient) 293 | .then(function (profile) { 294 | if (app.isListed && !profile.hasAppRegistryPublic()) { 295 | // Public App registry is needed, but doesn't exist. Create it. 296 | return initAppRegistryPublic(profile, webClient) 297 | } 298 | if (!app.isListed && !profile.hasAppRegistryPrivate()) { 299 | // Private App registry is needed, but doesn't exist. Create it. 300 | return initAppRegistryPrivate(profile, webClient) 301 | } 302 | // Relevant App registry exists, proceed 303 | return profile 304 | }) 305 | .then(function (profile) { 306 | // Made sure the relevant app registry exists, and can now add to it 307 | return addToAppRegistry(profile, app, webClient) 308 | }) 309 | } 310 | 311 | /** 312 | * Returns a list of registry entries from a given parsed type index graph. 313 | * @method registrationsFromGraph 314 | * @param graph {Graph} Parsed type index graph 315 | * @param type {NamedNode} RDF Class 316 | * @param rdf {RDF} RDF Library 317 | * @return {Array} 318 | */ 319 | function registrationsFromGraph (graph, type, rdf) { 320 | var entrySubject 321 | var ns = vocab(rdf) 322 | var registrations = [] 323 | if (!graph) { 324 | return registrations 325 | } 326 | graph.statementsMatching(null, ns.app('commonType'), type) 327 | .forEach(function (entry) { 328 | entrySubject = entry.subject 329 | var app = new AppRegistration() 330 | app.initFromGraph(entrySubject, graph, rdf) 331 | registrations.push(app) 332 | }) 333 | return registrations 334 | } 335 | -------------------------------------------------------------------------------- /src/identity.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides Solid helper functions involved with parsing a user's WebId profile. 4 | * @module identity 5 | */ 6 | module.exports.discoverWebID = discoverWebID 7 | module.exports.getProfile = getProfile 8 | module.exports.loadExtendedProfile = loadExtendedProfile 9 | 10 | var SolidProfile = require('./solid/profile') 11 | 12 | /** 13 | * Discovers a user's WebId (URL) starting from the account/domain URL. 14 | * Usage: 15 | * 16 | * ``` 17 | * solid.discoverWebID(url) 18 | * .then(function (webId) { 19 | * console.log('Web ID is: ' + webId) 20 | * }) 21 | * .catch(function (err) { 22 | * console.log('Could not discover web id: ' + err) 23 | * }) 24 | * ``` 25 | * @method discoverWebID 26 | * @param url {String} Location of a user's account or domain. 27 | * @throw {Error} Reason why the WebID could not be discovered 28 | * @return {Promise} 29 | */ 30 | function discoverWebID (url, webClient, ns) { 31 | return webClient.options(url) 32 | .then(function (response) { 33 | var metaUrl = response.metaAbsoluteUrl() 34 | if (!metaUrl) { 35 | throw new Error('Could not find a meta URL in the Link header') 36 | } 37 | return webClient.get(metaUrl) 38 | }) 39 | .then(function (response) { 40 | var graph = response.parsedGraph() 41 | var webId = graph.any(undefined, ns.solid('account')) 42 | if (!webId || !webId.uri) { 43 | throw new Error('Could not find a WebID matching the domain ' + url) 44 | } 45 | return webId 46 | }) 47 | } 48 | 49 | /** 50 | * Fetches a user's WebId profile, optionally follows `sameAs` etc links, 51 | * and return a promise with a parsed SolidProfile instance. 52 | * @method getProfile 53 | * @param webId {String} WebId 54 | * @param [options={}] Options hashmap (see solid.web.solidRequest() 55 | * function docs) 56 | * @param [options.ignoreExtended=false] Do not load extended profile if true. 57 | * @param webClient {SolidWebClient} 58 | * @param rdf {RDF} RDF Library 59 | * @return {Promise} 60 | */ 61 | function getProfile (webId, options, webClient, rdf) { 62 | options = options || {} 63 | // Politely ask for Turtle formatted profiles 64 | options.headers = options.headers || { 65 | 'Accept': 'text/turtle' 66 | } 67 | options.noCredentials = true // profiles are always public 68 | // Load main profile 69 | return webClient.get(webId, options) 70 | .then(function (response) { 71 | var parsedProfile = response.parsedGraph() 72 | var profile = new SolidProfile(response.url, parsedProfile, rdf, webClient, 73 | response) 74 | profile.isLoaded = true 75 | if (options.ignoreExtended) { 76 | return profile 77 | } else { 78 | return loadExtendedProfile(profile, options, webClient) 79 | } 80 | }) 81 | } 82 | 83 | /** 84 | * Loads the related external profile resources (all the `sameAs` and `seeAlso` 85 | * links, as well as Preferences), and appends them to the profile's 86 | * `parsedGraph`. Returns the profile instance. 87 | * @method loadExtendedProfile 88 | * @private 89 | * @param profile {SolidProfile} 90 | * @param [options] Options hashmap (see solid.web.solidRequest() function docs) 91 | * @return {Promise} 92 | */ 93 | function loadExtendedProfile (profile, options, webClient) { 94 | var links = profile.relatedProfilesLinks() 95 | return webClient.loadParsedGraphs(links, options) 96 | .then(function (loadedGraphs) { 97 | loadedGraphs.forEach(function (graph) { 98 | if (graph && graph.value) { 99 | profile.appendFromGraph(graph.value, graph.uri) 100 | } 101 | }) 102 | return profile 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015-2016 Solid 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | solid-client is a Javascript library for Solid applications. 25 | 26 | If you would like to know more about the solid Solid project, please see 27 | https://github.com/solid/solid 28 | */ 29 | 'use strict' 30 | /** 31 | * Provides a Solid client helper object (which exposes various static modules). 32 | * @module solid-client 33 | * @main solid-client 34 | */ 35 | 36 | const rdf = require('./util/rdf-parser') 37 | const ClientAuthOIDC = require('solid-auth-oidc') 38 | const auth = new ClientAuthOIDC() 39 | const webClient = require('solid-web-client')(rdf, { auth }) 40 | const ClientAuthTLS = require('solid-auth-tls') 41 | const tls = new ClientAuthTLS(webClient) 42 | const identity = require('./identity') 43 | const ns = require('solid-namespace')(rdf) 44 | const acl = require('solid-permissions') 45 | 46 | /** 47 | * @class Solid 48 | * @static 49 | */ 50 | const Solid = { 51 | acl, 52 | AppRegistration: require('./solid/app-registration'), 53 | appRegistry: require('./app-registry'), 54 | auth, 55 | tls, 56 | config: require('../config'), 57 | currentUser: tls.currentUser.bind(tls), 58 | identity: require('./identity'), 59 | login: tls.login.bind(tls), 60 | meta: require('./meta'), 61 | rdflib: rdf, 62 | signup: tls.signup.bind(tls), 63 | status: require('./status'), 64 | typeRegistry: require('./type-registry'), 65 | vocab: ns, 66 | web: webClient 67 | } 68 | 69 | Solid.clearPermissions = function clearPermissions (uri) { 70 | return acl.clearPermissions(uri, webClient) 71 | } 72 | Solid.discoverWebID = function discoverWebID (url) { 73 | return identity.discoverWebID(url, webClient, ns) 74 | } 75 | Solid.getPermissions = function getPermissions (uri) { 76 | return acl.getPermissions(uri, webClient, rdf) 77 | } 78 | Solid.getProfile = function getProfile (profileUrl, options) { 79 | return identity.getProfile(profileUrl, options, webClient, rdf) 80 | } 81 | 82 | module.exports = Solid 83 | -------------------------------------------------------------------------------- /src/meta.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides miscelaneous meta functions (such as library version) 4 | * @module meta 5 | */ 6 | var lib = require('../package') 7 | 8 | /** 9 | * Returns solid-client library version (read from `package.json`) 10 | * @return {String} Lib version 11 | */ 12 | module.exports.version = function version () { 13 | return lib.version 14 | } 15 | -------------------------------------------------------------------------------- /src/registry.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @module registry 4 | */ 5 | 6 | module.exports.isListed = isListed 7 | module.exports.isUnlisted = isUnlisted 8 | 9 | var vocab = require('solid-namespace') 10 | 11 | /** 12 | * Returns true if the parsed graph is a `solid:UnlistedDocument` document. 13 | * @method isUnlisted 14 | * @param graph {Graph} Parsed graph (loaded from a registry-like resource) 15 | * @return {Boolean} 16 | */ 17 | function isUnlisted (graph, rdf) { 18 | var ns = vocab(rdf) 19 | return graph.any(graph.uri, ns.rdf('type'), ns.solid('UnlistedDocument'), graph.uri) 20 | } 21 | 22 | /** 23 | * Returns true if the parsed graph is a `solid:ListedDocument` document. 24 | * @method isListed 25 | * @param graph {Graph} Parsed graph (loaded from a registry-like resource) 26 | * @return {Boolean} 27 | */ 28 | function isListed (graph, rdf) { 29 | var ns = vocab(rdf) 30 | return graph.any(graph.uri, ns.rdf('type'), ns.solid('ListedDocument'), graph.uri) 31 | } 32 | -------------------------------------------------------------------------------- /src/solid/app-registration.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @module app-registration 4 | */ 5 | module.exports = AppRegistration 6 | 7 | var hash = require('shorthash') 8 | var vocab = require('solid-namespace') 9 | var registry = require('../registry') 10 | 11 | /** 12 | * Represents a Solid App Registry registration (an entry in the App Registry). 13 | * Returned in a list by `profile.appForType()` 14 | * @class AppRegistration 15 | * @constructor 16 | * @param [options={}] {Object} Hashmap of app registration options. 17 | * @param [options.name] {String} App name (required for valid registration) 18 | * @param [options.shortdesc] {String} 19 | * @param [options.redirectTemplateUri] {String} 20 | * @param types {Array|Array} List of types / RDF classes for 21 | * which this app is registered. This app will be used to open those types 22 | * by Solid servers that support this functionality. 23 | * @param [isListed=false] {Boolean} Register in a listed or unlisted registry. 24 | */ 25 | function AppRegistration (options, types, isListed) { 26 | options = options || {} 27 | /** 28 | * Is this registered in a listed or unlisted registry 29 | * @property isListed 30 | * @type Boolean 31 | */ 32 | this.isListed = isListed 33 | /** 34 | * App name 35 | * @property name 36 | * @type String 37 | */ 38 | this.name = options.name 39 | /** 40 | * URI template that will be redirected to if the server gets a request 41 | * for one of the registered types. For example: 42 | * 'https://solid.github.io/contacts/?uri={uri}' 43 | * @property redirectTemplateUri 44 | * @type String 45 | */ 46 | this.redirectTemplateUri = options.redirectTemplateUri 47 | /** 48 | * Absolute URI (with fragment identifier) of the registration. 49 | * This is only set when this instance is created as a result of querying 50 | * the app registry. 51 | * @property registrationUri 52 | * @type String 53 | */ 54 | this.registrationUri = null 55 | /** 56 | * Short description of the app 57 | * @property shortdesc 58 | * @type String 59 | */ 60 | this.shortdesc = options.shortdesc 61 | /** 62 | * List of types / RDF classes for which this app is registered. 63 | * This app will be used to open those types by Solid servers that support 64 | * this functionality. 65 | * @property types 66 | * @type {Array|Array} 67 | */ 68 | this.types = types || [] 69 | } 70 | 71 | /** 72 | * Returns a unique hash fragment identifier for this registration (a hash of 73 | * the `redirectTemplateUri` property). 74 | * @method hashFragment 75 | * @return {String} 76 | */ 77 | AppRegistration.prototype.hashFragment = function hashFragment () { 78 | var fragmentId = hash.unique(this.redirectTemplateUri) 79 | return fragmentId 80 | } 81 | 82 | /** 83 | * Initializes the registration details from a parsed registry graph. 84 | * @method initFromGraph 85 | * @param subject {NamedNode} Hash fragment uri of the registration 86 | * @param graph {Graph} Parsed registry graph 87 | * @param rdf {RDF} RDF Library 88 | */ 89 | AppRegistration.prototype.initFromGraph = 90 | function initFromGraph (subject, graph, rdf) { 91 | this.registrationUri = subject.uri 92 | this.isListed = !!registry.isListed(graph, rdf) 93 | this.types = [] 94 | var self = this 95 | var ns = vocab(rdf) 96 | // Load the types 97 | graph.statementsMatching(subject, ns.app('commonType')) 98 | .forEach(function (typeStatement) { 99 | self.types.push(typeStatement.object.uri) 100 | }) 101 | var match 102 | match = graph.any(subject, ns.app('name')) 103 | if (match) { this.name = match.value } 104 | match = graph.any(subject, ns.app('shortdesc')) 105 | if (match) { this.shortdesc = match.value } 106 | match = graph.any(subject, ns.app('redirectTemplateUri')) 107 | if (match) { this.redirectTemplateUri = match.value } 108 | } 109 | 110 | /** 111 | * Is this a valid app registration entry that can be added to the registry? 112 | * (A registration is considered valid if it has a name, at least one type, 113 | * and a redirectUri) 114 | * @method isValid 115 | * @return {Boolean} 116 | */ 117 | AppRegistration.prototype.isValid = function isValid () { 118 | return this.name && this.redirectTemplateUri && this.types.length > 0 119 | } 120 | 121 | /** 122 | * Returns an array of RDF statements representing this app registration. 123 | * @method rdfStatements 124 | * @return {Array} List of RDF statements representing registration, 125 | * or an empty array if this registration is invalid. 126 | */ 127 | AppRegistration.prototype.rdfStatements = function rdfStatements (rdf) { 128 | var hashFragment = rdf.namedNode('#' + this.hashFragment()) 129 | var statements = [] 130 | var ns = vocab(rdf) 131 | // example: '<#ab09fd> a solid:AppRegistration;' 132 | statements.push( 133 | rdf.triple(hashFragment, ns.rdf('type'), ns.solid('AppRegistration')), 134 | rdf.triple(hashFragment, ns.app('name'), this.name), 135 | rdf.triple(hashFragment, ns.app('shortdesc'), this.shortdesc), 136 | rdf.triple(hashFragment, ns.app('redirectTemplateUri'), 137 | this.redirectTemplateUri) 138 | ) 139 | this.types.forEach(function (type) { 140 | statements.push( 141 | rdf.triple(hashFragment, ns.app('commonType'), type) 142 | ) 143 | }) 144 | 145 | return statements 146 | } 147 | -------------------------------------------------------------------------------- /src/solid/index-registration.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @module index-registration 4 | */ 5 | module.exports = IndexRegistration 6 | 7 | /** 8 | * Represents a Solid Index registration (an entry in the Type Index Registry). 9 | * Returned in a list by `profile.typeRegistryForClass()` 10 | * @class IndexRegistration 11 | * @constructor 12 | * @param registrationUri {String} Absolute URI (with fragment identifier) of 13 | * the registration (its location in the type index) 14 | * @param rdfClass {rdf.NamedNode} RDF Class for this registration 15 | * @param locationType {String} One of 'instance' or 'container' 16 | * @param locationUri {String} URI of the location containing resources of this 17 | * type 18 | * @param isListed {Boolean} Is this registration in a listed or unlisted index 19 | */ 20 | function IndexRegistration (registrationUri, rdfClass, locationType, 21 | locationUri, isListed) { 22 | /** 23 | * Is this a listed or unlisted registration 24 | * @property isListed 25 | * @type Boolean 26 | */ 27 | this.isListed = isListed 28 | /** 29 | * Location type, one of 'instance' or 'container' 30 | * @property locationType 31 | * @type String 32 | */ 33 | this.locationType = locationType 34 | /** 35 | * URI of the solid instance or container that holds resources of this type 36 | * @property locationUri 37 | * @type String 38 | */ 39 | this.locationUri = locationUri 40 | /** 41 | * RDF Class for this registration 42 | * @property rdfClass 43 | * @type rdf.NamedNode 44 | */ 45 | this.rdfClass = rdfClass 46 | /** 47 | * Absolute URI (with fragment identifier) of the registration 48 | * @property registrationUri 49 | * @type String 50 | */ 51 | this.registrationUri = registrationUri 52 | } 53 | 54 | /** 55 | * Convenience method, returns true if this registration is of type 56 | * `solid:instanceContainer` 57 | * @method isContainer 58 | * @return {Boolean} 59 | */ 60 | IndexRegistration.prototype.isContainer = function isInstance () { 61 | return this.locationType === 'container' 62 | } 63 | 64 | /** 65 | * Convenience method, returns true if this registration is of type 66 | * `solid:instance` 67 | * @method isInstance 68 | * @return {Boolean} 69 | */ 70 | IndexRegistration.prototype.isInstance = function isInstance () { 71 | return this.locationType === 'instance' 72 | } 73 | -------------------------------------------------------------------------------- /src/solid/profile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @module profile 4 | */ 5 | module.exports = SolidProfile 6 | 7 | var appRegistry = require('../app-registry') 8 | var vocab = require('solid-namespace') 9 | var registry = require('../registry') 10 | var typeRegistry = require('../type-registry') 11 | var graphUtil = require('../util/graph-util') 12 | var parseLinks = graphUtil.parseLinks 13 | 14 | var PREFERENCES_DEFAULT_URI = '/settings/prefs.ttl' 15 | var PROFILE_CONTAINER_DEFAULT_URI = '/profile/' 16 | 17 | /** 18 | * Provides convenience methods for a WebID Profile. 19 | * Used by `identity.getProfile()` 20 | * @class SolidProfile 21 | * @constructor 22 | */ 23 | function SolidProfile (profileUrl, parsedProfile, rdf, webClient, response) { 24 | /** 25 | * Listed (public) App Registry (link and parsed graph) 26 | * @property appRegistryListed 27 | * @type Object 28 | */ 29 | this.appRegistryListed = { 30 | uri: null, 31 | graph: null 32 | } 33 | /** 34 | * Unlisted (private) App Registry (link and parsed graph) 35 | * @property appRegistryUnlisted 36 | * @type Object 37 | */ 38 | this.appRegistryUnlisted = { 39 | uri: null, 40 | graph: null 41 | } 42 | /** 43 | * Main Inbox resource for this profile (link and parsed graph) 44 | * @property inbox 45 | * @type Object 46 | */ 47 | this.inbox = { 48 | uri: null, 49 | graph: null 50 | } 51 | /** 52 | * Has this profile been loaded? (Set in `identity.getProfile()`) 53 | * @property isLoaded 54 | * @type Boolean 55 | */ 56 | this.isLoaded = false 57 | /** 58 | * Profile owner's avatar / icon url. (Initialized in .appendFromGraph()) 59 | * @type String 60 | */ 61 | this.picture = null 62 | /** 63 | * Profile owner's name. (Initialized in .appendFromGraph()) 64 | * @property name 65 | * @type String 66 | */ 67 | this.name = null 68 | /** 69 | * RDF Library used by find(), parsedGraph(), etc. 70 | * @property rdf 71 | * @type RDF 72 | */ 73 | this.rdf = rdf 74 | /** 75 | * Links to root storage containers (read/write dataspaces for this profile) 76 | * @property storage 77 | * @type Array 78 | */ 79 | this.storage = [] 80 | /** 81 | * Listed (public) Type registry index (link and parsed graph) 82 | * @property typeIndexListed 83 | * @type Object 84 | */ 85 | this.typeIndexListed = { 86 | uri: null, 87 | graph: null 88 | } 89 | /** 90 | * Unlisted (private) Type registry index (link and parsed graph) 91 | * @property typeIndexUnlisted 92 | * @type Object 93 | */ 94 | this.typeIndexUnlisted = { 95 | uri: null, 96 | graph: null 97 | } 98 | /** 99 | * Parsed graph of the extended WebID Profile document. 100 | * Included the WebID profile, preferences, and related profile graphs 101 | * @property parsedGraph 102 | * @type Graph 103 | */ 104 | this.parsedGraph = null 105 | /** 106 | * Profile preferences object (link and parsed graph). 107 | * Currently used as a 'Private Profile', and is part of the Extended Profile. 108 | * @property preferences 109 | * @type Object 110 | */ 111 | this.preferences = { 112 | uri: null, 113 | graph: null 114 | } 115 | /** 116 | * SolidResponse instance from which this profile object was created. 117 | * Contains the raw profile source, the XHR object, etc. 118 | * @property response 119 | * @type SolidResponse 120 | */ 121 | this.response = response 122 | /** 123 | * Links to "see also" profile documents. Typically loaded immediately 124 | * after retrieving the initial WebID Profile document. 125 | * @property relatedProfiles 126 | * @type Object 127 | */ 128 | this.relatedProfiles = { 129 | sameAs: [], 130 | seeAlso: [] 131 | } 132 | /** 133 | * WebId URL (the `foaf:primaryTopic` of the profile document) 134 | * @property webId 135 | * @type String 136 | */ 137 | this.webId = null 138 | /** 139 | * Web client (for use with loadProfile() etc) 140 | * @type SolidWebClient 141 | */ 142 | this.webClient = webClient 143 | 144 | if (!profileUrl) { 145 | return 146 | } 147 | /** 148 | * Location of the base WebID Profile document (minus the hash fragment). 149 | * @property baseProfileUrl 150 | * @type String 151 | */ 152 | this.baseProfileUrl = (profileUrl.indexOf('#') >= 0) 153 | ? profileUrl.slice(0, profileUrl.indexOf('#')) 154 | : profileUrl 155 | 156 | if (parsedProfile) { 157 | this.initWebId(parsedProfile) 158 | this.appendFromGraph(parsedProfile, this.baseProfileUrl) 159 | } 160 | } 161 | 162 | /** 163 | * Update the profile based on a parsed graph, which can be either the 164 | * initial WebID profile, or the various extended profile graphs 165 | * (such as the seeAlso, sameAs and preferences links) 166 | * @method appendFromGraph 167 | * @private 168 | * @param parsedProfile {Graph} RDFLib-parsed user profile 169 | * @param profileUrl {String} URL of this particular parsed graph 170 | */ 171 | SolidProfile.prototype.appendFromGraph = 172 | function appendFromGraph (parsedProfile, profileUrl) { 173 | if (!parsedProfile) { 174 | return 175 | } 176 | var rdf = this.rdf 177 | var ns = vocab(rdf) 178 | this.parsedGraph = this.parsedGraph || rdf.graph() // initialize if null 179 | // Add the graph of this parsedProfile to the existing graph 180 | graphUtil.appendGraph(this.parsedGraph, parsedProfile, profileUrl) 181 | 182 | var webId = rdf.namedNode(this.webId) 183 | var links 184 | 185 | // Load the profile owner's name and avatar/icon url 186 | if (!this.name) { 187 | this.name = this.find(ns.foaf('name')) 188 | } 189 | if (!this.picture) { 190 | this.picture = this.find(ns.foaf('img')) 191 | } 192 | // Add sameAs and seeAlso 193 | links = parseLinks(parsedProfile, null, ns.owl('sameAs')) 194 | this.relatedProfiles.sameAs = this.relatedProfiles.sameAs.concat(links) 195 | 196 | links = parseLinks(parsedProfile, null, ns.rdfs('seeAlso')) 197 | this.relatedProfiles.seeAlso = this.relatedProfiles.seeAlso.concat(links) 198 | 199 | // Add preferencesFile link (singular). Note that preferencesFile has 200 | // Write-Once semantics -- it's initialized from public profile, but 201 | // cannot be overwritten by related profiles 202 | if (!this.preferences.uri) { 203 | this.preferences.uri = parseLink(parsedProfile, webId, 204 | ns.pim('preferencesFile')) 205 | } 206 | // Init inbox (singular). Note that inbox has 207 | // Write-Once semantics -- it's initialized from public profile, but 208 | // cannot be overwritten by related profiles 209 | if (!this.inbox.uri) { 210 | this.inbox.uri = parseLink(parsedProfile, webId, 211 | ns.solid('inbox')) 212 | } 213 | 214 | // Add storage 215 | links = parseLinks(parsedProfile, webId, ns.pim('storage')) 216 | this.storage = 217 | this.storage.concat(links) 218 | 219 | // Add links to Listed and Unlisted Type Indexes. 220 | // Note: these are just the links. 221 | // The actual index files will be loaded and parsed 222 | // in `profile.loadTypeRegistry()`) 223 | if (!this.typeIndexListed.uri) { 224 | this.typeIndexListed.uri = parseLink(parsedProfile, webId, 225 | ns.solid('publicTypeIndex')) 226 | } 227 | if (!this.typeIndexUnlisted.uri) { 228 | this.typeIndexUnlisted.uri = parseLink(parsedProfile, webId, 229 | ns.solid('privateTypeIndex')) 230 | } 231 | 232 | // Add links to Listed and Unlisted App Registry resources. 233 | // Note: these are just the links. 234 | // The actual index files will be loaded and parsed 235 | // in `profile.loadAppRegistry()`) 236 | if (!this.appRegistryListed.uri) { 237 | this.appRegistryListed.uri = parseLink(parsedProfile, webId, 238 | ns.solid('publicAppRegistry')) 239 | } 240 | if (!this.appRegistryUnlisted.uri) { 241 | this.appRegistryUnlisted.uri = parseLink(parsedProfile, webId, 242 | ns.solid('privateAppRegistry')) 243 | } 244 | } 245 | 246 | /** 247 | * Returns the default location of the container in which the App Registry 248 | * resources will reside. (Uses the same container as the profile 249 | * document.) 250 | * @method appRegistryDefaultContainer 251 | * @return {String} 252 | */ 253 | SolidProfile.prototype.appRegistryDefaultContainer = 254 | function appRegistryDefaultContainer () { 255 | var profileUri = this.webId || this.baseProfileUrl 256 | var baseContainer 257 | if (profileUri) { 258 | baseContainer = profileUri.replace(/\\/g, '/').replace(/\/[^\/]*\/?$/, '') + '/' 259 | } else { 260 | baseContainer = PROFILE_CONTAINER_DEFAULT_URI 261 | } 262 | return baseContainer 263 | } 264 | 265 | /** 266 | * Returns a list of registry entries for a given RDF Class. 267 | * @method appsForType 268 | * @param type {NamedNode} RDF Class 269 | * @return {Array} 270 | */ 271 | SolidProfile.prototype.appsForType = function appsForType (type) { 272 | return appRegistry.appsForType(this, type, this.rdf) 273 | } 274 | 275 | /** 276 | * Returns the value of a given "field" (predicate) from the profile's parsed 277 | * graph. If there are more than one matches for this predicate, .find() 278 | * returns the first one. If there are no matches, `null` is returned. 279 | * Usage: 280 | * 281 | * ``` 282 | * var inboxUrl = profile.find(ns.solid('inbox')) 283 | * if (inboxUrl) { 284 | * console.log('Inbox is located at:', inboxUrl) 285 | * } 286 | * ``` 287 | * @method find 288 | * @param predicate {NamedNode} RDF named node of the predicate 289 | * @return {String|Null} String value (or uri) 290 | */ 291 | SolidProfile.prototype.find = function find (predicate) { 292 | if (!this.parsedGraph) { 293 | throw new Error('Profile graph not yet loaded.') 294 | } 295 | var subject = this.rdf.namedNode(this.webId) 296 | var result = this.parsedGraph.any(subject, predicate) 297 | if (!result) { 298 | return result 299 | } 300 | return result.value || result.uri 301 | } 302 | 303 | /** 304 | * Returns all values of a given "field" (predicate) from the profile's parsed 305 | * graph. 306 | * Usage: 307 | * 308 | * ``` 309 | * var related = profile.findAll(vocab.owl('sameAs')) 310 | * ``` 311 | * @method findAll 312 | * @param predicate {NamedNode} RDF named node of the predicate 313 | * @return {Array} Array of string values/uris 314 | */ 315 | SolidProfile.prototype.findAll = function findAll (predicate) { 316 | if (!this.parsedGraph) { 317 | throw new Error('Profile graph not yet loaded.') 318 | } 319 | var subject = this.rdf.namedNode(this.webId) 320 | var matches = this.parsedGraph.statementsMatching(subject, predicate) 321 | matches = matches.map(function (ea) { 322 | return ea.object.value || ea.object.uri 323 | }) 324 | return matches.sort() 325 | } 326 | 327 | /** 328 | * Extracts the WebID from a parsed profile graph and initializes it. 329 | * Should only be done once (when creating a new SolidProfile instance) 330 | * @method initWebId 331 | * @param parsedProfile {Graph} RDFLib-parsed user profile 332 | */ 333 | SolidProfile.prototype.initWebId = function initWebId (parsedProfile) { 334 | if (!parsedProfile) { 335 | return 336 | } 337 | try { 338 | this.webId = extractWebId(this.baseProfileUrl, parsedProfile, 339 | this.rdf).uri 340 | } catch (e) { 341 | throw new Error('Unable to parse WebID from profile: ' + e) 342 | } 343 | } 344 | 345 | /** 346 | * Returns an array of related external profile links (sameAs and seeAlso and 347 | * Preferences files) 348 | * @method relatedProfilesLinks 349 | * @return {Array} 350 | */ 351 | SolidProfile.prototype.relatedProfilesLinks = function relatedProfilesLinks () { 352 | var links = [] 353 | links = links.concat(this.relatedProfiles.sameAs) 354 | .concat(this.relatedProfiles.seeAlso) 355 | if (this.preferences.uri) { 356 | links = links.concat(this.preferences.uri) 357 | } 358 | return links 359 | } 360 | 361 | /** 362 | * Returns whether or not the profile has a private (unlisted) App Registry 363 | * associated with it (linked to from the profile document). 364 | * @method hasAppRegistryPrivate 365 | * @throws {Error} If the profile has not been loaded (via getProfile()). 366 | * @return {Boolean} Returns truthy value if the private (unlisted) app registry 367 | * exists (that is, has a link in the profile). 368 | */ 369 | SolidProfile.prototype.hasAppRegistryPrivate = 370 | function hasAppRegistryPrivate () { 371 | if (!this.isLoaded) { 372 | throw new Error('Must load profile before checking if registry exists.') 373 | } 374 | return this.appRegistryUnlisted.uri 375 | } 376 | 377 | /** 378 | * Returns whether or not the profile has a public (listed) App Registry 379 | * associated with it (linked to from the profile document). 380 | * @method hasAppRegistryPublic 381 | * @throws {Error} If the profile has not been loaded (via getProfile()). 382 | * @return {Boolean} Returns truthy value if the public (listed) app registry 383 | * exists (that is, has a link in the profile). 384 | */ 385 | SolidProfile.prototype.hasAppRegistryPublic = 386 | function hasAppRegistryPublic () { 387 | if (!this.isLoaded) { 388 | throw new Error('Must load profile before checking if registry exists.') 389 | } 390 | return this.appRegistryListed.uri 391 | } 392 | 393 | /** 394 | * Returns true if the profile has any links to root storage 395 | * @method hasStorage 396 | * @return {Boolean} 397 | */ 398 | SolidProfile.prototype.hasStorage = function hasStorage () { 399 | return this.storage && this.storage.length > 0 400 | } 401 | 402 | /** 403 | * Returns whether or not the profile has a private (unlisted) Type Index 404 | * Registry associated with it (linked to from the profile document). 405 | * @method hasTypeRegistryPrivate 406 | * @throws {Error} If the profile has not been loaded (via getProfile()). 407 | * @return {Boolean} Returns truthy value if the private (unlisted) type index 408 | * registry exists (that is, has a link in the profile). 409 | */ 410 | SolidProfile.prototype.hasTypeRegistryPrivate = 411 | function hasTypeRegistryPrivate () { 412 | if (!this.isLoaded) { 413 | throw new Error('Must load profile before checking if registry exists.') 414 | } 415 | return this.typeIndexUnlisted.uri 416 | } 417 | 418 | /** 419 | * Returns whether or not the profile has a public (listed) Type Index Registry 420 | * associated with it (linked to from the profile document). 421 | * @method hasTypeRegistryPublic 422 | * @throws {Error} If the profile has not been loaded (via getProfile()). 423 | * @return {Boolean} Returns truthy value if the public (listed) type index 424 | * registry exists (that is, has a link in the profile). 425 | */ 426 | SolidProfile.prototype.hasTypeRegistryPublic = 427 | function hasTypeRegistryPublic () { 428 | if (!this.isLoaded) { 429 | throw new Error('Must load profile before checking if registry exists.') 430 | } 431 | return this.typeIndexListed.uri 432 | } 433 | 434 | /** 435 | * Convenience method to load the app registry. Usage: 436 | * 437 | * ``` 438 | * Solid.getProfile(url, options) 439 | * .then(function (profile) { 440 | * return profile.loadAppRegistry(webClient, options) 441 | * }) 442 | * ``` 443 | * @method loadAppRegistry 444 | * @param [options] Options hashmap (see Solid.web.solidRequest() function docs) 445 | * @return {Promise} 446 | */ 447 | SolidProfile.prototype.loadAppRegistry = 448 | function loadAppRegistry (webClient, options) { 449 | webClient = webClient || this.webClient 450 | return appRegistry.loadAppRegistry(this, webClient, options) 451 | } 452 | 453 | /** 454 | * Convenience method to load the type index registry. Usage: 455 | * 456 | * ``` 457 | * Solid.getProfile(url, options) 458 | * .then(function (profile) { 459 | * return profile.loadTypeRegistry(options) 460 | * }) 461 | * ``` 462 | * @method loadTypeRegistry 463 | * @param webClient {SolidWebClient} 464 | * @param [options] Options hashmap (see Solid.web.solidRequest() function docs) 465 | * @return {Promise} 466 | */ 467 | SolidProfile.prototype.loadTypeRegistry = 468 | function loadTypeRegistry (webClient, options) { 469 | webClient = webClient || this.webClient 470 | return typeRegistry.loadTypeRegistry(this, webClient, options) 471 | } 472 | 473 | /** 474 | * Adds a parsed app registry graph to the appropriate registry (public 475 | * or private). (Used when parsing the extended profile). 476 | * @method addAppRegistry 477 | * @private 478 | * @param graph {Graph} Parsed graph (loaded from an app registry resource) 479 | * @param uri {String} Location of the app registry document 480 | */ 481 | SolidProfile.prototype.addAppRegistry = function addAppRegistry (graph, uri) { 482 | // Is this a public app registry? 483 | if (registry.isListed(graph, this.rdf)) { 484 | if (!this.appRegistryListed.graph) { // only initialize once 485 | this.appRegistryListed.uri = uri 486 | this.appRegistryListed.graph = graph 487 | } 488 | } else if (registry.isUnlisted(graph, this.rdf)) { 489 | if (!this.appRegistryUnlisted.graph) { 490 | this.appRegistryUnlisted.uri = uri 491 | this.appRegistryUnlisted.graph = graph 492 | } 493 | } else { 494 | console.log(graph) 495 | throw new Error('Attempting to add an invalid app registry resource') 496 | } 497 | } 498 | 499 | /** 500 | * Adds a parsed type index graph to the appropriate type registry (public 501 | * or private). (Used when parsing the extended profile). 502 | * @method addTypeRegistry 503 | * @private 504 | * @param graph {Graph} Parsed graph (loaded from a type index 505 | * resource) 506 | * @param uri {String} Location of the type registry index document 507 | */ 508 | SolidProfile.prototype.addTypeRegistry = 509 | function addTypeRegistry (graph, uri) { 510 | // Is this a public type registry? 511 | if (registry.isListed(graph, this.rdf)) { 512 | if (!this.typeIndexListed.graph) { // only initialize once 513 | this.typeIndexListed.uri = uri 514 | this.typeIndexListed.graph = graph 515 | } 516 | } else if (registry.isUnlisted(graph, this.rdf)) { 517 | if (!this.typeIndexUnlisted.graph) { 518 | this.typeIndexUnlisted.uri = uri 519 | this.typeIndexUnlisted.graph = graph 520 | } 521 | } else { 522 | throw new Error('Attempting to add an invalid type registry index') 523 | } 524 | } 525 | 526 | /** 527 | * Reloads the contents of the profile's App Registry resources. 528 | * @method reloadAppRegistry 529 | * @return {Promise} 530 | */ 531 | SolidProfile.prototype.reloadAppRegistry = 532 | function reloadAppRegistry (webClient) { 533 | this.resetAppRegistry() 534 | return this.loadAppRegistry(webClient) 535 | } 536 | 537 | /** 538 | * Reloads the contents of the profile's Type Index registries. 539 | * @method reloadTypeRegistry 540 | * @return {Promise} 541 | */ 542 | SolidProfile.prototype.reloadTypeRegistry = 543 | function reloadTypeRegistry (webClient) { 544 | this.resetTypeRegistry() 545 | return this.loadTypeRegistry(webClient) 546 | } 547 | 548 | /** 549 | * Resets the contents (graphs) of the profile's App Registry resources to null. 550 | * Used internally by `reloadAppRegistry()`. 551 | * @method resetAppRegistry 552 | * @private 553 | */ 554 | SolidProfile.prototype.resetAppRegistry = function resetAppRegistry () { 555 | this.appRegistryListed.graph = null 556 | this.appRegistryUnlisted.graph = null 557 | } 558 | 559 | /** 560 | * Resets the contents (graphs) of the profile's Type Index registries to null. 561 | * Used internally by `reloadTypeRegistry()`. 562 | * @method resetTypeRegistry 563 | * @private 564 | */ 565 | SolidProfile.prototype.resetTypeRegistry = function resetTypeRegistry () { 566 | this.typeIndexListed.graph = null 567 | this.typeIndexUnlisted.graph = null 568 | } 569 | 570 | /** 571 | * Returns lists of registry entries for a given RDF Class. 572 | * @method typeRegistryForClass 573 | * @param rdfClass {rdf.NamedNode} RDF Class symbol 574 | * @return {Array} 575 | */ 576 | SolidProfile.prototype.typeRegistryForClass = 577 | function typeRegistryForClass (rdfClass) { 578 | return typeRegistry.typeRegistryForClass(this, rdfClass, this.rdf) 579 | } 580 | 581 | /** 582 | * Returns the default location of the container in which the Type Registry 583 | * Index resources will reside. (Uses the same container as the profile 584 | * document.) 585 | * @method typeRegistryDefaultContainer 586 | * @return {String} 587 | */ 588 | SolidProfile.prototype.typeRegistryDefaultContainer = 589 | function typeRegistryDefaultContainer () { 590 | var profileUri = this.webId || this.baseProfileUrl 591 | var baseContainer 592 | if (profileUri) { 593 | baseContainer = profileUri.replace(/\\/g, '/').replace(/\/[^\/]*\/?$/, '') + '/' 594 | } else { 595 | baseContainer = PROFILE_CONTAINER_DEFAULT_URI 596 | } 597 | return baseContainer 598 | } 599 | 600 | /** 601 | * Returns the relative URL of the private profile (preferences) resource. 602 | * @method privateProfileUri 603 | * @return {String} 604 | */ 605 | SolidProfile.prototype.privateProfileUri = function privateProfileUri () { 606 | if (this.preferences && this.preferences.uri) { 607 | return this.preferences.uri 608 | } else { 609 | return PREFERENCES_DEFAULT_URI 610 | } 611 | } 612 | 613 | /** 614 | * Registers a given entry in the app registry. 615 | * @method registerApp 616 | * @param app {AppRegistration} 617 | * @return {Promise} Returns updated profile. 618 | */ 619 | SolidProfile.prototype.registerApp = function registerApp (app, webClient) { 620 | webClient = webClient || this.webClient 621 | return appRegistry.registerApp(this, app, webClient) 622 | } 623 | 624 | /** 625 | * Registers a given RDF class in the user's type index registries, so that 626 | * other applications can discover it. 627 | * @method registerType 628 | * @param rdfClass {rdf.NamedNode} Type to register in the index. 629 | * @param location {String} Absolute URI to the location you want the class 630 | * registered to. (Example: Registering Address books in 631 | * `https://example.com/contacts/`) 632 | * @param [locationType='container'] {String} Either 'instance' or 'container', 633 | * defaults to 'container' 634 | * @param [isListed=false] {Boolean} Whether to register in a listed or unlisted 635 | * index). Defaults to `false` (unlisted). 636 | * @return {Promise} 637 | */ 638 | SolidProfile.prototype.registerType = 639 | function registerType (rdfClass, location, locationType, isListed) { 640 | return typeRegistry.registerType(this, rdfClass, location, locationType, 641 | isListed, this.webClient) 642 | } 643 | 644 | /** 645 | * Removes a given RDF class from the user's type index registry 646 | * @method unregisterType 647 | * @param rdfClass {NamedNode} Type to register in the index. 648 | * @param [isListed=false] {Boolean} Whether to register in a listed or unlisted 649 | * index). Defaults to `false` (unlisted). 650 | * @param [location] {String} If present, only unregister the class from this 651 | * location (absolute URI). 652 | * @return {Promise} 653 | */ 654 | SolidProfile.prototype.unregisterType = 655 | function unregisterType (rdfClass, isListed, location) { 656 | return typeRegistry.unregisterType(this, rdfClass, isListed, location, 657 | this.webClient) 658 | } 659 | 660 | /** 661 | * Extracts the WebID symbol from a parsed profile graph. 662 | * @method extractWebId 663 | * @param baseProfileUrl {String} Profile URL, with no hash fragment 664 | * @param parsedProfile {Graph} RDFLib-parsed user profile 665 | * @return {NamedNode} WebID symbol 666 | */ 667 | function extractWebId (baseProfileUrl, parsedProfile, rdf) { 668 | var ns = vocab(rdf) 669 | var subj = rdf.namedNode(baseProfileUrl) 670 | var pred = ns.foaf('primaryTopic') 671 | var match = parsedProfile.any(subj, pred) 672 | return match 673 | } 674 | 675 | /** 676 | * Extracts the first URI from a parsed graph that matches parameters 677 | * @method parseLinks 678 | * @param graph {Graph} 679 | * @param subject {NamedNode} 680 | * @param predicate {NamedNode} 681 | * @param object {NamedNode} 682 | * @param source {NamedNode} 683 | * @return {String} URI that matches the parameters 684 | */ 685 | function parseLink (graph, subject, predicate, object, source) { 686 | var first = graph.any(subject, predicate, object, source) 687 | if (first) { 688 | return first.uri 689 | } else { 690 | return null 691 | } 692 | } 693 | -------------------------------------------------------------------------------- /src/status.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides Web API helpers dealing with a user's online / offline status. 4 | * @module status 5 | */ 6 | module.exports.isOnline = isOnline 7 | module.exports.onOffline = onOffline 8 | module.exports.onOnline = onOnline 9 | 10 | /** 11 | * Returns a user's online status (true if user is on line) 12 | * @method isOnline 13 | * @static 14 | * @return {Boolean} 15 | */ 16 | function isOnline () { 17 | return window.navigator.onLine 18 | } 19 | 20 | /** 21 | * Adds an even listener to trigger when the user goes offline. 22 | * @method onOffline 23 | * @static 24 | * @param callback {Function} Callback to invoke when user goes offline. 25 | */ 26 | function onOffline (callback) { 27 | window.addEventListener('offline', callback, false) 28 | } 29 | 30 | /** 31 | * Adds an even listener to trigger when the user comes online. 32 | * @method onOnline 33 | * @static 34 | * @param callback {Function} Callback to invoke when user comes online 35 | */ 36 | function onOnline (callback) { 37 | window.addEventListener('online', callback, false) 38 | } 39 | -------------------------------------------------------------------------------- /src/type-registry.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides Solid helper functions involved with loading the Type Index 4 | * Registry files, and with registering resources with them. 5 | * @module type-registry 6 | */ 7 | module.exports.addToTypeIndex = addToTypeIndex 8 | module.exports.blankPrivateTypeIndex = blankPrivateTypeIndex 9 | module.exports.blankPublicTypeIndex = blankPublicTypeIndex 10 | module.exports.initTypeRegistryPrivate = initTypeRegistryPrivate 11 | module.exports.initTypeRegistryPublic = initTypeRegistryPublic 12 | module.exports.loadTypeRegistry = loadTypeRegistry 13 | module.exports.registerType = registerType 14 | module.exports.typeRegistryForClass = typeRegistryForClass 15 | module.exports.unregisterType = unregisterType 16 | 17 | var IndexRegistration = require('./solid/index-registration') 18 | var util = require('./util/web-util.js') 19 | var graphUtil = require('./util/graph-util.js') 20 | var webUtil = require('./util/web-util.js') 21 | var vocab = require('solid-namespace') 22 | 23 | /** 24 | * Returns a blank private type index registry option. 25 | * For use with `initTypeRegistry()`. 26 | * @method blankPrivateTypeIndex 27 | * @private 28 | * @return {Object} Blank type index registry object 29 | */ 30 | function blankPrivateTypeIndex (rdf) { 31 | var thisDoc = rdf.namedNode('') 32 | var ns = vocab(rdf) 33 | var indexStatements = [ 34 | rdf.triple(thisDoc, ns.rdf('type'), ns.solid('TypeIndex')), 35 | rdf.triple(thisDoc, ns.rdf('type'), ns.solid('UnlistedDocument')) 36 | ] 37 | var privateIndex = { 38 | data: graphUtil.serializeStatements(indexStatements), 39 | graph: graphUtil.graphFromStatements(indexStatements, rdf), 40 | slug: 'privateTypeIndex.ttl', 41 | uri: null // actual url not yet known 42 | } 43 | return privateIndex 44 | } 45 | 46 | /** 47 | * Returns a blank public type index registry option. 48 | * For use with `initTypeRegistry()`. 49 | * @method blankPublicTypeIndex 50 | * @private 51 | * @return {Object} Blank type index registry object 52 | */ 53 | function blankPublicTypeIndex (rdf) { 54 | var thisDoc = rdf.namedNode('') 55 | var ns = vocab(rdf) 56 | var indexStatements = [ 57 | rdf.triple(thisDoc, ns.rdf('type'), ns.solid('TypeIndex')), 58 | rdf.triple(thisDoc, ns.rdf('type'), ns.solid('ListedDocument')) 59 | ] 60 | var publicIndex = { 61 | data: graphUtil.serializeStatements(indexStatements), 62 | graph: graphUtil.graphFromStatements(indexStatements, rdf), 63 | slug: 'publicTypeIndex.ttl', 64 | uri: null // actual url not yet known 65 | } 66 | return publicIndex 67 | } 68 | 69 | /** 70 | * Initializes the private Type Index Registry resource, updates 71 | * the profile with the initialized index, and returns the updated profile. 72 | * @method initTypeRegistryPrivate 73 | * @param profile {SolidProfile} User's WebID profile 74 | * @param [options] Options hashmap (see solid.web.solidRequest() function docs) 75 | * @return {Promise} Resolves with the updated profile instance. 76 | */ 77 | function initTypeRegistryPrivate (profile, webClient, options) { 78 | options = options || {} 79 | var rdf = webClient.rdf 80 | var ns = vocab(rdf) 81 | var registryContainerUri = profile.typeRegistryDefaultContainer() 82 | var webId = rdf.namedNode(profile.webId) 83 | var privateIndex = blankPrivateTypeIndex(rdf) 84 | // First, create the private Type Index Registry resource 85 | return webClient.post(registryContainerUri, privateIndex.data, 86 | privateIndex.slug) 87 | .catch(function (err) { 88 | throw new Error('Could not create privateIndex document:', err) 89 | }) 90 | .then(function (response) { 91 | // Private type index resource created. 92 | // Update the private profile (preferences) to link to it. 93 | privateIndex.uri = util.absoluteUrl( 94 | webUtil.hostname(registryContainerUri), 95 | response.url 96 | ) 97 | var toAdd = [ 98 | rdf.triple(webId, ns.solid('privateTypeIndex'), 99 | rdf.namedNode(privateIndex.uri)) 100 | ] 101 | var toDel = [] 102 | // Note: this PATCH will actually create a private profile if it doesn't 103 | // already exist. 104 | return webClient.patch(profile.privateProfileUri(), toDel, toAdd, options) 105 | }) 106 | .catch(function (err) { 107 | throw new Error('Could not update profile with private index:' + err) 108 | }) 109 | .then(function (response) { 110 | // Profile successfully patched with a link to the created private index 111 | // It's safe to update this instance of profile 112 | profile.typeIndexUnlisted = privateIndex 113 | // Finally, return the updated profile with type index loaded 114 | return profile 115 | }) 116 | } 117 | 118 | /** 119 | * Initializes the public Type Index Registry resource, updates 120 | * the profile with the initialized index, and returns the updated profile. 121 | * @method initTypeRegistryPublic 122 | * @param profile {SolidProfile} User's WebID profile 123 | * @param webClient {SolidWebClient} 124 | * @param [options] Options hashmap (see solid.web.solidRequest() function docs) 125 | * @return {Promise} Resolves with the updated profile instance. 126 | */ 127 | function initTypeRegistryPublic (profile, webClient, options) { 128 | options = options || {} 129 | var rdf = webClient.rdf 130 | var ns = vocab(rdf) 131 | var registryContainerUri = profile.typeRegistryDefaultContainer() 132 | var webId = rdf.namedNode(profile.webId) 133 | var publicIndex = blankPublicTypeIndex(rdf) 134 | // First, create the public Type Index Registry resource 135 | return webClient.post(registryContainerUri, publicIndex.data, 136 | publicIndex.slug) 137 | .catch(function (err) { 138 | throw new Error('Could not create publicIndex document:', err) 139 | }) 140 | .then(function (response) { 141 | // Public type index resource created. Update the profile to link to it. 142 | publicIndex.uri = util.absoluteUrl( 143 | webUtil.hostname(registryContainerUri), 144 | response.url 145 | ) 146 | var toAdd = [ 147 | rdf.triple(webId, ns.solid('publicTypeIndex'), 148 | rdf.namedNode(publicIndex.uri)) 149 | ] 150 | var toDel = [] 151 | return webClient.patch(profile.webId, toDel, toAdd, options) 152 | }) 153 | .catch(function (err) { 154 | console.log(err) 155 | throw new Error('Could not update profile with public index:', err) 156 | }) 157 | .then(function (response) { 158 | // Profile successfully patched with a link to the created public index 159 | // It's safe to update this instance of profile 160 | profile.typeIndexListed = publicIndex 161 | // Finally, return the updated profile with type index loaded 162 | return profile 163 | }) 164 | } 165 | 166 | /** 167 | * Adds an RDF class to a user's type index registry, and returns the 168 | * profile (with the appropriate type registry index updated). 169 | * Called by `registerTypeIndex()`, which does all the argument validation. 170 | * @method addToTypeIndex 171 | * @param profile {SolidProfile} User's WebID profile 172 | * @param rdfClass {NamedNode} RDF type to register in the index. 173 | * @param location {String} Absolute URI to the location you want the class 174 | * registered to. 175 | * @param locationType {String} Either 'instance' or 'container' 176 | * @param isListed {Boolean} Whether to register in a listed or unlisted index). 177 | * @return {Promise} 178 | */ 179 | function addToTypeIndex (profile, rdfClass, location, webClient, 180 | locationType, isListed) { 181 | // TODO: Check to see if a registry entry for this type already exists. 182 | // Generate a fragment identifier for the new registration 183 | var hash = require('shorthash') 184 | var rdf = webClient.rdf 185 | var ns = vocab(rdf) 186 | var fragmentId = hash.unique(rdfClass.uri) 187 | var registryUri 188 | var registryGraph 189 | if (isListed) { 190 | registryUri = profile.typeIndexListed.uri 191 | registryGraph = profile.typeIndexListed.graph 192 | } else { 193 | registryUri = profile.typeIndexUnlisted.uri 194 | registryGraph = profile.typeIndexUnlisted.graph 195 | } 196 | if (!registryUri) { 197 | throw new Error('Cannot register type, registry URL missing') 198 | } 199 | var registrationUri = rdf.namedNode(registryUri + '#' + fragmentId) 200 | // Set the class for the location type 201 | var locationTypeClass 202 | if (locationType === 'instance') { 203 | locationTypeClass = ns.solid('instance') 204 | } else { 205 | locationTypeClass = ns.solid('instanceContainer') 206 | // Add trailing slash if it's missing and is a container 207 | if (location.lastIndexOf('/') !== location.length - 1) { 208 | location += '/' 209 | } 210 | } 211 | // triples to delete (none for the moment) 212 | var toDel = [] 213 | // Create the list of triples to add in the PATCH operation 214 | var toAdd = [ 215 | // example: '<#ab09fd> a solid:TypeRegistration;' 216 | rdf.triple(registrationUri, ns.rdf('type'), ns.solid('TypeRegistration')), 217 | // example: 'solid:forClass sioc:Post;' 218 | rdf.triple(registrationUri, ns.solid('forClass'), rdfClass), 219 | // example: 'solid:instanceContainer .' 220 | rdf.triple(registrationUri, locationTypeClass, rdf.namedNode(location)) 221 | ] 222 | return webClient.patch(registryUri, toDel, toAdd) 223 | .then(function (response) { 224 | // Update the profile object with the new registry without reloading 225 | var newRegistration = graphUtil.graphFromStatements(toAdd, rdf) 226 | if (registryGraph) { 227 | graphUtil.appendGraph(registryGraph, newRegistration) 228 | } else { 229 | profile[isListed ? 'typeIndexListed' : 'typeIndexUnlisted'].graph = newRegistration 230 | } 231 | return profile 232 | }) 233 | } 234 | 235 | /** 236 | * Loads the public and private type registry index resources, adds them 237 | * to the profile, and returns the profile. 238 | * Called by the profile.loadTypeRegistry() alias method. 239 | * Usage: 240 | * 241 | * ``` 242 | * var profile = solid.getProfile(url, options) 243 | * .then(function (profile) { 244 | * return profile.loadTypeRegistry(options) 245 | * }) 246 | * ``` 247 | * @method loadTypeRegistry 248 | * @param profile {SolidProfile} 249 | * @param webClient {SolidWebClient} 250 | * @param [options] Options hashmap (see solid.web.solidRequest() function docs) 251 | * @return {Promise} 252 | */ 253 | function loadTypeRegistry (profile, webClient, options) { 254 | options = options || {} 255 | options.headers = options.headers || {} 256 | // Politely ask for Turtle format 257 | if (!options.headers['Accept']) { 258 | options.headers['Accept'] = 'text/turtle' 259 | } 260 | // load public and private index resources 261 | var links = [] 262 | if (profile.typeIndexListed.uri) { 263 | links.push(profile.typeIndexListed.uri) 264 | } 265 | if (profile.typeIndexUnlisted.uri) { 266 | links.push(profile.typeIndexUnlisted.uri) 267 | } 268 | return webClient.loadParsedGraphs(links, options) 269 | .then(function (loadedGraphs) { 270 | const allFailed = loadedGraphs.length && 271 | loadedGraphs.reduce((acc, cur) => acc && !cur.value, true) 272 | if (allFailed) { 273 | throw new Error('Could not load any type index') 274 | } 275 | loadedGraphs.forEach(function (graph) { 276 | // For each index resource loaded, add it to `profile.typeIndexListed` 277 | // or `profile.typeIndexUnlisted` as appropriate 278 | if (graph && graph.value) { 279 | profile.addTypeRegistry(graph.value, graph.uri) 280 | } 281 | }) 282 | return profile 283 | }) 284 | } 285 | 286 | /** 287 | * Registers a given RDF class in the user's type index registries, so that 288 | * other applications can discover it. 289 | * Note: If the relevant type index registry does not exist, it will be created. 290 | * @method registerType 291 | * @param profile {SolidProfile} Loaded WebID profile 292 | * @param rdfClass {rdf.NamedNode} Type to register in the index. 293 | * @param location {String} Absolute URI to the location you want the class 294 | * registered to. (Example: Registering Address books in 295 | * `https://example.com/contacts/`) 296 | * @param [locationType='container'] {String} Either 'instance' or 'container', 297 | * defaults to 'container' 298 | * @param [isListed=false] {Boolean} Whether to register in a listed or unlisted 299 | * index). Defaults to `false` (unlisted). 300 | * @param webClient {SolidWebClient} 301 | * @return {Promise} Resolves with the updated profile. 302 | */ 303 | function registerType (profile, rdfClass, location, locationType, isListed, 304 | webClient) { 305 | if (!profile) { 306 | throw new Error('No profile provided') 307 | } 308 | if (!profile.isLoaded) { 309 | throw new Error('Profile is not loaded') 310 | } 311 | if (!rdfClass || !location) { 312 | throw new Error('Type registration requires type class and location') 313 | } 314 | locationType = locationType || 'container' 315 | if (locationType !== 'container' && locationType !== 'instance') { 316 | throw new Error('Invalid location type') 317 | } 318 | // make sure type registry is loaded 319 | return loadTypeRegistry(profile, webClient) 320 | .then(function (profile) { 321 | if (isListed && !profile.hasTypeRegistryPublic()) { 322 | // Public type registry is needed, but doesn't exist. Create it. 323 | return initTypeRegistryPublic(profile, webClient) 324 | } 325 | if (!isListed && !profile.hasTypeRegistryPrivate()) { 326 | // Private type registry is needed, but doesn't exist. Create it. 327 | return initTypeRegistryPrivate(profile, webClient) 328 | } 329 | // Relevant type registry exists, proceed 330 | return profile 331 | }) 332 | .then(function (profile) { 333 | // Made sure the relevant type registry exists, and can now add to it 334 | return addToTypeIndex(profile, rdfClass, location, webClient, 335 | locationType, isListed) 336 | }) 337 | } 338 | 339 | /** 340 | * Returns lists of registry entries for a profile and a given RDF Class. 341 | * @method typeRegistryForClass 342 | * @param profile {SolidProfile} 343 | * @param rdfClass {rdf.NamedNode} RDF Class 344 | * @return {Array} 345 | */ 346 | function typeRegistryForClass (profile, rdfClass, rdf) { 347 | var registrations = [] 348 | var isListed = true 349 | 350 | return registrations 351 | .concat( 352 | // Public/listed registrations 353 | registrationsFromGraph(profile.typeIndexListed.graph, rdfClass, isListed, rdf) 354 | ) 355 | .concat( 356 | // Private/unlisted registrations 357 | registrationsFromGraph(profile.typeIndexUnlisted.graph, rdfClass, 358 | !isListed, rdf) 359 | ) 360 | } 361 | 362 | /** 363 | * Returns a list of registry entries from a given parsed type index graph. 364 | * @method registrationsFromGraph 365 | * @param graph {Graph} Parsed type index graph 366 | * @param rdfClass {NamedNode} RDF Class 367 | * @param isListed {Boolean} Whether to register in a listed or unlisted index 368 | * @return {Array} 369 | */ 370 | function registrationsFromGraph (graph, rdfClass, isListed, rdf) { 371 | var entrySubject, instanceMatches, containerMatches 372 | var ns = vocab(rdf) 373 | var registrations = [] 374 | if (!graph) { 375 | return registrations 376 | } 377 | var matches = graph.statementsMatching(null, null, rdfClass) 378 | matches.forEach(function (match) { 379 | entrySubject = match.subject 380 | // Have the hash fragment of the registration, now need to determine 381 | // location type, and the actual location. 382 | instanceMatches = 383 | graph.statementsMatching(entrySubject, ns.solid('instance')) 384 | instanceMatches.forEach(function (location) { 385 | registrations.push(new IndexRegistration(entrySubject.uri, rdfClass, 386 | 'instance', location.object.uri, isListed)) 387 | }) 388 | // Now try to find solid:instanceContainer matches 389 | containerMatches = 390 | graph.statementsMatching(entrySubject, ns.solid('instanceContainer')) 391 | containerMatches.forEach(function (location) { 392 | registrations.push(new IndexRegistration(entrySubject.uri, rdfClass, 393 | 'container', location.object.uri, isListed)) 394 | }) 395 | }) 396 | return registrations 397 | } 398 | 399 | /** 400 | * Removes an RDF class from a user's type index registry. 401 | * Called by `unregisterTypeIndex()`, which does all the argument validation. 402 | * @param profile {SolidProfile} User's WebID profile 403 | * @param rdfClass {NamedNode} Type to remove from the registry 404 | * @param webClient {SolidWebClient} 405 | * @param [isListed=false] {Boolean} Whether to remove from a listed or 406 | * unlisted index 407 | * @param [location] {String} If present, only unregister the class from this 408 | * location (absolute URI). 409 | * @return {Promise} 410 | */ 411 | function removeFromTypeIndex (profile, rdfClass, webClient, isListed, 412 | location) { 413 | var rdf = webClient.rdf 414 | var registryUri 415 | var registryGraph 416 | if (isListed) { 417 | registryUri = profile.typeIndexListed.uri 418 | registryGraph = profile.typeIndexListed.graph 419 | } else { 420 | registryUri = profile.typeIndexUnlisted.uri 421 | registryGraph = profile.typeIndexUnlisted.graph 422 | } 423 | if (!registryUri) { 424 | throw new Error('Cannot unregister type, registry URL missing') 425 | } 426 | // Get the existing registrations 427 | var registrations = registrationsFromGraph(registryGraph, rdfClass, 428 | isListed, rdf) 429 | if (registrations.length === 0) { 430 | // No existing registrations, no need to do anything, just return profile 431 | return Promise.resolve(profile) 432 | } 433 | if (location) { 434 | // If location is present, filter the to-remove list only to registrations 435 | // that are in that location. 436 | registrations = registrations.filter(function (registration) { 437 | return registration.locationUri === location 438 | }) 439 | } 440 | // Generate triples to delete 441 | var toDel = [] 442 | registrations.forEach(function (registration) { 443 | registryGraph.statementsMatching(rdf.namedNode(registration.registrationUri)) 444 | .forEach(function (statement) { 445 | toDel.push(statement) 446 | }) 447 | }) 448 | // Nothing to add 449 | var toAdd = [] 450 | return webClient.patch(registryUri, toDel, toAdd) 451 | .then(function (result) { 452 | // Update the registry, to reflect new state 453 | return profile.reloadTypeRegistry(webClient) 454 | }) 455 | } 456 | 457 | /** 458 | * Removes a given RDF class from a user's type index registry, so that 459 | * other applications can discover it. 460 | * @method unregisterType 461 | * @param profile {SolidProfile} Loaded WebID profile 462 | * @param rdfClass {NamedNode} Type to register in the index. 463 | * @param [isListed=false] {Boolean} Whether to remove from a listed or unlisted 464 | * index). Defaults to `false` (unlisted). 465 | * @param [location] {String} If present, only unregister the class from this 466 | * location (absolute URI). 467 | * @param webClient {SolidWebClient} 468 | * @throws {Error} 469 | * @return {Promise} 470 | */ 471 | function unregisterType (profile, rdfClass, isListed, location, webClient) { 472 | if (!profile) { 473 | throw new Error('No profile provided') 474 | } 475 | if (!profile.isLoaded) { 476 | throw new Error('Profile is not loaded') 477 | } 478 | if (!rdfClass) { 479 | throw new Error('Unregistering a type requires type class') 480 | } 481 | // make sure type registry is loaded 482 | return loadTypeRegistry(profile, webClient) 483 | .then(function (profile) { 484 | if (isListed && !profile.typeIndexListed.graph) { 485 | throw new Error('Profile has no Listed type index') 486 | } 487 | if (!isListed && !profile.typeIndexUnlisted.graph) { 488 | throw new Error('Profile has no Unlisted type index') 489 | } 490 | return removeFromTypeIndex(profile, rdfClass, webClient, isListed, location) 491 | }) 492 | } 493 | -------------------------------------------------------------------------------- /src/util/graph-util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides convenience methods for graph manipulation. 4 | * Currently depends on RDFLib 5 | * @module graph-util 6 | */ 7 | module.exports.appendGraph = appendGraph 8 | module.exports.parseGraph = parseGraph 9 | module.exports.parseLinks = parseLinks 10 | module.exports.serializeStatements = serializeStatements 11 | module.exports.graphFromStatements = graphFromStatements 12 | module.exports.statementToNT = statementToNT 13 | 14 | var ALL_STATEMENTS = null 15 | 16 | /** 17 | * Appends RDF statements from one graph object to another 18 | * @method appendGraph 19 | * @param toGraph {Graph} Graph object to append to 20 | * @param fromGraph {Graph} Graph object to append from 21 | */ 22 | function appendGraph (toGraph, fromGraph) { 23 | // var source = (docURI) ? rdf.sym(docURI) : undefined 24 | fromGraph.statementsMatching(ALL_STATEMENTS) 25 | .forEach(function (st) { 26 | toGraph.add(st.subject, st.predicate, st.object, st.why) 27 | }) 28 | } 29 | 30 | /** 31 | * Converts a statement to string (if it isn't already), optionally slices off 32 | * the period at the end, and returns the statement. 33 | * @method statementToNT 34 | * @param statement {String|Statement} RDF Statement to be converted. 35 | * @param [excludeDot=false] {Boolean} Optionally slice off ending period. 36 | * @return {String} 37 | */ 38 | function statementToNT (statement, excludeDot) { 39 | if (typeof statement !== 'string') { 40 | // This is an RDF Statement. Convert to string 41 | statement = statement.toNT() 42 | } 43 | if (excludeDot && statement.endsWith('.')) { 44 | statement = statement.slice(0, -1) 45 | } 46 | return statement 47 | } 48 | 49 | /** 50 | * Converts a list of RDF statements into an rdflib Graph (Formula), and returns 51 | * it. 52 | * @method graphFromStatements 53 | * @param statements {Array} 54 | * @return {Graph} 55 | */ 56 | function graphFromStatements (statements, rdf) { 57 | var graph = rdf.graph() 58 | statements.forEach(function (st) { 59 | graph.add(st) 60 | }) 61 | return graph 62 | } 63 | 64 | /** 65 | * Parses a given graph, from text rdfSource, as a given content type. 66 | * Returns parsed graph. 67 | * @method parseGraph 68 | * @param baseUrl {String} 69 | * @param rdfSource {String} Text source code 70 | * @param contentType {String} Mime Type (determines which parser to use) 71 | * @return {Graph} 72 | */ 73 | function parseGraph (baseUrl, rdfSource, contentType, rdf) { 74 | var parsedGraph = rdf.graph() 75 | rdf.parse(rdfSource, parsedGraph, baseUrl, contentType) 76 | return parsedGraph 77 | } 78 | 79 | /** 80 | * Extracts the URIs from a parsed graph that match parameters. 81 | * The URIs are a set (duplicates are removed) 82 | * @method parseLinks+ 83 | * @param graph {Graph} 84 | * @param subject {Symbol} 85 | * @param predicate {Symbol} 86 | * @param object {Symbol} 87 | * @param source {Symbol} 88 | * @return {Array} Array of link URIs that match the parameters 89 | */ 90 | function parseLinks (graph, subject, predicate, object, source) { 91 | var links = {} 92 | var matches = graph.statementsMatching(subject, 93 | predicate, object, source) 94 | matches.forEach(function (match) { 95 | links[match.object.uri] = true 96 | }) 97 | return Object.keys(links) 98 | } 99 | 100 | /** 101 | * Serializes an array of RDF statements into a simple N-Triples format 102 | * suitable for writing to a solid server. 103 | * @method serializeStatements 104 | * @param statements {Array} List of RDF statements 105 | * @return {String} 106 | */ 107 | function serializeStatements (statements) { 108 | var source = statements.map(function (st) { return st.toNT() }) 109 | source = source.join('\n') 110 | return source 111 | } 112 | -------------------------------------------------------------------------------- /src/util/rdf-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides a generic wrapper around an RDF Parser library 4 | * (currently only RDFLib) 5 | * @@ RDFLib is NOT JUST a parser library. It is a quadstore and a serializer library! 6 | * @module rdf-parser 7 | */ 8 | var rdf 9 | if (typeof $rdf !== 'undefined') { 10 | rdf = $rdf // FF extension 11 | } else if (typeof tabulator !== 'undefined') { 12 | rdf = tabulator.rdf 13 | } else if (typeof require === 'function') { 14 | // Running with a CommonJS module system 15 | rdf = require('rdflib') 16 | } 17 | module.exports = rdf 18 | -------------------------------------------------------------------------------- /src/util/web-util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Provides misc utility functions for the web client 4 | * @module web-util 5 | */ 6 | module.exports.parseAllowedMethods = parseAllowedMethods 7 | module.exports.parseLinkHeader = parseLinkHeader 8 | module.exports.absoluteUrl = absoluteUrl 9 | module.exports.hostname = hostname 10 | 11 | /** 12 | * Extracts the allowed HTTP methods from the 'Allow' and 'Accept-Patch' 13 | * headers, and returns a hashmap of verbs allowed by the server 14 | * @method parseAllowedMethods 15 | * @param allowMethodsHeader {String} `Access-Control-Allow-Methods` response 16 | * header 17 | * @param acceptPatchHeader {String} `Accept-Patch` response header 18 | * @return {Object} Hashmap of verbs (in lowercase) allowed by the server for 19 | * the current user. Example: 20 | * ``` 21 | * { 22 | * 'get': true, 23 | * 'put': true 24 | * } 25 | * ``` 26 | */ 27 | function parseAllowedMethods (allowMethodsHeader, acceptPatchHeader) { 28 | var allowedMethods = {} 29 | if (allowMethodsHeader) { 30 | var verbs = allowMethodsHeader.split(',') 31 | verbs.forEach(function (methodName) { 32 | if (methodName && allowMethodsHeader.indexOf(methodName) >= 0) { 33 | allowedMethods[methodName.trim().toLowerCase()] = true 34 | } 35 | }) 36 | } 37 | if (acceptPatchHeader && 38 | acceptPatchHeader.indexOf('application/sparql-update') >= 0) { 39 | allowedMethods.patch = true 40 | } 41 | return allowedMethods 42 | } 43 | 44 | /** 45 | * Parses a Link header from an XHR HTTP Request. 46 | * @method parseLinkHeader 47 | * @param link {String} Contents of the Link response header 48 | * @return {Object} 49 | */ 50 | function parseLinkHeader (link) { 51 | if (!link) { 52 | return {} 53 | } 54 | var linkexp = /<[^>]*>\s*(\s*;\s*[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*")))*(,|$)/g 55 | var paramexp = /[^\(\)<>@,;:"\/\[\]\?={} \t]+=(([^\(\)<>@,;:"\/\[\]\?={} \t]+)|("[^"]*"))/g 56 | var matches = link.match(linkexp) 57 | var rels = {} 58 | for (var i = 0; i < matches.length; i++) { 59 | var split = matches[i].split('>') 60 | var href = split[0].substring(1) 61 | var ps = split[1] 62 | var s = ps.match(paramexp) 63 | 64 | for (var j = 0; j < s.length; j++) { 65 | var p = s[j] 66 | var paramsplit = p.split('=') 67 | // var name = paramsplit[0] 68 | var rel = paramsplit[1].replace(/["']/g, '') 69 | if (!rels[rel]) { 70 | rels[rel] = [] 71 | } 72 | rels[rel].push(href) 73 | if (rels[rel].length > 1) { 74 | rels[rel].sort() 75 | } 76 | } 77 | } 78 | return rels 79 | } 80 | 81 | function hostname (url) { 82 | var protocol, hostname, result, pathSegments 83 | var fragments = url.split('//') 84 | if (fragments.length === 2) { 85 | protocol = fragments[0] 86 | hostname = fragments[1] 87 | } else { 88 | hostname = url 89 | } 90 | pathSegments = hostname.split('/') 91 | if (protocol) { 92 | result = protocol + '//' + pathSegments[0] 93 | } else { 94 | result = pathSegments[0] 95 | } 96 | if (url.startsWith('//')) { 97 | result = '//' + result 98 | } 99 | return result 100 | } 101 | 102 | /** 103 | * Return an absolute URL 104 | * @method absoluteUrl 105 | * @param baseUrl {String} URL to be used as base 106 | * @param pathUrl {String} Absolute or relative URL 107 | * @return {String} 108 | */ 109 | function absoluteUrl (baseUrl, pathUrl) { 110 | if (pathUrl && pathUrl.slice(0, 4) !== 'http') { 111 | return [baseUrl, pathUrl].map(function (path) { 112 | if (path[0] === '/') { 113 | path = path.slice(1) 114 | } 115 | if (path[path.length - 1] === '/') { 116 | path = path.slice(0, path.length - 1) 117 | } 118 | return path 119 | }).join('/') 120 | } 121 | return pathUrl 122 | } 123 | -------------------------------------------------------------------------------- /test/integration/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | solid-client QUnit Tests 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/integration/solid-profile-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var solid = SolidClient 4 | var vocab = solid.vocab 5 | var webClient = solid.web 6 | 7 | var serverUrl = 'https://localhost:8443/' 8 | var profileUrl = serverUrl + 'profile/test-profile-card' 9 | var rawProfileSource = require('test-minimal-profile') 10 | var prefsUrl = serverUrl + 'settings/test-test.ttl' 11 | var rawPrefsSource = require('test-minimal-prefs') 12 | var defaultPrivateTypeRegistryUrl = serverUrl + 'profile/privateTypeIndex.ttl' 13 | var defaultPublicTypeRegistryUrl = serverUrl + 'profile/publicTypeIndex.ttl' 14 | var defaultPrivateAppRegistryUrl = serverUrl + 'profile/privateAppRegistry.ttl' 15 | var defaultPublicAppRegistryUrl = serverUrl + 'profile/publicAppRegistry.ttl' 16 | 17 | var AppRegistration = solid.AppRegistration 18 | 19 | function clearRegistries () { 20 | return clearResource(defaultPublicTypeRegistryUrl) 21 | .then(function () { 22 | return clearResource(defaultPrivateTypeRegistryUrl) 23 | }) 24 | .then(function () { 25 | return clearResource(defaultPrivateAppRegistryUrl) 26 | }) 27 | .then(function () { 28 | return clearResource(defaultPrivateAppRegistryUrl) 29 | }) 30 | .then(function () { 31 | return clearResource(defaultPublicAppRegistryUrl) 32 | }) 33 | } 34 | 35 | function clearResource (url) { 36 | return solid.web.del(url) 37 | .catch(function () { 38 | // do nothing (likely tried to delete a non-existent resource) 39 | }) 40 | } 41 | 42 | function ensureProfile (profileUrl) { 43 | var createProfile = false 44 | return solid.web.head(profileUrl) 45 | .catch(function (error) { 46 | if (error.code === 404) { 47 | console.log('Profile not found.') 48 | createProfile = true 49 | } else { 50 | console.log('Error on HEAD profile url:', error) 51 | } 52 | return createProfile 53 | }) 54 | .then(function () { 55 | if (createProfile) { 56 | console.log('Creating test profile...') 57 | return solid.web.put(profileUrl, rawProfileSource, 'text/turtle') 58 | } else { 59 | console.log('Profile detected, no problem.') 60 | } 61 | }) 62 | .catch(function (error) { 63 | console.log('Error creating test profile:', error) 64 | }) 65 | .then(function () { 66 | if (createProfile) { 67 | console.log('Profile created.') 68 | } 69 | }) 70 | } 71 | 72 | function resetProfile (url) { 73 | console.log('Resetting profile...') 74 | // First, delete the current profile 75 | return clearResource(url) 76 | .then(function () { 77 | // Re-add the profile from template 78 | return ensureProfile(url) 79 | }) 80 | .then(function () { 81 | // Delete the private profile (prefs.ttl) 82 | return clearResource(prefsUrl) 83 | }) 84 | .then(function () { 85 | // Re-add the private profile 86 | return solid.web.put(prefsUrl, rawPrefsSource, 'text/turtle') 87 | }) 88 | .then(function () { 89 | return solid.getProfile(profileUrl) 90 | }) 91 | } 92 | 93 | QUnit.module('SolidProfile tests', { 94 | /** 95 | * Runs after the last test in this module 96 | */ 97 | after: function (details) { 98 | return resetProfile(profileUrl) 99 | .then(function () { 100 | console.log('Profile reset.') 101 | return clearRegistries() 102 | }) 103 | } 104 | }) 105 | 106 | QUnit.test('getProfile() test', function (assert) { 107 | assert.expect(3) 108 | return ensureProfile(profileUrl) 109 | .then(function () { 110 | return solid.getProfile(profileUrl) 111 | }) 112 | .then(function (profile) { 113 | assert.ok(profile.isLoaded) 114 | assert.deepEqual(profile.response.types, 115 | ['http://www.w3.org/ns/ldp#Resource']) 116 | assert.deepEqual(profile.storage, [serverUrl]) 117 | }) 118 | }) 119 | 120 | QUnit.test('initTypeRegistryPublic() test', function (assert) { 121 | assert.expect(6) 122 | var profile 123 | return clearResource(defaultPublicTypeRegistryUrl) 124 | .then(function () { 125 | // Make sure registry does not exist 126 | // return resetProfile(profileUrl) 127 | return solid.getProfile(profileUrl) 128 | }) 129 | .then(function (profileResult) { 130 | profile = profileResult 131 | return solid.typeRegistry.initTypeRegistryPublic(profile, webClient) 132 | }) 133 | .then(function () { 134 | // Check to make sure the type index is loaded after init 135 | assert.ok(profile.hasTypeRegistryPublic()) 136 | assert.equal(profile.typeIndexListed.uri, defaultPublicTypeRegistryUrl, 137 | 'public type index uri should be loaded after registry init') 138 | assert.ok(profile.typeIndexListed.graph, 139 | 'public type index graph should be loaded after init') 140 | // reload the profile 141 | return solid.getProfile(profileUrl) 142 | }) 143 | .then(function (profileResult) { 144 | profile = profileResult 145 | assert.ok(profile.hasTypeRegistryPublic(), 146 | 'hasTypeRegistryPublic() should be true after initTypeRegistryPublic() + profile reload') 147 | assert.equal(profile.typeIndexListed.uri, defaultPublicTypeRegistryUrl, 148 | 'public type index uri should be loaded after registry init + profile reload') 149 | // The profile has been reloaded, but the type registry wasn't loaded. 150 | // Load it now. 151 | return profile.loadTypeRegistry(webClient) 152 | }) 153 | .then(function (profileResult) { 154 | profile = profileResult 155 | assert.ok(profile.typeIndexListed.graph, 156 | 'public type index graph should be loaded after profile reload + loadTypeRegistry()') 157 | // Test to make sure that the freshly initialized registry is empty 158 | }) 159 | .then(function () { 160 | return clearResource(defaultPublicTypeRegistryUrl) 161 | }) 162 | }) 163 | 164 | QUnit.test('initTypeRegistryPrivate() test', function (assert) { 165 | assert.expect(6) 166 | var profile 167 | return clearResource(defaultPrivateTypeRegistryUrl) 168 | .then(function () { 169 | // Make sure registry does not exist 170 | // return resetProfile(profileUrl) 171 | return solid.getProfile(profileUrl) 172 | }) 173 | .then(function (profileResult) { 174 | profile = profileResult 175 | return solid.typeRegistry.initTypeRegistryPrivate(profile, webClient) 176 | }) 177 | .then(function () { 178 | // Check to make sure the type index is loaded after init 179 | assert.ok(profile.hasTypeRegistryPrivate()) 180 | assert.equal(profile.typeIndexUnlisted.uri, defaultPrivateTypeRegistryUrl, 181 | 'private type index uri should be loaded after registry init') 182 | assert.ok(profile.typeIndexUnlisted.graph, 183 | 'private type index graph should be loaded after init') 184 | // reload the profile 185 | return solid.getProfile(profileUrl) 186 | }) 187 | .then(function (profileResult) { 188 | profile = profileResult 189 | assert.ok(profile.hasTypeRegistryPrivate(), 190 | 'hasTypeRegistryPrivate() should be true after initTypeRegistryPrivate() + profile reload') 191 | assert.equal(profile.typeIndexUnlisted.uri, defaultPrivateTypeRegistryUrl, 192 | 'private type index uri should be loaded after registry init + profile reload') 193 | // The profile has been reloaded, but the type registry wasn't loaded. 194 | // Load it now. 195 | return profile.loadTypeRegistry(webClient) 196 | }) 197 | .then(function (profileResult) { 198 | profile = profileResult 199 | assert.ok(profile.typeIndexUnlisted.graph, 200 | 'private type index graph should be loaded after profile reload + loadTypeRegistry()') 201 | // Test to make sure that the freshly initialized registry is empty 202 | }) 203 | .then(function () { 204 | return clearResource(defaultPrivateTypeRegistryUrl) 205 | }) 206 | }) 207 | 208 | QUnit.test('registerType()/unregisterType() test', function (assert) { 209 | assert.expect(5) 210 | var classToRegister = vocab.sioc('Post') 211 | var locationToRegister = 'https://localhost:8443/posts-container/' 212 | var isListed = true 213 | var profile 214 | console.log('** registerType() -- reset profile and registries') 215 | return clearRegistries() 216 | .then(function () { 217 | return resetProfile(profileUrl) 218 | }) 219 | .then(function (profileResult) { 220 | profile = profileResult 221 | return profile.registerType(classToRegister, locationToRegister, 222 | 'container', isListed) 223 | }) 224 | .then(function (profileResult) { 225 | profile = profileResult 226 | // Now the type is registered, and the profile's type registry is refreshed 227 | // querying the registry now will include the new container 228 | var registrations = profile.typeRegistryForClass(vocab.sioc('Post')) 229 | assert.equal(registrations.length, 1) 230 | var newRegistration = registrations[0] 231 | assert.equal(newRegistration.locationType, 'container') 232 | assert.equal(newRegistration.locationUri, locationToRegister) 233 | assert.equal(newRegistration.rdfClass.uri, vocab.sioc('Post').uri) 234 | return profile 235 | }) 236 | .catch(function (err) { 237 | console.log('Error while registerType()', err) 238 | }) 239 | .then(function () { 240 | console.log('** unregisterType() test - reloading profile') 241 | return solid.getProfile(profileUrl) 242 | }) 243 | .then(function (profileResult) { 244 | profile = profileResult 245 | var classToRemove = vocab.sioc('Post') 246 | var isListed = true 247 | console.log('loadedProfile: ', profileResult) 248 | console.log('Calling unregisterType()') 249 | // At this point, the profile has been loaded, but the type registries 250 | // have not been. They will be loaded during unregisterType() 251 | return profile.unregisterType(classToRemove, isListed) 252 | }) 253 | .catch(function (err) { 254 | console.log('Error while unregisterType:', err) 255 | }) 256 | .then(function (profileResult) { 257 | profile = profileResult 258 | // Check to make sure the registration was removed 259 | var registrations = profile.typeRegistryForClass(vocab.sioc('Post')) 260 | assert.equal(registrations.length, 0) 261 | }) 262 | .then(function () { 263 | console.log('clearing registries...') 264 | return clearRegistries() 265 | }) 266 | }) 267 | 268 | QUnit.test('initAppRegistryPublic() test', function (assert) { 269 | assert.expect(7) 270 | var profile 271 | console.log('** initAppRegistryPublic() test') 272 | return clearResource(defaultPublicAppRegistryUrl) 273 | .then(function () { 274 | // Make sure registry does not exist 275 | // return resetProfile(profileUrl) 276 | return solid.getProfile(profileUrl) 277 | }) 278 | .then(function (profileResult) { 279 | profile = profileResult 280 | return solid.appRegistry.initAppRegistryPublic(profile, webClient) 281 | }) 282 | .then(function () { 283 | // Check to make sure the app registry is loaded after init 284 | assert.ok(profile.hasAppRegistryPublic()) 285 | assert.equal(profile.appRegistryListed.uri, defaultPublicAppRegistryUrl, 286 | 'public app registry uri should be loaded after registry init') 287 | assert.ok(profile.appRegistryListed.graph, 288 | 'public app registry graph should be loaded after init') 289 | // reload the profile 290 | return solid.getProfile(profileUrl) 291 | }) 292 | .then(function (profileResult) { 293 | profile = profileResult 294 | assert.ok(profile.hasAppRegistryPublic(), 295 | 'hasTypeRegistryPublic() should be true after initAppRegistryPublic() + profile reload') 296 | assert.equal(profile.appRegistryListed.uri, defaultPublicAppRegistryUrl, 297 | 'public app registry uri should be loaded after registry init + profile reload') 298 | assert.notOk(profile.appRegistryListed.graph) 299 | // The profile has been reloaded, but the app registry wasn't loaded. 300 | // Load it now. 301 | return profile.loadAppRegistry(webClient) 302 | }) 303 | .then(function (profileResult) { 304 | profile = profileResult 305 | assert.ok(profile.appRegistryListed.graph, 306 | 'public app registry graph should be loaded after profile reload + loadAppRegistry()') 307 | // Test to make sure that the freshly initialized registry is empty 308 | }) 309 | .then(function () { 310 | // return clearResource(defaultPublicAppRegistryUrl) 311 | }) 312 | }) 313 | 314 | QUnit.test('initAppRegistryPrivate() test', function (assert) { 315 | assert.expect(6) 316 | var profile 317 | return clearResource(defaultPrivateAppRegistryUrl) 318 | .then(function () { 319 | // Make sure registry does not exist 320 | // return resetProfile(profileUrl) 321 | return solid.getProfile(profileUrl) 322 | }) 323 | .then(function (profileResult) { 324 | profile = profileResult 325 | return solid.appRegistry.initAppRegistryPrivate(profile, webClient) 326 | }) 327 | .then(function () { 328 | // Check to make sure the app registry is loaded after init 329 | assert.ok(profile.hasAppRegistryPrivate()) 330 | assert.equal(profile.appRegistryUnlisted.uri, defaultPrivateAppRegistryUrl, 331 | 'private app registry uri should be loaded after registry init') 332 | assert.ok(profile.appRegistryUnlisted.graph, 333 | 'private app registry graph should be loaded after init') 334 | // reload the profile 335 | return solid.getProfile(profileUrl) 336 | }) 337 | .then(function (profileResult) { 338 | profile = profileResult 339 | assert.ok(profile.hasAppRegistryPrivate(), 340 | 'hasAppRegistryPrivate() should be true after initAppRegistryPrivate() + profile reload') 341 | assert.equal(profile.appRegistryUnlisted.uri, defaultPrivateAppRegistryUrl, 342 | 'private app registry uri should be loaded after registry init + profile reload') 343 | // The profile has been reloaded, but the app registry wasn't loaded. 344 | // Load it now. 345 | return profile.loadAppRegistry(webClient) 346 | }) 347 | .then(function (profileResult) { 348 | profile = profileResult 349 | assert.ok(profile.appRegistryUnlisted.graph, 350 | 'private app registry graph should be loaded after profile reload + loadAppRegistry()') 351 | // Test to make sure that the freshly initialized registry is empty 352 | }) 353 | .then(function () { 354 | // return clearResource(defaultPrivateAppRegistryUrl) 355 | }) 356 | }) 357 | 358 | QUnit.test('profile.appsForType() test', function (assert) { 359 | var REDIRECT_URI = 'https://solid.github.io/contacts/?uri={uri}' 360 | assert.expect(9) 361 | var profile 362 | var typesForApp = [ 363 | vocab.vcard('AddressBook') 364 | ] 365 | return ensureProfile(profileUrl) 366 | .then(function () { 367 | return solid.getProfile(profileUrl) 368 | }) 369 | .then(function (loadedProfile) { 370 | profile = loadedProfile 371 | // The registries have been initialized by the preceding tests 372 | assert.ok(profile.hasAppRegistryPrivate()) 373 | assert.ok(profile.hasAppRegistryPublic()) 374 | // Check to make sure no registry entry exists 375 | var registeredApps = profile.appsForType(vocab.vcard('AddressBook')) 376 | assert.deepEqual(registeredApps, [], 377 | 'An empty app registry should have no registrations for AddressBook') 378 | var options = { 379 | name: 'Contact Manager', 380 | shortdesc: 'desc', 381 | redirectTemplateUri: REDIRECT_URI 382 | } 383 | var isListed = true 384 | var app = new AppRegistration(options, typesForApp, isListed) 385 | return profile.registerApp(app, webClient) 386 | }) 387 | .then(function (updatedProfile) { 388 | profile = updatedProfile 389 | return profile.appsForType(vocab.vcard('AddressBook')) 390 | }) 391 | .then(function (registrationResults) { 392 | assert.equal(registrationResults.length, 1, 393 | 'Only one app should have been registered') 394 | var app = registrationResults[0] 395 | assert.equal(app.name, 'Contact Manager') 396 | assert.equal(app.shortdesc, 'desc') 397 | assert.equal(app.redirectTemplateUri, REDIRECT_URI) 398 | assert.equal(app.types.length, 1) 399 | assert.ok(app.isListed) 400 | }) 401 | .then(function () { 402 | return clearRegistries() 403 | }) 404 | }) 405 | -------------------------------------------------------------------------------- /test/integration/web-client-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var solid = SolidClient 4 | 5 | var serverUrlWeb = 'https://localhost:8443' 6 | 7 | QUnit.module('web client tests') 8 | 9 | QUnit.test('web.head() test', function (assert) { 10 | assert.expect(2) 11 | return solid.web.head(serverUrl) 12 | .then(function (result) { 13 | assert.equal(result.xhr.status, 200) 14 | assert.deepEqual(result.types, 15 | ['http://www.w3.org/ns/ldp#BasicContainer', 16 | 'http://www.w3.org/ns/ldp#Container']) 17 | }) 18 | }) 19 | 20 | QUnit.test('web.createContainer() test', function (assert) { 21 | assert.expect(6) 22 | var containerName = 'qunit-test-container' 23 | var folderLocation, resourceLocation 24 | var serverUrl = serverUrlWeb 25 | console.log('serverUrl 1:', serverUrl) 26 | return solid.web.createContainer(serverUrl, containerName) 27 | .then(function (response) { 28 | console.log('Container created') 29 | console.log(response) 30 | assert.equal(response.xhr.status, 201, 31 | 'Result of createContainer() should be a 201') 32 | assert.deepEqual(response.types, 33 | ['http://www.w3.org/ns/ldp#BasicContainer', 34 | 'http://www.w3.org/ns/ldp#Container']) 35 | assert.ok(response.isContainer(), 36 | 'Result of createContainer() should be isContainer()') 37 | folderLocation = serverUrl + response.xhr.getResponseHeader('Location') 38 | console.log('Location:', folderLocation) 39 | return solid.web.get(folderLocation) 40 | }) 41 | .catch(function (err) { 42 | console.log('Error creating container', err) 43 | }) 44 | .then(function (response) { 45 | console.log('Listing response:', response) 46 | assert.ok(response.isContainer(), 47 | 'Result of listing should be isContainer()') 48 | assert.ok(response.isEmpty(), 'Newly created container should be empty') 49 | return solid.web.post(folderLocation, '') 50 | }) 51 | .then(function (response) { 52 | console.log('New resource POSTed:', response) 53 | resourceLocation = serverUrl + response.xhr.getResponseHeader('Location') 54 | return solid.web.get(folderLocation) 55 | }) 56 | .then(function (response) { 57 | console.log('Listing, after resource posted:', response) 58 | assert.equal(response.contentsUris.length, 1, 59 | 'After posting, a folder should contain 1 resource') 60 | console.log('Cleaning up test resource') 61 | return solid.web.del(resourceLocation) 62 | }) 63 | .catch(function (err) { 64 | console.log('Error deleting test resource', err) 65 | }) 66 | .then(function (response) { 67 | console.log('result of deleting resource:', response) 68 | return solid.web.del(folderLocation) 69 | }) 70 | .catch(function (err) { 71 | console.log('Error deleting test resource', err) 72 | }) 73 | .then(function (response) { 74 | console.log('result of deleting:', response) 75 | return solid.web.get(folderLocation) 76 | }) 77 | .catch(function (err) { 78 | console.log('GETing a deleted container:', err) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/resources/app-registry-listed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample public AppRegistry resource 3 | */ 4 | module.exports = `@prefix solid: . 5 | @prefix app: . 6 | @prefix vcard: . 7 | 8 | <> 9 | a solid:AppRegistry; 10 | a solid:ListedDocument. 11 | <#iTsLp> 12 | a solid:AppRegistration; 13 | app:commonType vcard:AddressBook; 14 | app:name "Contact Manager"; 15 | app:shortdesc "A reference contact manager"; 16 | app:redirectTemplateUri "https://solid.github.io/contacts/?uri={uri}". 17 | ` 18 | -------------------------------------------------------------------------------- /test/resources/app-registry-unlisted.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample private AppRegistry resource 3 | */ 4 | module.exports = `@prefix solid: . 5 | 6 | <> 7 | a solid:AppRegistry; 8 | a solid:UnlistedDocument. 9 | ` 10 | -------------------------------------------------------------------------------- /test/resources/profile-extended.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample LDNode user profile, for use with `solid-profile-test.js` 3 | */ 4 | module.exports = `@prefix rdfs: . 5 | @prefix foaf: . 6 | @prefix rdfs: . 7 | @prefix owl: . 8 | @prefix sp: . 9 | @prefix loc: . 10 | @prefix terms: . 11 | @prefix ldp: . 12 | @prefix inbox: . 13 | @prefix cert: . 14 | @prefix ter: . 15 | @prefix XML: . 16 | 17 | <> 18 | a foaf:PersonalProfileDocument; 19 | foaf:primaryTopic <#me>; 20 | rdfs:seeAlso . 21 | 22 | <#me> 23 | a foaf:Person; 24 | foaf:name "Alice"; 25 | foaf:img ; 26 | 27 | # owl:sameAs, rdfs:seeAlso, and sp:preferencesFile link 28 | # to the Extended Profile 29 | owl:sameAs ; 30 | cert:key 31 | <#key-1455289666916>; 32 | # This preferencesFile can be thought of as a private profile 33 | sp:preferencesFile 34 | ; 35 | # Add a duplicate Preferences link, to test client side de-duplication 36 | sp:preferencesFile 37 | ; 38 | 39 | # Link to root storage container 40 | sp:storage 41 | loc:; 42 | # Link to the public (listed) Type Registry index. 43 | # The link to the private (unlisted) index is in the private profile 44 | terms:publicTypeIndex 45 | ; 46 | # Link to the public App Registry. 47 | terms:publicAppRegistry 48 | ; 49 | # Link to the Solid messaging inbox 50 | ldp:inbox 51 | inbox:. 52 | 53 | # Public key certificate section 54 | <#key-1455289666916> 55 | ter:created 56 | "2016-02-12T15:07:46.916Z"^^XML:dateTime; 57 | ter:title 58 | "Created by ldnode"; 59 | a cert:RSAPublicKey; 60 | rdfs:label 61 | "LDNode Localhost Test Cert"; 62 | cert:exponent 63 | "65537"^^XML:int; 64 | cert:modulus 65 | "970E88053BC7D146A50AFAB79044B9D3BACE8B1283AB98BBDD9B598799AEB9711A7DA9A2CCA50A9F5D30C776EA06FA749F84A359B2CBC9DEF9F4DF7C27E7ED143A25E5F658CC12C87986482200969A3C04AB29BED20860791CEA1D515952821E1FFEE4CBF5F5F9949D6E2C88CDAFEB64C5D610A3B97E58AD19585B4DFDD2AA662FDB8F7889EBAA97D53FEE5740B71549E00A8DA0565DF2A901718D60AC6281642D592C865921F525640BC2FB8BC9EF79A3171F156120C41557374CE1FE735A8948C43B44399495EB4392E57C6FC17B4AD72ABA831C1BF40EA75C01F79CDFEE95CA38DAA7C4DDDFEC4ECED90091D3E68B7C0D364BEF5C454849FE6AA5E5167801"^^XML:hexBinary. 66 | ` 67 | -------------------------------------------------------------------------------- /test/resources/profile-minimal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample LDNode user profile, for use with `solid-profile-test.js` 3 | */ 4 | module.exports = `@prefix rdfs: . 5 | @prefix foaf: . 6 | @prefix sp: . 7 | @prefix loc: . 8 | @prefix ldp: . 9 | @prefix inbox: . 10 | 11 | <> 12 | a foaf:PersonalProfileDocument; 13 | foaf:primaryTopic <#me>. 14 | 15 | <#me> 16 | a foaf:Person; 17 | sp:preferencesFile 18 | ; 19 | 20 | sp:storage 21 | loc:; 22 | ldp:inbox 23 | inbox:. 24 | ` 25 | -------------------------------------------------------------------------------- /test/resources/profile-private.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample "private profile"/preferences file, for use with 3 | * `solid-profile-test.js` 4 | */ 5 | module.exports = ` 6 | # Located in /settings/test-prefs.ttl 7 | 8 | 9 | . 10 | ` 11 | -------------------------------------------------------------------------------- /test/resources/type-index-listed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample LDNode Public Type Registry index resource, 3 | * for use with `identity-test.js` 4 | */ 5 | module.exports = `@prefix solid: . 6 | @prefix vcard: . 7 | 8 | <> 9 | a solid:TypeIndex ; 10 | a solid:ListedDocument. 11 | 12 | <#ab09fd> a solid:TypeRegistration; 13 | solid:forClass vcard:AddressBook; 14 | solid:instance . 15 | ` 16 | -------------------------------------------------------------------------------- /test/resources/type-index-unlisted.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample LDNode Private Type Registry index resource, 3 | * for use with `identity-test.js` 4 | */ 5 | module.exports = `@prefix solid: . 6 | @prefix sioc: . 7 | 8 | <> 9 | a solid:TypeIndex ; 10 | a solid:UnlistedDocument. 11 | 12 | <#ab09cc> a solid:TypeRegistration; 13 | solid:forClass sioc:Post; 14 | solid:instanceContainer . 15 | ` 16 | -------------------------------------------------------------------------------- /test/unit/app-registry-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const nock = require('nock') 4 | const test = require('tape') 5 | 6 | const appRegistry = require('../../src/app-registry') 7 | const parseGraph = require('../../src/util/graph-util').parseGraph 8 | const registry = require('../../src/registry') 9 | const AppRegistration = require('../../src/solid/app-registration') 10 | const SolidProfile = require('../../src/solid/profile') 11 | 12 | const rdf = require('../../src/util/rdf-parser') 13 | const vocab = require('solid-namespace')(rdf) 14 | const webClient = require('solid-web-client')(rdf) 15 | 16 | const sampleProfileUrl = 'https://localhost:8443/profile/card' 17 | const rawProfileSource = require('../resources/profile-extended') 18 | const parsedProfileGraph = parseGraph( 19 | sampleProfileUrl, 20 | rawProfileSource, 21 | 'text/turtle', 22 | rdf 23 | ) 24 | 25 | test('blankPublicAppRegistry() test', function (t) { 26 | let blankRegistry = appRegistry.blankPublicAppRegistry(rdf) 27 | t.equal(blankRegistry.slug, 'publicAppRegistry.ttl') 28 | t.notOk(blankRegistry.uri) 29 | t.equal(typeof blankRegistry.data, 'string') 30 | t.ok(blankRegistry.graph) 31 | t.end() 32 | }) 33 | 34 | test('blankPrivateAppRegistry() test', function (t) { 35 | let blankRegistry = appRegistry.blankPrivateAppRegistry(rdf) 36 | t.equal(blankRegistry.slug, 'privateAppRegistry.ttl') 37 | t.notOk(blankRegistry.uri) 38 | t.equal(typeof blankRegistry.data, 'string') 39 | t.ok(blankRegistry.graph) 40 | t.end() 41 | }) 42 | 43 | test('appRegistry isListed() test', function (t) { 44 | var url = 'https://localhost:8443/profile/publicAppRegistry.ttl' 45 | var rawSource = require('../resources/app-registry-listed') 46 | var graph = parseGraph(url, rawSource, 'text/turtle', rdf) 47 | var result = registry.isListed(graph, rdf) 48 | t.ok(result) 49 | t.end() 50 | }) 51 | 52 | test('appRegistry isUnlisted() test', function (t) { 53 | var url = 'https://localhost:8443/profile/privateAppRegistry.ttl' 54 | var rawSource = require('../resources/app-registry-unlisted') 55 | var graph = parseGraph(url, rawSource, 'text/turtle', rdf) 56 | var result = registry.isUnlisted(graph, rdf) 57 | t.ok(result) 58 | t.end() 59 | }) 60 | 61 | test('new app registration test', function (t) { 62 | let app = new AppRegistration() 63 | t.notOk(app.isListed, 'An app registration is unlisted by default') 64 | t.deepEqual(app.types, []) 65 | t.end() 66 | }) 67 | 68 | test('app registration isValid() test', function (t) { 69 | let app = new AppRegistration() 70 | t.notOk(app.isValid(), 'A new/empty app registration should be not valid') 71 | app.name = 'Contact Manager' 72 | t.notOk(app.isValid()) 73 | app.redirectTemplateUri = 'https://solid.github.io/contacts/?uri={uri}' 74 | t.notOk(app.isValid()) 75 | app.types.push(vocab.vcard('AddressBook')) 76 | t.ok(app.isValid(), 77 | 'A registration should be valid with a name, redirectTemplateUri, and at least one type') 78 | t.end() 79 | }) 80 | 81 | test('app registrationsFromGraph test', function (t) { 82 | var url = 'https://localhost:8443/profile/publicAppRegistry.ttl' 83 | var rawSource = require('../resources/app-registry-listed') 84 | var graph = parseGraph(url, rawSource, 'text/turtle', rdf) 85 | var isListed = true 86 | var registrations = appRegistry.registrationsFromGraph(graph, 87 | vocab.vcard('AddressBook'), rdf) 88 | var app = registrations[0] 89 | t.equal(app.name, 'Contact Manager') 90 | t.equal(app.shortdesc, 'A reference contact manager') 91 | t.equal(app.redirectTemplateUri, 'https://solid.github.io/contacts/?uri={uri}') 92 | t.end() 93 | }) 94 | 95 | test('app registry addToAppRegistry() updates the profile with new registry when there was previously nothing in the app registry', t => { 96 | nock('https://localhost:8443/') 97 | .patch('/settings/publicAppRegistry.ttl') 98 | .reply(200) 99 | 100 | const profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 101 | const app = new AppRegistration( 102 | { 103 | name: 'Example App', 104 | shortdesc: 'An example app registration for testing', 105 | redirectTemplateUri: 'https://example.com/app/?uri={uri}' 106 | }, 107 | [], 108 | true 109 | ) 110 | 111 | appRegistry.addToAppRegistry(profile, app, webClient) 112 | .then(updatedProfile => { 113 | app.rdfStatements(rdf).map(st => { 114 | t.ok( 115 | updatedProfile.appRegistryListed.graph.anyStatementMatching( 116 | st.subject, st.predicate, st.object 117 | ) 118 | ) 119 | }) 120 | t.end() 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /test/unit/identity-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const sinon = require('sinon') 4 | 5 | const $rdf = require('rdflib') 6 | const identity = require('../../src/identity') 7 | 8 | test('getProfile can handle WebID which does HTTP 303 redirect', t => { 9 | let webId = 'https://idp.example/alice' 10 | let profileUrl = 'https://dataset.example/alice' 11 | let graph = $rdf.graph() 12 | graph.add($rdf.sym(profileUrl), 13 | $rdf.sym('http://xmlns.com/foaf/0.1/primaryTopic'), 14 | $rdf.sym(webId)) 15 | let response = { parsedGraph: sinon.stub().returns(graph), 16 | url: profileUrl } 17 | let client = { get: sinon.stub().returns(Promise.resolve(response)) } 18 | identity.getProfile(webId, { ignoreExtended: true }, client, $rdf) 19 | .then(profile => { 20 | t.equal(profile.webId, webId) 21 | t.equal(profile.baseProfileUrl, profileUrl) 22 | t.end() 23 | }).catch((e) => { 24 | t.error(e) 25 | t.end() 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/unit/registry-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const registry = require('../../src/registry') 5 | const rdf = require('../../src/util/rdf-parser') 6 | const parseGraph = require('../../src/util/graph-util').parseGraph 7 | 8 | test('registry.isListed - invalid predicate', t => { 9 | let url = 'https://example.com/registry' 10 | let source = `@prefix solid: . 11 | <> solid:ListedDocument.` 12 | 13 | let graph = parseGraph(url, source, 'text/turtle', rdf) 14 | 15 | t.notOk(registry.isListed(graph, rdf)) 16 | t.end() 17 | }) 18 | 19 | test('registry.isListed - valid predicate', t => { 20 | let url = 'https://example.com/registry' 21 | let source = `@prefix solid: . 22 | <> a solid:ListedDocument.` 23 | 24 | let graph = parseGraph(url, source, 'text/turtle', rdf) 25 | 26 | t.ok(registry.isListed(graph, rdf)) 27 | t.end() 28 | }) 29 | 30 | test('registry.isUnlisted - invalid predicate', t => { 31 | let url = 'https://example.com/registry' 32 | let source = `@prefix solid: . 33 | <> solid:UnlistedDocument.` 34 | 35 | let graph = parseGraph(url, source, 'text/turtle', rdf) 36 | 37 | t.notOk(registry.isUnlisted(graph, rdf)) 38 | t.end() 39 | }) 40 | 41 | test('registry.isUnlisted - valid predicate', t => { 42 | let url = 'https://example.com/registry' 43 | let source = `@prefix solid: . 44 | <> a solid:UnlistedDocument.` 45 | 46 | let graph = parseGraph(url, source, 'text/turtle', rdf) 47 | 48 | t.ok(registry.isUnlisted(graph, rdf)) 49 | t.end() 50 | }) 51 | -------------------------------------------------------------------------------- /test/unit/solid-client-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const solid = require('../../src/index') 4 | 5 | test('solid-permissions api export test', t => { 6 | t.ok(solid.acl, 'solid-permissions lib not exposed as solid.acl') 7 | t.ok(solid.acl.Authorization) 8 | t.ok(solid.acl.PermissionSet) 9 | t.ok(solid.acl.READ && solid.acl.WRITE && solid.acl.APPEND && 10 | solid.acl.CONTROL) 11 | t.ok(solid.acl.ALL_MODES) 12 | t.end() 13 | }) 14 | -------------------------------------------------------------------------------- /test/unit/solid-profile-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var SolidProfile = require('../../src/solid/profile') 5 | var parseGraph = require('../../src/util/graph-util').parseGraph 6 | 7 | var rdf = require('../../src/util/rdf-parser') 8 | var vocab = require('solid-namespace')(rdf) 9 | 10 | var rawProfileSource = require('../resources/profile-extended') 11 | var sampleProfileUrl = 'https://localhost:8443/profile/card' 12 | var parsedProfileGraph = parseGraph(sampleProfileUrl, 13 | rawProfileSource, 'text/turtle', rdf) 14 | 15 | 16 | function getPrefsGraph (urlPrefs) { 17 | let rawPrefsSource = require('../resources/profile-private') 18 | let graphPrefs = parseGraph(urlPrefs, rawPrefsSource, 'text/turtle', rdf) 19 | return graphPrefs 20 | } 21 | 22 | /** 23 | * Returns a sample test profile with the following graphs loaded: 24 | * - test/resources/profile-extended.js 25 | * - test/resources/profile-private.js 26 | */ 27 | function sampleExtendedProfile () { 28 | let profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 29 | let urlPrivateProfile = 'https://localhost:8443/settings/prefs.ttl' 30 | let graphPrivateProfile = getPrefsGraph(urlPrivateProfile) 31 | profile.appendFromGraph(graphPrivateProfile, urlPrivateProfile) 32 | profile.isLoaded = true 33 | return profile 34 | } 35 | 36 | test('SolidProfile empty profile test', function (t) { 37 | let profile = new SolidProfile() 38 | t.notOk(profile.isLoaded, 'Empty profile - isLoaded should be false') 39 | t.notOk(profile.webId, 'Empty profile should not have webId set') 40 | t.notOk(profile.response, 'Empty profile - no response object') 41 | t.notOk(profile.inbox.uri || profile.inbox.graph, 'Empty profile - no inbox') 42 | t.notOk(profile.preferences.uri || profile.preferences.graph, 43 | 'Empty profile - no preferences') 44 | t.deepEqual(profile.storage, [], 45 | 'Empty profile - no storage') 46 | t.notOk(profile.hasStorage(), 'Empty profile - hasStorage() false') 47 | t.notOk(profile.typeIndexUnlisted.uri || profile.typeIndexUnlisted.graph, 48 | 'Empty profile - no private type registry index') 49 | t.notOk(profile.typeIndexListed.uri || profile.typeIndexListed.graph, 50 | 'Empty profile - no public type registry index') 51 | t.deepEqual(profile.relatedProfiles.sameAs, [], 52 | 'Empty profile - no sameAs') 53 | t.deepEqual(profile.relatedProfiles.seeAlso, [], 54 | 'Empty profile - no seeAlso') 55 | t.end() 56 | }) 57 | 58 | test('SolidProfile base profile url test', function (t) { 59 | t.plan(1) 60 | let profileUrl = 'https://localhost:8443/profile/card#me' 61 | let expectedBaseProfileUrl = 'https://localhost:8443/profile/card' 62 | let profile = new SolidProfile(profileUrl) 63 | t.equal(profile.baseProfileUrl, expectedBaseProfileUrl) 64 | }) 65 | 66 | test('SolidProfile parse webId test', function (t) { 67 | t.plan(1) 68 | let profileUrl = 'https://localhost:8443/profile/card' 69 | let profile = new SolidProfile(profileUrl, parsedProfileGraph, rdf) 70 | // Make sure the webId (different from the profileUrl) was parsed correctly 71 | let expectedWebId = 'https://localhost:8443/profile/card#me' 72 | t.equal(profile.webId, expectedWebId) 73 | }) 74 | 75 | test('SolidProfile parsed profile test', function (t) { 76 | let profileUrl = 'https://localhost:8443/profile/card' 77 | let profile = new SolidProfile(profileUrl, parsedProfileGraph, rdf) 78 | t.equal(profile.name, 'Alice', 79 | 'Name should be pre-loaded for a parsed profile') 80 | t.equal(profile.picture, 'https://localhost:8443/profile/img.png', 81 | 'Picture url should be pre-loaded for a parsed profile') 82 | t.end() 83 | }) 84 | 85 | test('SolidProfile .find() test', function (t) { 86 | let profileUrl = 'https://localhost:8443/profile/card' 87 | let profile = new SolidProfile(profileUrl, parsedProfileGraph, rdf) 88 | let expectedAnswer = 'Alice' 89 | t.equal(profile.find(vocab.foaf('name')), expectedAnswer, 90 | '.find() should fetch name') 91 | expectedAnswer = 'https://localhost:8443/settings/privateProfile2.ttl' 92 | t.equal(profile.find(vocab.owl('sameAs')), expectedAnswer, 93 | '.find() should fetch owl:sameAs') 94 | 95 | t.notOk(profile.find(vocab.solid('invalidPredicate')), 96 | 'Trying to find() non-existent resources should return null') 97 | t.end() 98 | }) 99 | 100 | test('SolidProfile .findAll() test', function (t) { 101 | let profileUrl = 'https://localhost:8443/profile/card' 102 | let profile = new SolidProfile(profileUrl, parsedProfileGraph, rdf) 103 | let expectedAnswer = ['Alice'] 104 | t.deepEqual(profile.findAll(vocab.foaf('name')), expectedAnswer, 105 | '.findAll() should fetch all names') 106 | expectedAnswer = ['https://localhost:8443/settings/privateProfile2.ttl'] 107 | t.deepEqual(profile.findAll(vocab.owl('sameAs')), expectedAnswer, 108 | '.findAll() should fetch all owl:sameAs values') 109 | 110 | t.deepEqual(profile.findAll(vocab.solid('invalidPredicate')), [], 111 | 'findAll() on non-existent resources should return []') 112 | t.end() 113 | }) 114 | 115 | test('SolidProfile preferences test', function (t) { 116 | let profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 117 | let expectedPreferences = 'https://localhost:8443/settings/prefs.ttl' 118 | t.equal(profile.preferences.uri, expectedPreferences) 119 | t.end() 120 | }) 121 | 122 | test('SolidProfile relatedProfilesLinks() test', function (t) { 123 | let profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 124 | // Make sure the Preferences, seeAlso and sameAs are parsed 125 | let expectedLinks = 126 | [ 127 | 'https://localhost:8443/settings/prefs.ttl', 128 | 'https://localhost:8443/settings/privateProfile1.ttl', 129 | 'https://localhost:8443/settings/privateProfile2.ttl' 130 | ] 131 | t.deepEqual(profile.relatedProfilesLinks().sort(), expectedLinks) 132 | t.end() 133 | }) 134 | 135 | test('SolidProfile inbox test', function (t) { 136 | let profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 137 | let expectedInboxLink = 'https://localhost:8443/inbox/' 138 | t.equal(profile.inbox.uri, expectedInboxLink) 139 | t.end() 140 | }) 141 | 142 | test('SolidProfile storage test', function (t) { 143 | let profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 144 | let expectedStorageLinks = ['https://localhost:8443/'] 145 | t.deepEqual(profile.storage, expectedStorageLinks) 146 | t.ok(profile.hasStorage()) 147 | t.end() 148 | }) 149 | 150 | test('SolidProfile extended profile test', function (t) { 151 | // Load the initial parsed profile graph 152 | // The public profile has the link to publicTypeIndex.ttl 153 | let profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 154 | // Test that the parsed profile graph is loaded, and contains the name 155 | let name = profile.parsedGraph 156 | .any(rdf.sym(profile.webId), vocab.foaf('name')).value 157 | t.equal(name, 'Alice') 158 | 159 | // Also load and parse the private profile (prefs.ttl) resource 160 | // This is where the link to privateTypeIndex.ttl comes from 161 | profile = sampleExtendedProfile() 162 | // profile is an Extended Profile at this point 163 | 164 | // Make sure the original parsed graph is not overwritten at this point 165 | name = profile.parsedGraph 166 | .any(rdf.sym(profile.webId), vocab.foaf('name')).value 167 | t.equal(name, 'Alice') 168 | t.end() 169 | }) 170 | 171 | test('SolidProfile typeRegistryDefaultContainer() test', function (t) { 172 | let profile = new SolidProfile() 173 | t.equal(profile.typeRegistryDefaultContainer(), '/profile/', 174 | 'Default type registry uri for a profile without a web id should be /profile/') 175 | 176 | let profileUrl = 'https://example.com/test/card' 177 | profile = new SolidProfile(profileUrl) 178 | t.equal(profile.typeRegistryDefaultContainer(), 'https://example.com/test/', 179 | 'Default type registry uri should use the same container as profile') 180 | t.end() 181 | }) 182 | 183 | test('SolidProfile privateProfileUri() test', function (t) { 184 | let profile = new SolidProfile() 185 | t.equal(profile.privateProfileUri(), '/settings/prefs.ttl', 186 | 'Default private profile uri for new profile should be /settings/prefs.ttl') 187 | 188 | t.end() 189 | }) 190 | 191 | test('SolidProfile hasTypeRegistry*() test', function (t) { 192 | let profileEmpty = new SolidProfile() 193 | t.throws(function () { 194 | profileEmpty.hasTypeRegistryPrivate() 195 | }, 'Calling hasTypeRegistryPrivate() on unloaded profile should throw an error') 196 | t.throws(function () { 197 | profileEmpty.hasTypeRegistryPublic() 198 | }, 'Calling hasTypeRegistryPublic() on unloaded profile should throw an error') 199 | 200 | // Fake loading the profile. Now the hasTypeRegistry* methods should be false 201 | profileEmpty.isLoaded = true 202 | t.notOk(profileEmpty.hasTypeRegistryPublic(), 203 | 'Empty just-loaded profile should not have a public type registry') 204 | t.notOk(profileEmpty.hasTypeRegistryPrivate(), 205 | 'Empty just-loaded profile should not have a private type registry') 206 | 207 | let profileExtended = sampleExtendedProfile() 208 | t.ok(profileExtended.hasTypeRegistryPublic(), 209 | 'Sample extended profile should have a link to the public type registry') 210 | t.ok(profileExtended.hasTypeRegistryPrivate(), 211 | 'Sample extended profile should have a link to the private type registry') 212 | t.end() 213 | }) 214 | -------------------------------------------------------------------------------- /test/unit/type-registry-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const registry = require('../../src/registry') 4 | const test = require('tape') 5 | const nock = require('nock') 6 | const identity = require('../../src/identity') 7 | const typeRegistry = require('../../src/type-registry') 8 | const parseGraph = require('../../src/util/graph-util').parseGraph 9 | const SolidProfile = require('../../src/solid/profile') 10 | const rdf = require('../../src/util/rdf-parser') 11 | const vocab = require('solid-namespace')(rdf) 12 | const webClient = require('solid-web-client')(rdf) 13 | 14 | const rawProfileSource = require('../resources/profile-extended') 15 | const rawPrivateProfileSource = require('../resources/profile-private') 16 | const rawIndexSourceListed = require('../resources/type-index-listed') 17 | const rawIndexSourceUnlisted = require('../resources/type-index-unlisted') 18 | const sampleProfileUrl = 'https://localhost:8443/profile/card' 19 | const parsedProfileGraph = parseGraph(sampleProfileUrl, 20 | rawProfileSource, 'text/turtle', rdf) 21 | 22 | test('blankPrivateTypeIndex() test', function (t) { 23 | let blankIndex = typeRegistry.blankPrivateTypeIndex(rdf) 24 | t.equal(blankIndex.slug, 'privateTypeIndex.ttl') 25 | t.notOk(blankIndex.uri) 26 | t.equal(typeof blankIndex.data, 'string') 27 | t.ok(blankIndex.graph) 28 | t.end() 29 | }) 30 | 31 | test('blankPublicTypeIndex() test', function (t) { 32 | let blankIndex = typeRegistry.blankPublicTypeIndex(rdf) 33 | t.equal(blankIndex.slug, 'publicTypeIndex.ttl') 34 | t.notOk(blankIndex.uri) 35 | t.equal(typeof blankIndex.data, 'string') 36 | t.ok(blankIndex.graph) 37 | t.end() 38 | }) 39 | 40 | test('typeRegistry isListed() test', function (t) { 41 | var url = 'https://localhost:8443/profile/publicTypeIndex.ttl' 42 | var rawIndexSource = require('../resources/type-index-listed') 43 | var graph = parseGraph(url, rawIndexSource, 'text/turtle', rdf) 44 | var result = registry.isListed(graph, rdf) 45 | t.ok(result) 46 | t.end() 47 | }) 48 | 49 | test('typeRegistry isUnlisted() test', function (t) { 50 | var url = 'https://localhost:8443/profile/privateTypeIndex.ttl' 51 | var rawIndexSource = require('../resources/type-index-unlisted') 52 | var graph = parseGraph(url, rawIndexSource, 'text/turtle', rdf) 53 | var result = registry.isUnlisted(graph, rdf) 54 | t.ok(result) 55 | t.end() 56 | }) 57 | 58 | test('registerType - throws error for invalid arguments', function (t) { 59 | let profile = new SolidProfile() // not loaded 60 | t.throws(function () { 61 | typeRegistry.registerType(profile) 62 | }, 'Registering a type without loading a profile throws an error') 63 | t.end() 64 | }) 65 | 66 | test('SolidProfile addTypeRegistry() test', function (t) { 67 | let urlListed = 'https://localhost:8443/settings/publicTypeIndex.ttl' 68 | let graphListedIndex = parseGraph(urlListed, rawIndexSourceListed, 69 | 'text/turtle', rdf) 70 | 71 | let urlUnlisted = 'https://localhost:8443/settings/privateTypeIndex.ttl' 72 | let graphUnlistedIndex = parseGraph(urlUnlisted, rawIndexSourceUnlisted, 73 | 'text/turtle', rdf) 74 | let profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 75 | 76 | profile.addTypeRegistry(graphListedIndex, urlListed) 77 | profile.addTypeRegistry(graphUnlistedIndex, urlUnlisted) 78 | profile.isLoaded = true 79 | 80 | // Look up the address book (loaded from public registry) 81 | var result = 82 | profile.typeRegistryForClass(vocab.vcard('AddressBook')) 83 | t.equal(result.length, 1) // one listed registry match 84 | var registration = result[0] 85 | t.ok(registration.registrationUri) 86 | t.equal(registration.rdfClass.uri, vocab.vcard('AddressBook').uri) 87 | t.equal(registration.locationType, 'instance') 88 | t.equal(registration.locationUri, 89 | 'https://localhost:8443/contacts/addressBook.ttl') 90 | t.ok(registration.isListed) 91 | 92 | // Look up the SIOC posts (loaded from the unlisted registry) 93 | result = 94 | profile.typeRegistryForClass(vocab.sioc('Post')) 95 | t.equal(result.length, 1) // one unlisted registry match 96 | registration = result[0] 97 | t.ok(registration.registrationUri) 98 | t.equal(registration.rdfClass.uri, vocab.sioc('Post').uri) 99 | t.equal(registration.locationType, 'container') 100 | t.equal(registration.locationUri, 101 | 'https://localhost:8443/posts/') 102 | t.notOk(registration.isListed) 103 | 104 | // var classToRegister = vocab.vcard('Contact') 105 | // var location = 'https://localhost:8443/contacts/' 106 | // var locationType = 'container' 107 | // profile.registerType(classToRegister, location, locationType) 108 | 109 | t.end() 110 | }) 111 | 112 | test('type registry addToTypeIndex() updates the profile with new registry when there was previously nothing in the type index', t => { 113 | nock('https://localhost:8443/') 114 | .patch('/settings/publicTypeIndex.ttl') 115 | .reply(200) 116 | 117 | const profile = new SolidProfile(sampleProfileUrl, parsedProfileGraph, rdf) 118 | const rdfClass = vocab.vcard('Contact') 119 | const location = 'https://example.com/Contacts' 120 | const locationType = 'instance' 121 | const isListed = true 122 | 123 | typeRegistry.addToTypeIndex(profile, rdfClass, location, webClient, locationType, isListed) 124 | .then(updatedProfile => { 125 | const graph = profile.typeIndexListed.graph 126 | const subj = graph.any(null, vocab.rdf('type'), vocab.solid('TypeRegistration')).subject 127 | t.ok(graph.any(subj, vocab.rdf('type'), vocab.solid('TypeRegistration'))) 128 | t.ok(graph.any(subj, vocab.solid('forClass'), rdfClass)) 129 | t.ok(graph.any(subj, vocab.solid('instance'), rdf.namedNode(location))) 130 | t.end() 131 | }) 132 | }) 133 | 134 | test('loadTypeRegistry loads all the type registrations', t => { 135 | const headers = { 'Content-Type': 'text/turtle' } 136 | nock('https://localhost:8443/') 137 | .get('/profile/card') 138 | .reply(200, rawProfileSource, headers) 139 | .get('/settings/privateProfile1.ttl') 140 | .reply(200, rawPrivateProfileSource, headers) 141 | .get('/settings/publicTypeIndex.ttl') 142 | .reply(200, rawIndexSourceListed, headers) 143 | .get('/settings/privateTypeIndex.ttl') 144 | .reply(200, rawIndexSourceUnlisted, headers) 145 | 146 | identity.getProfile('https://localhost:8443/profile/card#me', {}, webClient, rdf) 147 | .then(solidProfile => typeRegistry.loadTypeRegistry(solidProfile, webClient)) 148 | .then(solidProfile => { 149 | t.ok(solidProfile.typeIndexListed.graph.any(null, vocab.rdf('type'), vocab.solid('TypeRegistration'))) 150 | t.ok(solidProfile.typeIndexUnlisted.graph.any(null, vocab.rdf('type'), vocab.solid('TypeRegistration'))) 151 | t.end() 152 | }) 153 | }) 154 | 155 | test('loadTypeRegistry succeeds when at least one type index succeeds in loading', t => { 156 | const headers = { 'Content-Type': 'text/turtle' } 157 | nock('https://localhost:8443/') 158 | .get('/profile/card') 159 | .reply(200, rawProfileSource, headers) 160 | .get('/settings/privateProfile1.ttl') 161 | .reply(200, rawPrivateProfileSource, headers) 162 | .get('/settings/publicTypeIndex.ttl') 163 | .reply(200, rawIndexSourceListed, headers) 164 | .get('/settings/privateTypeIndex.ttl') 165 | .reply(500) 166 | 167 | identity.getProfile('https://localhost:8443/profile/card#me', {}, webClient, rdf) 168 | .then(solidProfile => typeRegistry.loadTypeRegistry(solidProfile, webClient)) 169 | .then(solidProfile => { 170 | t.ok(solidProfile.typeIndexListed.graph.any(null, vocab.rdf('type'), vocab.solid('TypeRegistration'))) 171 | t.notOk(solidProfile.typeIndexUnlisted.graph) 172 | t.end() 173 | }) 174 | }) 175 | 176 | test('loadTypeRegistry fails when all of the type indices fail to load', t => { 177 | const headers = { 'Content-Type': 'text/turtle' } 178 | nock('https://localhost:8443/') 179 | .get('/profile/card') 180 | .reply(200, rawProfileSource, headers) 181 | .get('/settings/privateProfile1.ttl') 182 | .reply(200, rawPrivateProfileSource, headers) 183 | .get('/settings/publicTypeIndex.ttl') 184 | .reply(500) 185 | .get('/settings/privateTypeIndex.ttl') 186 | .reply(500) 187 | 188 | identity.getProfile('https://localhost:8443/profile/card#me', {}, webClient, rdf) 189 | .then(solidProfile => typeRegistry.loadTypeRegistry(solidProfile, webClient)) 190 | .catch(error => { 191 | t.equal(error.message, 'Could not load any type index') 192 | t.end() 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /test/unit/vocab-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var solid = require('../../src/index') 5 | 6 | test('solid.vocab test', function (t) { 7 | t.plan(1) 8 | t.ok(solid.vocab.ldp('Resource'), 'vocab.ldp("Resource") should exist') 9 | }) 10 | -------------------------------------------------------------------------------- /vendor/qunit-1.21.0.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.21.0 3 | * https://qunitjs.com/ 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * https://jquery.org/license 8 | * 9 | * Date: 2016-02-01T13:07Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-testrunner-toolbar label { 56 | display: inline-block; 57 | padding: 0 0.5em 0 0.1em; 58 | } 59 | 60 | #qunit-banner { 61 | height: 5px; 62 | } 63 | 64 | #qunit-testrunner-toolbar { 65 | padding: 0.5em 1em 0.5em 1em; 66 | color: #5E740B; 67 | background-color: #EEE; 68 | overflow: hidden; 69 | } 70 | 71 | #qunit-filteredTest { 72 | padding: 0.5em 1em 0.5em 1em; 73 | background-color: #F4FF77; 74 | color: #366097; 75 | } 76 | 77 | #qunit-userAgent { 78 | padding: 0.5em 1em 0.5em 1em; 79 | background-color: #2B81AF; 80 | color: #FFF; 81 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 82 | } 83 | 84 | #qunit-modulefilter-container { 85 | float: right; 86 | padding: 0.2em; 87 | } 88 | 89 | .qunit-url-config { 90 | display: inline-block; 91 | padding: 0.1em; 92 | } 93 | 94 | .qunit-filter { 95 | display: block; 96 | float: right; 97 | margin-left: 1em; 98 | } 99 | 100 | /** Tests: Pass/Fail */ 101 | 102 | #qunit-tests { 103 | list-style-position: inside; 104 | } 105 | 106 | #qunit-tests li { 107 | padding: 0.4em 1em 0.4em 1em; 108 | border-bottom: 1px solid #FFF; 109 | list-style-position: inside; 110 | } 111 | 112 | #qunit-tests > li { 113 | display: none; 114 | } 115 | 116 | #qunit-tests li.running, 117 | #qunit-tests li.pass, 118 | #qunit-tests li.fail, 119 | #qunit-tests li.skipped { 120 | display: list-item; 121 | } 122 | 123 | #qunit-tests.hidepass { 124 | position: relative; 125 | } 126 | 127 | #qunit-tests.hidepass li.running, 128 | #qunit-tests.hidepass li.pass { 129 | visibility: hidden; 130 | position: absolute; 131 | width: 0; 132 | height: 0; 133 | padding: 0; 134 | border: 0; 135 | margin: 0; 136 | } 137 | 138 | #qunit-tests li strong { 139 | cursor: pointer; 140 | } 141 | 142 | #qunit-tests li.skipped strong { 143 | cursor: default; 144 | } 145 | 146 | #qunit-tests li a { 147 | padding: 0.5em; 148 | color: #C2CCD1; 149 | text-decoration: none; 150 | } 151 | 152 | #qunit-tests li p a { 153 | padding: 0.25em; 154 | color: #6B6464; 155 | } 156 | #qunit-tests li a:hover, 157 | #qunit-tests li a:focus { 158 | color: #000; 159 | } 160 | 161 | #qunit-tests li .runtime { 162 | float: right; 163 | font-size: smaller; 164 | } 165 | 166 | .qunit-assert-list { 167 | margin-top: 0.5em; 168 | padding: 0.5em; 169 | 170 | background-color: #FFF; 171 | 172 | border-radius: 5px; 173 | } 174 | 175 | .qunit-source { 176 | margin: 0.6em 0 0.3em; 177 | } 178 | 179 | .qunit-collapsed { 180 | display: none; 181 | } 182 | 183 | #qunit-tests table { 184 | border-collapse: collapse; 185 | margin-top: 0.2em; 186 | } 187 | 188 | #qunit-tests th { 189 | text-align: right; 190 | vertical-align: top; 191 | padding: 0 0.5em 0 0; 192 | } 193 | 194 | #qunit-tests td { 195 | vertical-align: top; 196 | } 197 | 198 | #qunit-tests pre { 199 | margin: 0; 200 | white-space: pre-wrap; 201 | word-wrap: break-word; 202 | } 203 | 204 | #qunit-tests del { 205 | background-color: #E0F2BE; 206 | color: #374E0C; 207 | text-decoration: none; 208 | } 209 | 210 | #qunit-tests ins { 211 | background-color: #FFCACA; 212 | color: #500; 213 | text-decoration: none; 214 | } 215 | 216 | /*** Test Counts */ 217 | 218 | #qunit-tests b.counts { color: #000; } 219 | #qunit-tests b.passed { color: #5E740B; } 220 | #qunit-tests b.failed { color: #710909; } 221 | 222 | #qunit-tests li li { 223 | padding: 5px; 224 | background-color: #FFF; 225 | border-bottom: none; 226 | list-style-position: inside; 227 | } 228 | 229 | /*** Passing Styles */ 230 | 231 | #qunit-tests li li.pass { 232 | color: #3C510C; 233 | background-color: #FFF; 234 | border-left: 10px solid #C6E746; 235 | } 236 | 237 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 238 | #qunit-tests .pass .test-name { color: #366097; } 239 | 240 | #qunit-tests .pass .test-actual, 241 | #qunit-tests .pass .test-expected { color: #999; } 242 | 243 | #qunit-banner.qunit-pass { background-color: #C6E746; } 244 | 245 | /*** Failing Styles */ 246 | 247 | #qunit-tests li li.fail { 248 | color: #710909; 249 | background-color: #FFF; 250 | border-left: 10px solid #EE5757; 251 | white-space: pre; 252 | } 253 | 254 | #qunit-tests > li:last-child { 255 | border-radius: 0 0 5px 5px; 256 | } 257 | 258 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 259 | #qunit-tests .fail .test-name, 260 | #qunit-tests .fail .module-name { color: #000; } 261 | 262 | #qunit-tests .fail .test-actual { color: #EE5757; } 263 | #qunit-tests .fail .test-expected { color: #008000; } 264 | 265 | #qunit-banner.qunit-fail { background-color: #EE5757; } 266 | 267 | /*** Skipped tests */ 268 | 269 | #qunit-tests .skipped { 270 | background-color: #EBECE9; 271 | } 272 | 273 | #qunit-tests .qunit-skipped-label { 274 | background-color: #F4FF77; 275 | display: inline-block; 276 | font-style: normal; 277 | color: #366097; 278 | line-height: 1.8em; 279 | padding: 0 0.5em; 280 | margin: -0.4em 0.4em -0.4em 0; 281 | } 282 | 283 | /** Result */ 284 | 285 | #qunit-testresult { 286 | padding: 0.5em 1em 0.5em 1em; 287 | 288 | color: #2B81AF; 289 | background-color: #D2E0E6; 290 | 291 | border-bottom: 1px solid #FFF; 292 | } 293 | #qunit-testresult .module-name { 294 | font-weight: 700; 295 | } 296 | 297 | /** Fixture */ 298 | 299 | #qunit-fixture { 300 | position: absolute; 301 | top: -10000px; 302 | left: -10000px; 303 | width: 1000px; 304 | height: 1000px; 305 | } 306 | -------------------------------------------------------------------------------- /webpack-no-rdflib.config.js: -------------------------------------------------------------------------------- 1 | var config = require('./webpack.config') 2 | 3 | // Exclude rdflib 4 | config.externals['rdflib'] = '$rdf' 5 | config.output.filename = 'solid-client-lite.min.js' 6 | 7 | module.exports = config 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = { 4 | entry: [ 5 | './lib/index.js' 6 | ], 7 | output: { 8 | path: path.join(__dirname, '/dist/'), 9 | filename: 'solid-client.min.js', 10 | library: 'SolidClient', 11 | libraryTarget: 'var' 12 | }, 13 | resolve: { 14 | modulesDirectories: ['node_modules'], 15 | fallback: path.join(__dirname, 'node_modules') 16 | }, 17 | resolveLoader: { fallback: path.join(__dirname, 'node_modules') }, 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /(node_modules)/, 23 | loader: 'babel', 24 | query: { 25 | presets: ['es2015'] 26 | } 27 | }, 28 | { 29 | test: /\.json$/, 30 | loader: 'json' 31 | } 32 | ] 33 | }, 34 | node: { 35 | fs: 'empty' 36 | }, 37 | externals: { 38 | 'xhr2': 'XMLHttpRequest', 39 | 'xmlhttprequest': 'XMLHttpRequest', 40 | 'node-fetch': 'fetch', 41 | 'text-encoding': 'TextEncoder', 42 | 'urlutils': 'URL', 43 | 'webcrypto': 'crypto' 44 | }, 45 | devtool: 'source-map' 46 | } 47 | --------------------------------------------------------------------------------