Skip to content
This repository was archived by the owner on Apr 13, 2022. It is now read-only.

solid-contrib/solid-client

Repository files navigation

solid-client

NPM VersionBuild Status

NOT SUPPORTED - PLEASE USE solid-auth-client

Javascript library for writing Solid applications. (See Changelog for version history.)

Usage

The solid client can be used by solid applications that run in the browser or on Node.js. A minified UMD bundle is provided along with the regular set of CommonJS modules.

For example:

<!DOCTYPE HTML><html><head><scriptsrc="dist/solid-client.min.js"></script></head><body><script>varsolid=SolidClient</script></body></html>

Or, using a module loader:

varsolid=require('solid-client')

See the installation docs for more installation examples.

Take a look at the solid-client demo page (source located in demo/index.html) for usage examples.

Tutorials

If you would like to learn how to build Solid apps using solid-client, please see:

Developing solid-client

Node version: 6.0+.

Install dev dependencies:

npm install 

Building (uses Browserify, builds to solid-client.js and dist/solid-client-no-rdflib.js):

npm run build 

Testing

To run the unit tests:

npm test 

This runs the Tape unit test suite.

Releases

The following steps specify how to release solid-client:

Make sure you're at the HEAD of master.

$ git checkout master && git pull

Run npm version to bump the package version via git commit and git tags.

# refer to http://semver.org/ for which of (major, minor, patch) to use $ npm version [major|minor|patch]

Next, push the commit and tags:

$ git push --follow-tags

Finally release the package to npmjs.

$ npm publish

Logging In and User Profiles

Before doing any sort of reading or writing of Solid resources, your app will likely need to authenticate a user and load their profile, so let's start with those sections.

Authentication

Solid currently uses WebID-TLS for authentication, which relies on a web browser's built-in key store to manage certificates and prompt the user to select the correct certificate when accessing a server.

Solid servers must always return a Solid-specific HTTP header called User, which contains the WebID that the user used to access this particular server. An empty header usually means that the user is not authenticated.

Detecting the Current Logged-in User

Most of the WebID-TLS authentication process takes place before a web page gets fully loaded and the javascript code has had a chance to run. Since client-side Javascript code does not have access to most HTTP headers (including the User header) of the page on which it runs, how does an app discover if there is an already authenticated user that is accessing it?

The current best practice answer is -- the app should do an Ajax/XHR HEAD request to the relevant resource:

  1. either to the current page if it's a standalone app, or
  2. to the requested resource (if it's an app that's acting as a viewer or editor, and requires a resource URI as a parameter)

For the first case (standalone apps), solid-client provides a convenience solid.currentUser() method (which does a HEAD request to the current page in the background). Usage:

solid.currentUser().then(function(currentWebId){if(currentWebId){console.log('Current WebID: %s',currentWebId)}else{console.log('You are not logged in')}})

For the second case (apps that are wrapping a resource as viewers or editors), client apps can just use a solid.login(targetUrl) function to return the current user's WebID. And if users are unable to log in, prompt the user to create an account with solid.signup().

Login example

Here is a typical example of authenticating a user and returning their WebID. The following login function, specific to your application, wraps the solid.login() function. If the promise is resolved, then an application will do something with the webId value. Otherwise, if the promise is rejected, the application may choose to display an error message.

HTML:

<ahref="#" onclick="login()">Login</a>

Javascript:

