UNPKG

solid-ui

Version:

UI library for writing Solid read-write-web applications

1,625 lines (1,351 loc) • 57.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); /** * signin.js * * Signing in, signing up, profile and preferences reloading * Type index management * * Many functions in this module take a context object, add to it, and return a promise of it. */ /* global localStorage confirm alert */ // const Solid = require('solid-client') var SolidTls = require('solid-auth-tls'); var $rdf = require('rdflib'); // const error = require('./widgets/error') var widgets = require('./widgets/index'); // const utils = require('./utils') var solidAuthClient = require('solid-auth-client'); var UI = { authn: require('./signin'), icons: require('./iconBase'), log: require('./log'), ns: require('./ns'), store: require('./store'), style: require('./style'), utils: require('./utils'), widgets: require('./widgets') // 2018-07-31 }; var ns = UI.ns; var kb = UI.store; module.exports = { checkUser: checkUser, // Async currentUser: currentUser, // Sync defaultTestUser: defaultTestUser, // Sync filterAvailablePanes: filterAvailablePanes, // Async findAppInstances: findAppInstances, findOriginOwner: findOriginOwner, getUserRoles: getUserRoles, // Async loadTypeIndexes: loadTypeIndexes, logIn: logIn, logInLoadProfile: logInLoadProfile, logInLoadPreferences: logInLoadPreferences, loginStatusBox: loginStatusBox, newAppInstance: newAppInstance, offlineTestID: offlineTestID, registerInTypeIndex: registerInTypeIndex, registrationControl: registrationControl, registrationList: registrationList, selectWorkspace: selectWorkspace, setACLUserPublic: setACLUserPublic, saveUser: saveUser, solidAuthClient: solidAuthClient }; // const userCheckSite = 'https://databox.me/' // Look for and load the User who has control over it function findOriginOwner(doc, callback) { var uri = doc.uri || doc; var i = uri.indexOf('://'); if (i < 0) return false; var j = uri.indexOf('/', i + 3); if (j < 0) return false; var origin = uri.slice(0, j + 1); // @@ TBC return origin; } // Promises versions // // These pass a context object which hold various RDF symbols // as they become available // // me RDF symbol for the users' webid // publicProfile The user's public profile, iff loaded // preferencesFile The user's personal preferences file, iff loaded // index.public The user's public type index file // index.private The user's private type index file // not RDF symbols: // noun A string in english for the type of thing -- like "address book" // instance An array of nodes which are existing instances // containers An array of nodes of containers of instances // div A DOM element where UI can be displayed // statusArea A DOM element (opt) progress stuff can be displayed, or error messages /** * @param webId {NamedNode} * @param context {Object} * * @returns {NamedNode|null} Returns the Web ID, after setting it */ function saveUser(webId, context) { var webIdUri, me; if (webId) { webIdUri = webId.uri || webId; var _me = $rdf.namedNode(webIdUri); if (context) { context.me = _me; } return _me; } return me || null; } /** * @returns {NamedNode|null} */ function defaultTestUser() { // Check for offline override var offlineId = offlineTestID(); if (offlineId) { return offlineId; } return null; } /** Checks syncronously whether user is logged in * * @returns Named Node or null */ function currentUser() { var str = localStorage['solid-auth-client']; if (str) { var da = JSON.parse(str); if (da.session && da.session.webId) { // @@ check has not expired return $rdf.sym(da.session.webId); } } return offlineTestID(); // null unless testing // JSON.parse(localStorage['solid-auth-client']).session.webId } /** * Resolves with the logged in user's Web ID * * @param context * * @returns {Promise<context>} */ function logIn(context) { var me = defaultTestUser(); // me is a NamedNode or null if (me) { context.me = me; return Promise.resolve(context); } return new Promise(function (resolve) { checkUser().then(function (webId) { // Already logged in? if (webId) { context.me = $rdf.sym(webId); console.log('logIn: Already logged in as ' + context.me); return resolve(context); } if (!context.div || !context.dom) { return resolve(context); } var box = loginStatusBox(context.dom, function (webIdUri) { saveUser(webIdUri, context); resolve(context); // always pass growing context }); context.div.appendChild(box); }); }); } /** * Logs the user in and loads their WebID profile document into the store * * @private * * @param context {Object} * * @returns {Promise<Object>} Resolves with the context after login / fetch */ function logInLoadProfile(context) { if (context.publicProfile) { return Promise.resolve(context); } // already done var fetcher = UI.store.fetcher; var profileDocument; return new Promise(function (resolve, reject) { return logIn(context).then(function (context) { var webID = context.me; if (!webID) { return reject(new Error('Could not log in')); } profileDocument = webID.doc(); // Load the profile into the knowledge base (fetcher.store) // withCredentials: Web arch should let us just load by turning off creds helps CORS // reload: Gets around a specifc old Chrome bug caching/origin/cors fetcher.load(profileDocument, { withCredentials: false, cache: 'reload' }).then(function (response) { context.publicProfile = profileDocument; resolve(context); })["catch"](function (err) { var message = 'Logged in but cannot load profile ' + profileDocument + ' : ' + err; if (context.div && context.dom) { context.div.appendChild(UI.widgets.errorMessageBlock(context.dom, message)); } reject(message); }); })["catch"](function (err) { reject(new Error("Can't log in: " + err)); }); }); } /** * Loads preferences file * Do this after having done log in and load profile * * @private * * @param context * * @returns {Promise<context>} */ function logInLoadPreferences(context) { if (context.preferencesFile) return Promise.resolve(context); // already done var kb = UI.store; var statusArea = context.statusArea || context.div || null; var progressDisplay; return new Promise(function (resolve, reject) { return logInLoadProfile(context).then(function (context) { var preferencesFile = kb.any(context.me, UI.ns.space('preferencesFile')); function complain(message) { message = 'logInLoadPreferences: ' + message; if (statusArea) { // statusArea.innerHTML = '' statusArea.appendChild(UI.widgets.errorMessageBlock(context.dom, message)); } console.log(message); reject(new Error(message)); } /** Are we working cross-origin? * * @returns {Boolean} True if we are in a webapp at an origin, and the file origin is different */ function differentOrigin() { return window.location && window.location.origin + '/' !== preferencesFile.site().uri; } if (!preferencesFile) { var message = "Can't find a preferences file pointer in profile " + context.publicProfile; return reject(new Error(message)); } // //// Load preferences file return kb.fetcher.load(preferencesFile, { withCredentials: true }).then(function () { if (progressDisplay) { progressDisplay.parentNode.removeChild(progressDisplay); } context.preferencesFile = preferencesFile; return resolve(context); })["catch"](function (err) { // Really important to look at why var status = err.status; var message = err.message; console.log('HTTP status ' + status + ' for pref file ' + preferencesFile); var m2; if (status === 401) { m2 = 'Strange - you are not authenticated (properly logged on) to read preferences file.'; alert(m2); } else if (status === 403) { if (differentOrigin()) { m2 = 'Unauthorized: Assuming prefs file blocked for origin ' + window.location.origin; context.preferencesFileError = m2; return resolve(context); } m2 = 'You are not authorized to read your preferences file. This may be because you are using an untrusted web app.'; console.warn(m2); } else if (status === 404) { if (confirm('You do not currently have a Preferences file. Ok for me to create an empty one? ' + preferencesFile)) { // @@@ code me ... weird to have a name o fthe file but no file alert('Sorry; I am not prepared to do this. Please create an empty file at ' + preferencesFile); return complain(new Error('Sorry; no code yet to create a preferences file at ')); } else { reject(new Error('User declined to create a preferences file at ' + preferencesFile)); } } else { m2 = 'Strange: Error ' + status + ' trying to read your preferences file.' + message; alert(m2); } }); // load prefs file then })["catch"](function (err) { // Fail initial login load prefs reject(new Error('(via loadPrefs) ' + err)); }); }); } /** * Resolves with the same context, outputting * output: index.public, index.private * * @see https://github.com/solid/solid/blob/master/proposals/data-discovery.md#discoverability * * @param context * @param context.div - place to put UI * * @returns {Promise<context>} */ function loadTypeIndexes(_x) { return _loadTypeIndexes.apply(this, arguments); } function _loadTypeIndexes() { _loadTypeIndexes = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee(context) { return _regenerator["default"].wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return loadPublicTypeIndex(context); case 2: _context.next = 4; return loadPrivateTypeIndex(context); case 4: case "end": return _context.stop(); } } }, _callee); })); return _loadTypeIndexes.apply(this, arguments); } function loadPublicTypeIndex(_x2) { return _loadPublicTypeIndex.apply(this, arguments); } function _loadPublicTypeIndex() { _loadPublicTypeIndex = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee2(context) { return _regenerator["default"].wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: return _context2.abrupt("return", loadIndex(context, ns.solid('publicTypeIndex'), true)); case 1: case "end": return _context2.stop(); } } }, _callee2); })); return _loadPublicTypeIndex.apply(this, arguments); } function loadPrivateTypeIndex(_x3) { return _loadPrivateTypeIndex.apply(this, arguments); } function _loadPrivateTypeIndex() { _loadPrivateTypeIndex = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee3(context) { return _regenerator["default"].wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: return _context3.abrupt("return", loadIndex(context, ns.solid('privateTypeIndex'), false)); case 1: case "end": return _context3.stop(); } } }, _callee3); })); return _loadPrivateTypeIndex.apply(this, arguments); } function loadOneTypeIndex(_x4, _x5) { return _loadOneTypeIndex.apply(this, arguments); } function _loadOneTypeIndex() { _loadOneTypeIndex = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee4(context, isPublic) { var predicate; return _regenerator["default"].wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: predicate = isPublic ? ns.solid('publicTypeIndex') : ns.solid('privateTypeIndex'); return _context4.abrupt("return", loadIndex(context, predicate, isPublic)); case 2: case "end": return _context4.stop(); } } }, _callee4); })); return _loadOneTypeIndex.apply(this, arguments); } function loadIndex(_x6, _x7, _x8) { return _loadIndex.apply(this, arguments); } /** * Resolves with the same context, outputting * @see https://github.com/solid/solid/blob/master/proposals/data-discovery.md#discoverability * * @private * * @param context {Object} * @param context.me * @param context.preferencesFile * @param context.preferencesFileError - Set if preferences file is blocked at theis origin so don't use it * @param context.publicProfile * @param context.index * * @returns {Promise} */ function _loadIndex() { _loadIndex = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee5(context, predicate, isPublic) { var ns, kb, me, ixs; return _regenerator["default"].wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: ns = UI.ns; kb = UI.store; // Loading preferences is more than loading profile _context5.prev = 2; _context5.next = 5; return isPublic; case 5: if (!_context5.sent) { _context5.next = 9; break; } logInLoadProfile(context); _context5.next = 10; break; case 9: logInLoadPreferences(context); case 10: _context5.next = 15; break; case 12: _context5.prev = 12; _context5.t0 = _context5["catch"](2); UI.widgets.complain(context, 'loadPubicIndex: login and load problem ' + _context5.t0); case 15: me = context.me; context.index = context.index || {}; if (!isPublic) { _context5.next = 22; break; } ixs = kb.each(me, predicate, undefined, context.publicProfile); context.index["public"] = ixs; _context5.next = 31; break; case 22: if (context.preferencesFileError) { _context5.next = 30; break; } ixs = kb.each(me, ns.solid('privateTypeIndex'), undefined, context.preferencesFile); context.index["private"] = ixs; if (!(ixs.length === 0)) { _context5.next = 28; break; } UI.widgets.complain('Your preference file ' + context.preferencesFile + ' does not point to a private type index.'); return _context5.abrupt("return", context); case 28: _context5.next = 31; break; case 30: console.log('We know your preferences file is noty available, so not bothering with private type indexes.'); case 31: _context5.prev = 31; _context5.next = 34; return kb.fetcher.load(ixs); case 34: _context5.next = 39; break; case 36: _context5.prev = 36; _context5.t1 = _context5["catch"](31); UI.widgets.complain(context, 'loadPubicIndex: loading public type index ' + _context5.t1); case 39: return _context5.abrupt("return", context); case 40: case "end": return _context5.stop(); } } }, _callee5, null, [[2, 12], [31, 36]]); })); return _loadIndex.apply(this, arguments); } function ensureTypeIndexes(_x9) { return _ensureTypeIndexes.apply(this, arguments); } /* Load or create ONE type index * Find one or mke one or fail * Many reasons for filing including script not having permission etc * */ /** * Adds it output to the context * @see https://github.com/solid/solid/blob/master/proposals/data-discovery.md#discoverability * * @private * * @param context {Object} * @param context.me * @param context.preferencesFile * @param context.preferencesFileError - Set if preferences file is blocked at theis origin so don't use it * @param context.publicProfile * @param context.index * * @returns {Promise} */ function _ensureTypeIndexes() { _ensureTypeIndexes = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee6(context) { return _regenerator["default"].wrap(function _callee6$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: _context6.next = 2; return ensureOneTypeIndex(context, true); case 2: _context6.next = 4; return ensureOneTypeIndex(context, false); case 4: case "end": return _context6.stop(); } } }, _callee6); })); return _ensureTypeIndexes.apply(this, arguments); } function ensureOneTypeIndex(_x10, _x11) { return _ensureOneTypeIndex.apply(this, arguments); } /** * Returns promise of context with arrays of symbols * * 2016-12-11 change to include forClass arc a la * https://github.com/solid/solid/blob/master/proposals/data-discovery.md * * @param context.div - inuput - Place to put UI for login * @param context.instances - output - array of instances * @param context.containers - output - array of containers to look in * @param klass * @returns {Promise} of context */ function _ensureOneTypeIndex() { _ensureOneTypeIndex = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee9(context, isPublic) { var makeIndexIfNecesary, _makeIndexIfNecesary; return _regenerator["default"].wrap(function _callee9$(_context9) { while (1) { switch (_context9.prev = _context9.next) { case 0: _makeIndexIfNecesary = function _ref4() { _makeIndexIfNecesary = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee8(context, isPublic) { var relevant, visibility, putIndex, _putIndex, newIndex, addMe, msg, ixs; return _regenerator["default"].wrap(function _callee8$(_context8) { while (1) { switch (_context8.prev = _context8.next) { case 0: _putIndex = function _ref2() { _putIndex = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee7(newIndex) { var _msg; return _regenerator["default"].wrap(function _callee7$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: _context7.prev = 0; _context7.next = 3; return kb.fetcher.webOperation('PUT', newIndex.uri, { data: '# ' + new Date() + ' Blank initial Type index\n', contentType: 'text/turtle' }); case 3: return _context7.abrupt("return", context); case 6: _context7.prev = 6; _context7.t0 = _context7["catch"](0); _msg = 'Error creating new index ' + _context7.t0; widgets.complain(context, _msg); case 10: case "end": return _context7.stop(); } } }, _callee7, null, [[0, 6]]); })); return _putIndex.apply(this, arguments); }; putIndex = function _ref(_x22) { return _putIndex.apply(this, arguments); }; relevant = isPublic ? context.publicProfile : context.preferencesFile; visibility = isPublic ? 'public' : 'private'; // putIndex context.index = context.index || {}; context.index[visibility] = context.index[visibility] || []; if (!(context.index[visibility].length === 0)) { _context8.next = 29; break; } newIndex = $rdf.sym(relevant.dir().uri + visibility + 'TypeIndex.ttl'); console.log('Linking to new fresh type index ' + newIndex); if (confirm('Ok to create a new empty index file at ' + newIndex + ', overwriting anything that was there?')) { _context8.next = 11; break; } throw new Error('cancelled by user'); case 11: console.log('Linking to new fresh type index ' + newIndex); addMe = [$rdf.st(context.me, ns.solid(visibility + 'TypeIndex'), newIndex, relevant)]; _context8.prev = 13; _context8.next = 16; return updatePromise([], addMe); case 16: _context8.next = 23; break; case 18: _context8.prev = 18; _context8.t0 = _context8["catch"](13); msg = 'Error saving type index link saving back ' + newIndex + ': ' + _context8.t0; UI.widgets.complain(context, msg); return _context8.abrupt("return", context); case 23: console.log('Creating new fresh type index file' + newIndex); _context8.next = 26; return putIndex(newIndex); case 26: context.index[visibility].push(newIndex); // @@ wait _context8.next = 38; break; case 29: // officially exists ixs = context.index[visibility]; _context8.prev = 30; _context8.next = 33; return kb.fetcher.load(ixs); case 33: _context8.next = 38; break; case 35: _context8.prev = 35; _context8.t1 = _context8["catch"](30); UI.widgets.complain(context, 'ensureOneTypeIndex: loading indexes ' + _context8.t1); case 38: case "end": return _context8.stop(); } } }, _callee8, null, [[13, 18], [30, 35]]); })); return _makeIndexIfNecesary.apply(this, arguments); }; makeIndexIfNecesary = function _ref3(_x20, _x21) { return _makeIndexIfNecesary.apply(this, arguments); }; _context9.prev = 2; _context9.next = 5; return loadOneTypeIndex(context, isPublic); case 5: console.log('ensureOneTypeIndex: Type index exists already ' + isPublic ? context.index["public"][0] : context.index["private"][0]); return _context9.abrupt("return", context); case 9: _context9.prev = 9; _context9.t0 = _context9["catch"](2); _context9.next = 13; return makeIndexIfNecesary(context, isPublic); case 13: case "end": return _context9.stop(); } } }, _callee9, null, [[2, 9]]); })); return _ensureOneTypeIndex.apply(this, arguments); } function findAppInstances(_x12, _x13, _x14) { return _findAppInstances.apply(this, arguments); } // @@@@ use teh one in rdflib.js when it is avaiable and delete this function _findAppInstances() { _findAppInstances = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee10(context, klass, isPublic) { var kb, ns, fetcher, visibility, thisIndex, registrations, instances, containers, e, i, cont; return _regenerator["default"].wrap(function _callee10$(_context10) { while (1) { switch (_context10.prev = _context10.next) { case 0: kb = UI.store; ns = UI.ns; fetcher = UI.store.fetcher; if (!(isPublic === undefined)) { _context10.next = 9; break; } _context10.next = 6; return findAppInstances(context, klass, true); case 6: _context10.next = 8; return findAppInstances(context, klass, false); case 8: return _context10.abrupt("return", context); case 9: visibility = isPublic ? 'public' : 'private'; _context10.prev = 10; _context10.next = 13; return loadOneTypeIndex(context, isPublic); case 13: _context10.next = 17; break; case 15: _context10.prev = 15; _context10.t0 = _context10["catch"](10); case 17: thisIndex = context.index[visibility]; registrations = thisIndex.map(function (ix) { return kb.each(undefined, ns.solid('forClass'), klass, ix); }).flat(); instances = registrations.map(function (reg) { return kb.each(reg, ns.solid('instance')); }).flat(); containers = registrations.map(function (reg) { return kb.each(reg, ns.solid('instanceContainer')); }).flat(); context.instances = context.instances || []; context.instances = context.instances.concat(instances); context.containers = context.containers || []; context.containers = context.containers.concat(containers); if (containers.length) { _context10.next = 27; break; } return _context10.abrupt("return", context); case 27: _context10.prev = 27; _context10.next = 30; return fetcher.load(containers); case 30: _context10.next = 37; break; case 32: _context10.prev = 32; _context10.t1 = _context10["catch"](27); e = new Error('[FAI] Unable to load containers' + _context10.t1); console.log(e); // complain UI.widgets.complain(context, "Error looking for ".concat(UI.utils.label(klass), ": ").concat(_context10.t1)); // but then ignoire it // throw new Error(e) case 37: for (i = 0; i < containers.length; i++) { cont = containers[i]; context.instances = context.instances.concat(kb.each(cont, ns.ldp('contains'))); } return _context10.abrupt("return", context); case 39: case "end": return _context10.stop(); } } }, _callee10, null, [[10, 15], [27, 32]]); })); return _findAppInstances.apply(this, arguments); } function updatePromise(updater, del, ins) { return new Promise(function (resolve, reject) { updater.update(del, ins, function (uri, ok, errorBody) { if (!ok) { reject(new Error(errorBody)); } else { resolve(); } }); // callback }); // promise } /* Register a new app in a type index */ function registerInTypeIndex(_x15, _x16, _x17, _x18) { return _registerInTypeIndex.apply(this, arguments); } /** * UI to control registration of instance * * @param context * @param instance * @param klass * * @returns {Promise} */ function _registerInTypeIndex() { _registerInTypeIndex = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee11(context, instance, klass, isPublic) { var kb, ns, indexes, index, registration, ins; return _regenerator["default"].wrap(function _callee11$(_context11) { while (1) { switch (_context11.prev = _context11.next) { case 0: kb = UI.store; ns = UI.ns; _context11.next = 4; return ensureOneTypeIndex(context, isPublic); case 4: indexes = isPublic ? context.index["public"] : context.index["private"]; if (indexes.length) { _context11.next = 7; break; } throw new Error('registerInTypeIndex: What no type index?'); case 7: index = indexes[0]; registration = UI.widgets.newThing(index); ins = [// See https://github.com/solid/solid/blob/master/proposals/data-discovery.md $rdf.st(registration, ns.rdf('type'), ns.solid('TypeRegistration'), index), $rdf.st(registration, ns.solid('forClass'), klass, index), $rdf.st(registration, ns.solid('instance'), instance, index)]; _context11.prev = 10; _context11.next = 13; return updatePromise(kb.updater, [], ins); case 13: _context11.next = 19; break; case 15: _context11.prev = 15; _context11.t0 = _context11["catch"](10); console.log(_context11.t0); alert(_context11.t0); case 19: return _context11.abrupt("return", context); case 20: case "end": return _context11.stop(); } } }, _callee11, null, [[10, 15]]); })); return _registerInTypeIndex.apply(this, arguments); } function registrationControl(context, instance, klass) { var kb = UI.store; var ns = UI.ns; var dom = context.dom; var box = dom.createElement('div'); context.div.appendChild(box); return ensureTypeIndexes(context).then(function () { box.innerHTML = '<table><tbody><tr></tr><tr></tr></tbody></table>'; // tbody will be inserted anyway box.setAttribute('style', 'font-size: 120%; text-align: right; padding: 1em; border: solid gray 0.05em;'); var tbody = box.children[0].children[0]; var form = kb.bnode(); // @@ say for now var registrationStatements = function registrationStatements(index) { var registrations = kb.each(undefined, ns.solid('instance'), instance).filter(function (r) { return kb.holds(r, ns.solid('forClass'), klass); }); var reg = registrations.length ? registrations[0] : widgets.newThing(index); return [$rdf.st(reg, ns.solid('instance'), instance, index), $rdf.st(reg, ns.solid('forClass'), klass, index)]; }; var index, statements; if (context.index["public"] && context.index["public"].length > 0) { index = context.index["public"][0]; statements = registrationStatements(index); tbody.children[0].appendChild(widgets.buildCheckboxForm(context.dom, UI.store, 'Public link to this ' + context.noun, null, statements, form, index)); } if (context.index["private"] && context.index["private"].length > 0) { index = context.index["private"][0]; statements = registrationStatements(index); tbody.children[1].appendChild(widgets.buildCheckboxForm(context.dom, UI.store, 'Personal note of this ' + context.noun, null, statements, form, index)); } return context; }, function (e) { var msg; if (context.preferencesFileError) { msg = '(Preferences not available)'; context.div.appendChild(dom.createElement('p')).textContent = msg; } else { msg = 'registrationControl: Type indexes not available: ' + e; context.div.appendChild(UI.widgets.errorMessageBlock(context.dom, e)); } console.log(msg); })["catch"](function (e) { var msg = 'registrationControl: Error making panel:' + e; context.div.appendChild(UI.widgets.errorMessageBlock(context.dom, e)); console.log(msg); }); } /** * UI to List at all registered things * @param context * @param options * * @returns {Promise} */ function registrationList(context, options) { var kb = UI.store; var ns = UI.ns; var dom = context.dom; var box = dom.createElement('div'); context.div.appendChild(box); return ensureTypeIndexes(context).then(function (indexes) { box.innerHTML = '<table><tbody></tbody></table>'; // tbody will be inserted anyway box.setAttribute('style', 'font-size: 120%; text-align: right; padding: 1em; border: solid #eee 0.5em;'); var table = box.firstChild; var ix = []; var sts = []; var vs = ['private', 'public']; vs.forEach(function (visibility) { if (options[visibility]) { ix = ix.concat(context.index[visibility][0]); sts = sts.concat(kb.statementsMatching(undefined, ns.solid('instance'), undefined, context.index[visibility][0])); } }); for (var i = 0; i < sts.length; i++) { var statement = sts[i]; // var cla = statement.subject var inst = statement.object; // if (false) { // var tr = table.appendChild(dom.createElement('tr')) // var anchor = tr.appendChild(dom.createElement('a')) // anchor.setAttribute('href', inst.uri) // anchor.textContent = utils.label(inst) // } else { // } var deleteInstance = function deleteInstance(x) { kb.updater.update([statement], [], function (uri, ok, errorBody) { if (ok) { console.log('Removed from index: ' + statement.subject); } else { console.log('Error: Cannot delete ' + statement + ': ' + errorBody); } }); }; var opts = { deleteFunction: deleteInstance }; var tr = widgets.personTR(dom, ns.solid('instance'), inst, opts); table.appendChild(tr); } /* //var containers = kb.each(klass, ns.solid('instanceContainer')); if (containers.length) { fetcher.load(containers).then(function(xhrs){ for (var i=0; i<containers.length; i++) { var cont = containers[i]; instances = instances.concat(kb.each(cont, ns.ldp('contains'))); } }); } */ return context; }); } /** * Simple Access Control * * This function sets up a simple default ACL for a resource, with * RWC for the owner, and a specified access (default none) for the public. * In all cases owner has read write control. * Parameter lists modes allowed to public * * @param docURI * @param me {NamedNode} WebID of user * @param options * @param options.public {Array<string>} eg ['Read', 'Write'] * * @returns {Promise<NamedNode>} Resolves with aclDoc uri on successful write */ function setACLUserPublic(docURI, me, options) { var kb = UI.store; var aclDoc = kb.any(kb.sym(docURI), kb.sym('http://www.iana.org/assignments/link-relations/acl')); return Promise.resolve().then(function () { if (aclDoc) { return aclDoc; } return fetchACLRel(docURI)["catch"](function (err) { throw new Error("Error fetching rel=ACL header for ".concat(docURI, ": ").concat(err)); }); }).then(function (aclDoc) { var aclText = genACLText(docURI, me, aclDoc.uri, options); return kb.fetcher.webOperation('PUT', aclDoc.uri, { data: aclText, contentType: 'text/turtle' }).then(function (result) { if (!result.ok) { throw new Error('Error writing ACL text: ' + result.error); } return aclDoc; }); }); } /** * @param docURI {string} * @returns {Promise<NamedNode|null>} */ function fetchACLRel(docURI) { var kb = UI.store; var fetcher = kb.fetcher; return fetcher.load(docURI).then(function (result) { if (!result.ok) { throw new Error('fetchACLRel: While loading:' + result.error); } var aclDoc = kb.any(kb.sym(docURI), kb.sym('http://www.iana.org/assignments/link-relations/acl')); if (!aclDoc) { throw new Error('fetchACLRel: No Link rel=ACL header for ' + docURI); } return aclDoc; }); } /** * @param docURI {string} * @param me {NamedNode} * @param aclURI {string} * @param options {Object} * * @returns {string} Serialized ACL */ function genACLText(docURI, me, aclURI) { var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; var optPublic = options["public"] || []; var g = $rdf.graph(); var auth = $rdf.Namespace('http://www.w3.org/ns/auth/acl#'); var a = g.sym(aclURI + '#a1'); var acl = g.sym(aclURI); var doc = g.sym(docURI); g.add(a, UI.ns.rdf('type'), auth('Authorization'), acl); g.add(a, auth('accessTo'), doc, acl); if (options.defaultForNew) { // TODO: Should this be auth('default') instead? g.add(a, auth('defaultForNew'), doc, acl); } g.add(a, auth('agent'), me, acl); g.add(a, auth('mode'), auth('Read'), acl); g.add(a, auth('mode'), auth('Write'), acl); g.add(a, auth('mode'), auth('Control'), acl); if (optPublic.length) { a = g.sym(aclURI + '#a2'); g.add(a, UI.ns.rdf('type'), auth('Authorization'), acl); g.add(a, auth('accessTo'), doc, acl); g.add(a, auth('agentClass'), UI.ns.foaf('Agent'), acl); for (var p = 0; p < optPublic.length; p++) { g.add(a, auth('mode'), auth(optPublic[p]), acl); // Like 'Read' etc } } return $rdf.serialize(acl, g, aclURI, 'text/turtle'); } /** * @returns {NamedNode|null} */ function offlineTestID() { if (typeof $SolidTestEnvironment !== 'undefined' && $SolidTestEnvironment.username) { // Test setup console.log('Assuming the user is ' + $SolidTestEnvironment.username); return $rdf.sym($SolidTestEnvironment.username); } if (typeof document !== 'undefined' && document.location && ('' + document.location).slice(0, 16) === 'http://localhost') { var div = document.getElementById('appTarget'); if (!div) return null; var id = div.getAttribute('testID'); if (!id) return null; /* me = kb.any(subject, UI.ns.acl('owner')); // when testing on plane with no webid */ console.log('Assuming user is ' + id); return $rdf.sym(id); } return null; } /** * Bootstrapping identity * (Called by `loginStatusBox()`) * @private * * @param dom * @param setUserCallback(user: object) * * @returns {Element} */ function getDefaultSignInButtonStyle() { return 'padding: 1em; border-radius:0.5em; margin: 2em; font-size: 100%;'; } function signInOrSignUpBox(dom, setUserCallback, options) { options = options || {}; var signInButtonStyle = options.buttonStyle || getDefaultSignInButtonStyle(); var box = dom.createElement('div'); var magicClassName = 'SolidSignInOrSignUpBox'; console.log('widgets.signInOrSignUpBox'); box.setUserCallback = setUserCallback; box.setAttribute('class', magicClassName); box.style = 'display:flex;'; // Sign in button with PopUP var signInPopUpButton = dom.createElement('input'); // multi box.appendChild(signInPopUpButton); signInPopUpButton.setAttribute('type', 'button'); signInPopUpButton.setAttribute('value', 'Log in'); signInPopUpButton.setAttribute('style', signInButtonStyle + 'background-color: #eef;'); signInPopUpButton.addEventListener('click', function () { var offline = offlineTestID(); if (offline) return setUserCallback(offline.uri); return solidAuthClient.popupLogin().then(function (session) { var webIdURI = session.webId; // setUserCallback(webIdURI) var divs = dom.getElementsByClassName(magicClassName); console.log('Logged in, ' + divs.length + ' panels to be serviced'); // At the same time, satiffy all the other login boxes for (var i = 0; i < divs.length; i++) { var div = divs[i]; if (div.setUserCallback) { try { div.setUserCallback(webIdURI); var parent = div.parentNode; if (parent) { parent.removeChild(div); } } catch (e) { console.log('## Error satisfying login box: ' + e); div.appendChild(UI.widgets.errorMessageBlock(dom, e)); } } } }); }, false); // Sign up button var signupButton = dom.createElement('input'); box.appendChild(signupButton); signupButton.setAttribute('type', 'button'); signupButton.setAttribute('value', 'Sign Up for Solid'); signupButton.setAttribute('style', signInButtonStyle + 'background-color: #efe;'); signupButton.addEventListener('click', function (e) { var signupMgr = new SolidTls.Signup(); signupMgr.signup().then(function (uri) { console.log('signInOrSignUpBox signed up ' + uri); setUserCallback(uri); }); }, false); return box; } /** * @returns {Promise<string|null>} Resolves with WebID URI or null */ function webIdFromSession(session) { var webId = session ? session.webId : null; if (webId) { saveUser(webId); } return webId; } /** * @returns {Promise<string|null>} Resolves with WebID URI or null */ /* function checkCurrentUser () { return checkUser() } */ /** * @param [setUserCallback] {Function} Optional callback, `setUserCallback(webId|null)` * * @returns {Promise<string|null>} Resolves with web id uri, if no callback provided */ function checkUser(setUserCallback) { // Check to see if already logged in / have the WebID var me = defaultTestUser(); if (me) { return Promise.resolve(setUserCallback ? setUserCallback(me) : me); } // doc = kb.any(doc, UI.ns.link('userMirror')) || doc return solidAuthClient.currentSession().then(webIdFromSession, function (err) { console.log('Error fetching currentSession:', err); return null; }).then(function (webId) { // if (webId.startsWith('dns:')) { // legacy rww.io pseudo-users // webId = null // } var me = saveUser(webId); if (me) { console.log('(Logged in as ' + me + ' by authentication)'); } return setUserCallback ? setUserCallback(me) : me; }); } /** * Login status box * * A big sign-up/sign in box or a logout box depending on the state * * @param dom * @param listener(uri) * * @returns {Element} */ function loginStatusBox(dom, listener, options) { // 20190630 var me = defaultTestUser(); var box = dom.createElement('div'); function setIt(newidURI) { if (!newidURI) { return; } var uri = newidURI.uri || newidURI; // UI.preferences.set('me', uri) me = $rdf.sym(uri); box.refresh(); if (listener) listener(me.uri); } function logoutButtonHandler(event) { // UI.preferences.set('me', '') solidAuthClient.logout().then(function () { var message = 'Your Web ID was ' + me + '. It has been forgotten.'; me = null; try { UI.log.alert(message); } catch (e) { window.alert(message); } box.refresh(); if (listener) listener(null); }, function (err) { alert('Fail to log out:' + err); }); } var logoutButton = function logoutButton(me, options) { options = options || {}; var signInButtonStyle = options.buttonStyle || getDefaultSignInButtonStyle(); var logoutLabel = 'Web ID logout'; if (me) { var nick = UI.store.any(me, UI.ns.foaf('nick')) || UI.store.any(me, UI.ns.foaf('name')); if (nick) { logoutLabel = 'Logout ' + nick.value; } } var signOutButton = dom.createElement('input'); // signOutButton.className = 'WebIDCancelButton' signOutButton.setAttribute('type', 'button'); signOutButton.setAttribute('value', logoutLabel); signOutButton.setAttribute('style', signInButtonStyle + 'background-color: #eee;'); signOutButton.addEventListener('click', logoutButtonHandler, false); return signOutButton; }; box.refresh = function () { solidAuthClient.currentSession().then(function (session) { if (session && session.webId) { me = $rdf.sym(session.webId); } else { me = null; } if (me && box.me !== me.uri || !me && box.me) { widgets.clearElement(box); if (me) { box.appendChild(logoutButton(me, options)); } else { box.appendChild(signInOrSignUpBox(dom, setIt, options)); } } box.me = me ? me.uri : null; }, function (err) { alert('loginStatusBox: ' + err); }); }; if (solidAuthClient.trackSession) { solidAuthClient.trackSession(function (session) { if (session && session.webId) { me = $rdf.sym(session.webId); } else { me = null; } box.refresh(); }); } box.me = '99999'; // Force refresh box.refresh(); return box; } /** * Workspace selection etc */ /** * Returns a UI object which, if it selects a workspace, * will callback(workspace, newBase). * * If necessary, will get an account, preferences file, etc. In sequence: * * - If not logged in, log in. * - Load preferences file * - Prompt user for workspaces * - Allows the user to just type in a URI by hand * * Calls back with the ws and the base URI * * @param dom * @param appDetails * @param callbackWS * @returns {Element} */ function selectWorkspace(dom, appDetails, callbackWS) { var noun = appDetails.noun; var appPathSegment = appDetails.appPathSegment; var me = defaultTestUser(); var kb = UI.store; var box = dom.createElement('div'); var context = { me: me, dom: dom, div: box }; var say = function say(s) { box.appendChild(UI.widgets.errorMessageBlock(dom, s)); }; var figureOutBase = function figureOutBase(ws) { var newBase = kb.any(ws, UI.ns.space('uriPrefix')); if (!newBase) { newBase = ws.uri.split('#')[0]; } else { newBase = newBase.value; } if (newBase.slice(-1) !== '/') { console.log(appPathSegment + ': No / at end of uriPrefix ' + newBase); // @@ paramater? newBase = newBase + '/'; } var now = new Date(); newBase += appPathSegment + '/id' + now.getTime() + '/'; // unique id return newBase; }; var displayOptions = function displayOptions(context) { // var status = '' var id = context.me; var preferencesFile = context.preferencesFile; var newBase = null; // A workspace specifically defined in the private preferences file: var w = kb.statementsMatching(id, UI.ns.space('workspace'), // Only trust prefs file here undefined, preferencesFile).map(function (st) { return st.object; }); // A workspace in a storage in the public profile: var storages = kb.each(id, UI.ns.space('storage')); // @@ No provenance requirement at the moment storages.map(function (s) { w = w.concat(kb.each(s, UI.ns.ldp('contains'))); }); if (w.length === 1) { say('Workspace used: ' + w[0].uri); // @@ allow user to see URI newBase = figureOutBase(w[0]); // callbackWS(w[0], newBase) } else if (w.length === 0) { say("You don't seem to have any workspaces. You have " + storages.length + ' storages.'); } // Prompt for ws selection or creation // say( w.length + " workspaces for " + id + "Chose one."); var table = dom.createElement('table'); table.setAttribute('style', 'border-collapse:separate; border-spacing: 0.5em;'); // var popup = window.open(undefined, '_blank', { height: 300, width:400 }, false) box.appendChild(table); // Add a field for directly adding the URI yourself // var hr = box.appendChild(dom.createElement('hr')) // @@ box.appendChild(dom.createElement('hr')); // @@ var p = box.appendChild(dom.createElement('p')); p.textContent = 'Where would you like to store the data for the ' + noun + '? ' + 'Give the URL of the directory where you would like the data stored.'; var baseField = box.appendChild(dom.createElement('input')); baseField.setAttribute('type', 'text'); baseField.size = 80; // really a string baseField.label = 'base URL'; baseField.autocomplete = 'on'; if (newBase) { // set to default baseField.value = newBase; } context.baseField = baseField; box.appendChild(dom.createElement('br')); // @@ var button = box.appendChild(dom.createElement('button')); button.textContent = 'Start new ' +