varsolid=require('solid')varlogin=function(){// Get the current usersolid.login().then(function(webId){// authentication succeeded; do something with the WebID stringconsole.log(webId)}).catch(function(err){// authentication failed; display some error messageconsole.log(err)})}

Signup example

The signup function is very similar to the login function, wrapping the solid.signup() function. It results in either a WebID or an error message being returned.

HTML:

<ahref="#" onclick="signup()">Sign up</a>

Javascript:

varsolid=require('solid')// Signup for a WebIDvarsignup=function(){solid.signup().then(function(webId){// authentication succeeded; do something with the WebID stringconsole.log(webId)}).catch(function(err){// authentication failed; display some error messageconsole.log(err)})}

User Profiles

Once you have a user's WebID (say, from a login() call), it's often useful to load the user profile:

varprofile=solid.login().then(function(webId){// have the webId, now load the profilereturnsolid.getProfile(webId)})

The call to getProfile(url) loads the full extended profile: the profile document itself, any sameAs and seeAlso links it finds there, as well as the Preferences file.

Once a profile is loaded, you can access the values of the profile's pre-defined fields, or look for predicates in the profile's parsed graph using profile.find() and profile.findAll():

varns=solid.vocabsolid.login().then(function(webId){returnsolid.getProfile(webId)}).then(function(profile){profile.name// -> 'Alice'profile.picture// -> 'https://example.com/profile/icon.png'profile.find(ldp.solid('inbox'))// -> 'https://example.com/inbox/'profile.findAll(ns.owl('sameAs'))// -> [ url1, url2 ]}).catch(function(err){console.log('Error accessing profile: '+err)})

Profile App Registry

The profile provides an interface to the user's App Registry.

varns=solid.vocabsolid.login().then(function(webId){returnsolid.getProfile(webId)}).then(function(profile){returnprofile.loadAppRegistry()}).then(function(profile){// The profile has been updated, app registry loaded. Now you can register// apps with is.varoptions={name: 'Contact Manager',shortdesc: 'A reference contact manager',redirectTemplateUri: 'https://solid.github.io/contacts/?uri={uri}'}vartypesForApp=[ns.vcard('AddressBook')]varisListed=truevarapp=newAppRegistration(options,typesForApp,isListed)returnprofile.registerApp(app)}).then(function(profile){// The app entry was created. You can now query the registry for itreturnprofile.appsForType(ns.vcard('AddressBook'))}).then(function(registrationResults){varapp=registrationResults[0]app.name// -> 'Contact Manager'app.shortdesc// -> ...app.redirectTemplateUri})

User Type Registry Index

If your application needs to do data discovery, it can also call loadTypeRegistry() after loading the profile:

varprofile=solid.login().then(function(webId){returnsolid.getProfile(webId)}).then(function(profile){returnprofile.loadTypeRegistry()})

Now, both listed and unlisted type indexes are loaded, and you can look up where the user keeps various types.

varns=solid.vocab// .. load profile and load type registryvaraddressBookRegistrations=solid.getProfile(webId).then(function(profile){returnprofile.loadTypeRegistry()}).then(function(profile){returnprofile.typeRegistryForClass(ns.vcard('AddressBook'))})/*-->[ an IndexRegistration( locationUri: 'https://localhost:8443/public-contacts/AddressBook.ttl', locationType: 'instance', isListed: true ), an IndexRegistration( locationUri: 'https://localhost:8443/personal-address-books/', locationType: 'container', isListed: false )]*/

You can then load the resources from the returned locations, as usual.

addressBookRegistrations.forEach(function(registration){if(registration.isInstance()){// This is an instance (an individual resource)}elseif(registration.isContainer()){// This is a container with many address books}})

Registering (and un-registering) Types in the Type Registry

To register an RDF Class with a user's Type Registry (listed or unlisted), use profile.registerType():

varns=solid.vocab// .. load profilevarclassToRegister=vocab.sioc('Post')varlocationToRegister='https://localhost:8443/new-posts-container/'varisListed=trueprofile.registerType(classToRegister,locationToRegister,'container',isListed).then(function(profile){// Now the type is registered, and the profile's type registry is refreshed// querying the registry now will include the new containerprofile.typeRegistryForClass(ns.sioc('Post'))})// To remove the same class from registry:varclassToRemove=ns.sioc('Post')profile.unregisterType(classToRemove,isListed).then(function(profile){// Type is removedprofile.typeRegistryForClass(ns.sioc('Post'))// --> []})

Web operations

solid-client uses a mix of LDP and Solid-specific functions to manipulate Web resources. Please see the Solid spec for more details.

Getting information about a resource

Sometimes an application may need to get some useful meta data about a resource. For instance, it may want to find out where the ACL resource is. Clients should take notice to the fact that the solid.web.head() function will always successfully complete, even for resources that don't exists, since that is considered useful information. For instance, clients can use the solidResponse.xhr.status value will indicate whether the resource exists or not.

Here, for example, we can find out where the corresponding ACL resource is for our new blog post hello-world.

varsolid=require('solid')varurl='https://example.org/blog/hello-world'solid.web.head(url).then(function(solidResponse){console.log(solidResponse.acl)// the ACL uriif(!solidResponse.exists()){console.log("This resource doesn't exist")}elseif(solidResponse.xhr.status===403){if(solidResponse.isLoggedIn()){console.log("You don't have access to the resource")}else{console.log("Please authenticate")}}})

The SolidResponse object returned by most solid.web calls, including head(), contains the following properties:

  • url - the URL of the resource // https://example.org/blog/hello-world
  • acl - the URL of the corresponding .acl resource // https://example.org/blog/hello-world.acl
  • meta - the URL of the corresponding .meta resource // https://example.org/blog/hello-world.meta
  • types - An array of LDP types for the resource, if applicable. For example: [ 'http://www.w3.org/ns/ldp#LDPResource', 'http://www.w3.org/ns/ldp#Resource' ]
  • user - the WebID of the authenticated user (if authenticated) // https://user.example.org/profile#me
  • websocket - the URI of the corresponding websocket instance // wss://example.org/blog/hello-world
  • method - the HTTP verb (get, put, etc) of the original request that resulted in this response.
  • xhr - the raw XMLHttpRequest object (e.g. xhr.status)

The response object also has some convenience methods:

  • contentType() - returns the MIME type of the resource
  • isContainer() - determines whether the resource is a Container or a regular resource

Fetching a Resource

Assuming that a resource or a container exists (see creating resources and creating containers below), you can retrieve it using web.get():

solid.web.get(url).then(function(response){if(response.isContainer()){// This is an instance of SolidContainer, see Listing Containers belowfor(resourceUrlinresponse.resources){// iterate over resources}for(subcontainerUrlinresponse.containers){// iterate over sub-containers}}else{// Regular resourceconsole.log('Raw resource: %s',response.raw())// You can access the parsed graph (parsed by RDFLib.js):varparsedGraph=response.parsedGraph()}}).catch(function(err){console.log(err)// error object// ...})

Fetching a Parsed Graph

Once a resource is retrieved, we can access it as a parsed graph (here, parsed by rdflib.js). This graph can then be queried.

varsolid=require('solid')varvocab=solid.vocabvarurl='https://example.org/blog/hello-world'solid.web.get(url).then(function(response){vargraph=response.parsedGraph()// Print all statements matching resources of type foaf:Postconsole.log(graph.statementsMatching(undefined,vocab.rdf('type'),vocab.sioc('Post')))}).catch(function(err){console.log(err)// error object})

Creating a Solid Container

The Solid client offers a function called solid.web.createContainer(), which is used to create containers. The function accepts the following parameters:

  • parentDir (string) - the URL of the parent container in which the new resource/container will be created.
  • containerName (string) (optional) - the value for the Slug header -- i.e. the name of the new resource to be created; this value is optional.
  • options (object) - Optional hashmap of request options
  • data (string) - Optional RDF data serialized as text/turtle; can also be an empty string if no data will be sent.

In the example below we are also sending some meta data (semantics) about the container, setting its type to sioc:Blog.

// Assumes you've loaded rdflib.js and solid-client, see Dependences abovevarsolid=require('solid')varparentUrl='https://example.org/'varcontainerName='blog'varoptions={}vardata='<#this> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://rdfs.org/sioc/ns#Blog> .'solid.web.createContainer(parentUrl,containerName,options,data).then(function(solidResponse){console.log(solidResponse)// The resulting object has several useful properties.// See lib/solid/response.js for details// solidResponse.url - value of the Location header// solidResponse.acl - url of acl resource// solidResponse.meta - url of meta resource}).catch(function(err){console.log(err)// error object})

Note that the options and data parameters are optional, and you can simply do solid.web.createContainer(url, name).

Listing a Solid Container

To list the contents of a Solid container, just use solid.web.get(). This returns a promise that resolves to a SolidContainer instance, which will contain various useful properties:

  • A short name (.name) and absolute URI (.uri)
  • A .parsedGraph property for further RDF queries
  • A parsed list of links to all the contents (both containers and resources) (.contentsUris)
  • A list of RDF types to which the container belongs (.types)
  • A hashmap of all sub-Containers within this container, keyed by absolute uri (.containers)
  • A hashmap of all non-container Resources within this container, also keyed by absolute uri. (.resources)

Containers also have several convenience methods:

  • container.isEmpty() will return true when there are no sub-containers or resources inside it
  • container.findByType(rdfClass) will return an array of resources or containers that have the given rdfClass in their .types array

For example:

varcontainer=solid.web.get('/settings/').then(function(container){console.log(container)// See below})// container is an instance of SolidContainer (see lib/solid/container.js)container.uri// -> 'https://localhost:8443/settings/'container.name// -> 'settings'container.isEmpty()// -> falsecontainer.types// ->['http://www.w3.org/ns/ldp#BasicContainer','http://www.w3.org/ns/ldp#Container']container.contentsUris// ->['https://localhost:8443/settings/prefs.ttl','https://localhost:8443/settings/privateTypeIndex.ttl','https://localhost:8443/settings/testcontainer/']varsubContainer=container.containers['https://localhost:8443/settings/testcontainer/']subContainer.name// -> 'testcontainer'subContainer.types// ->['http://www.w3.org/ns/ldp#BasicContainer','http://www.w3.org/ns/ldp#Container','http://www.w3.org/ns/ldp#Resource']varresource=container.resources['https://localhost:8443/settings/privateTypeIndex.ttl']// resource - SolidResource instanceresource.name// -> 'privateTypeIndex.ttl'resource.types// ->['http://www.w3.org/ns/ldp#Resource','http://www.w3.org/ns/solid/terms#TypeIndex','http://www.w3.org/ns/solid/terms#UnlistedDocument']resource.isType('http://www.w3.org/ns/solid/terms#TypeIndex')// -> truecontainer.findByType('http://www.w3.org/ns/ldp#Resource')// ->[// a SolidContainer('testcontainer'),// a SolidResource('privateTypeIndex.ttl'),// a SolidResource('prefs.ttl')]

Creating a resource

Creating a regular LDP resource is done using the web.post() method.

In this example we will create the resource hello-world under the newly created blog/ container.

varsolid=require('solid')varparentDir='https://example.org/blog/'varslug='hello-world'vardata=`<> a <http://rdfs.org/sioc/ns#Post> <http://purl.org/dc/terms/title> "First post" ; <http://rdfs.org/sioc/ns#content> "Hello world! This is my first post" .`solid.web.post(parentDir,data,slug).then(function(response){console.log(response.url)// URL of the newly created resource}).catch(function(err){console.log(err)// error object})

Updating a resource

Sometimes we need to update a resource after making a small change. For instance, we sometimes need to delete a triple, or update the value of an object (technically by replacing the triple with a new one). Luckily, Solid allows us to use the HTTP PATCH operation to do very small changes.

Let's try to change the value of the title in our first post. To do so, we need to indicate which triple we want to replace, and then the triple that will replace it.

Let's create the statements and serialize them to Turtle before patching the blog post resource:

varrdf=require('rdflib')varurl='https://example.org/blog/hello-world'varvocab=ns.vocabvaroldTitleTriple=rdf.triple(rdf.namedNode(url),ns.dct('title'),rdf.literal("First post")).toCanonical()varnewTitleTriple=rdf.triple(rdf.namedNode(url),ns.dct('title'),rdf.literal("Hello")).toCanonical()

Now we can actually patch the resource. The solid.web.patch() function (also aliased to solid.web.update()) takes three arguments:

  • url (string) - the URL of the resource to be overwritten.
  • toDel (array) - an array of statements to be deleted, serialized as Turtle.
  • toIns (array) - an array of statements to be inserted, serialized as Turtle.
varsolid=require('solid')vartoDel=[oldTitleTriple]vartoIns=[newTitleTriple]solid.web.patch(url,toDel,toIns).then(function(response){console.log(response.xhr.status)// HTTP 200 (OK)}).catch(function(err){console.log(err)// error object})

Replacing a resource

We can also completely replace (overwrite) existing resources with new content, using the client's solid.web.put() function (also aliased to replace()). The function accepts the following parameters:

  • url (string) - the URL of the resource to be overwritten.
  • data (string) - RDF data serialized as text/turtle; can also be an empty string if no data will be sent.
  • mime (string) (optional) - the mime type for this resource; this value is optional and defaults to text/turtle.

Here is an example where we try to overwrite the existing resource hello-world, giving it a bogus type - http://example.org/#Post.

varsolid=require('solid')varurl='https://example.org/blog/hello-world'vardata='<> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/#Post> .'solid.web.put(url,data).then(function(response){console.log(response.xhr.status)// HTTP 200 (OK)}).catch(function(err){console.log(err)// error object})

Deleting a resource

Delete an RDF resource from the Web. For example, we can delete the blog post hello-world we created earlier, using the solid.web.del() function.

NOTE: while this function can also be used to delete containers, it will only work for empty containers. For now, app developers should make sure to empty a container by recursively calling this function on its contents.

varsolid=require('solid')varurl='https://example.org/blog/hello-world'solid.web.del(url).then(function(response){console.log(response)}).catch(function(err){console.log(err)// error object})

Managing Resource Permissions

Each Solid resource has a set of permissions that determine which user (identified by their WebID) has read and write access to it, called an ACL resource. (See the web-access-control-spec repo for the exact details.)

solid-client has a set of convenience methods to help developers manage those permissions.

Reading Permissions

To load the corresponding ACL resource, for a given file:

varsolid=require('solid')varresourceUrl='https://example.org/blog/hello-world'solid.getPermissions(resourceUrl).then(function(permissionSet){// Now the permission set, parsed from `hello-world.acl` is loaded,// and you can iterate over the individual authorizationspermissionSet.forEach(function(auth){if(auth.isAgent()){console.log('agent webId: '+auth.agent)}elseif(auth.isPublic()){// this permission is for everyone (acl:agentClass foaf:Agent)}elseif(auth.isGroup()){console.log('agentClass webId: '+auth.group)}// You can also use auth.webId() for all cases:console.log('agent/group webId: '+auth.webId())// You can check what sort of access modes are granted:auth.allowsRead()// -> true if the authorization contains acl:Read modeauth.allowsWrite()auth.allowsAppend()auth.allowsControl()// Check to see if this Authorization is inherited (`acl:default`)auth.isInherited()// -> false for a resource, usually true for container// Check to see if access is allowed from a given Originauth.allowsOrigin('https://example.com')})})

Note: You can read the permissions for a given resource only if you have acl:Control access mode for that resource. (You also need that access mode to edit those permissions, as well.)

You can also access individual authorizations from a resource set:

solid.getPermissions(resourceUrl).then(function(permissionSet){varauth=permissionSet.permissionFor(bobWebId)auth.webId()// -> bob's web idauth.allowsRead()// -> true if bob has acl:Read permissionauth.allModes()// -> array of access modes grantedauth.allOrigins()// -> array of allowed origin URLs// If this is for the root container's ACL, you can also load a user's// emails using the `mailTo` property. (Unofficial functionality)auth.mailTo// -> ['[email protected]', '[email protected]']})

Editing Permissions

To manage the set of permissions for a given resource (provided the current user has acl:Control access mode granted to them for that resource), use the convenience methods provided by PermissionSet.

The example below adds 3 different permissions:

  1. Allows Alice to Read, Write and Control the resource
  2. Allows Public Read access (that's the solid.acl.EVERYONE)
  3. Grants Bob Write access (in addition to the Read access he inherits from the above permission, since he's a member of the Public). Also, this Write access is only allowed from a particular origin.
varsolid=require('solid')varresourceUrl='https://example.org/blog/hello-world'varaliceWebId='https://alice.example.org/profile/card#me'varbobWebId='https://bob.example.org/profile/card#me'varallowedOrigin='https://example.org'solid.getPermissions(resourceUrl).then(function(permissionSet){returnpermissionSet.addPermission(aliceWebId,[solid.acl.READ,solid.acl.WRITE,solid.acl.CONTROL]).addPermission(solid.acl.EVERYONE,solid.acl.READ)// see also .addGroupPermission().addPermission(bobWebId,solid.acl.WRITE,allowedOrigin).save()}).then(function(response){console.log('Permissions saved successfully')}).catch(function(err){console.log('Error saving permissions')})

To delete all permissions associated with a resource, use clearPermissions(). Keep in mind that permissions are inherited from a resource's parent container, and if you delete an individual ACL resource, this simply means that the permissions reset to that of the upstream container. You can also clear the ACLs of the container, all the way up to the root storage container's ACL, which cannot be deleted. Refer to the ACL Inheritance Algorithm section of the spec.

// If you have an existing PermissionSet as a result of `getPermissions()`:solid.getPermissions('https://www.example.com/file1').then(function(permissionSet){returnpermissionSet.clear()// deletes the file1.acl resource})// Otherwise, use the helper function// solid.clearPermissions(resourceUrl) insteadsolid.clearPermissions('https://www.example.com/file1').then(function(response){// file1.acl is now deleted})

About

Javascript client library for Solid applications

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 10

Languages