"use strict";
/**
 * Non-UI functions for access control.
 * See https://github.com/solidos/web-access-control-spec
 * for the spec that defines how ACL documents work.
 * @packageDocumentation
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.adoptACLDefault = adoptACLDefault;
exports.readACL = readACL;
exports.sameACL = sameACL;
exports.ACLunion = ACLunion;
exports.loadUnionACL = loadUnionACL;
exports.ACLbyCombination = ACLbyCombination;
exports.makeACLGraph = makeACLGraph;
exports.makeACLGraphbyCombo = makeACLGraphbyCombo;
exports.ACLToString = ACLToString;
exports.comboToString = comboToString;
exports.makeACLString = makeACLString;
exports.putACLObject = putACLObject;
exports.putACLbyCombo = putACLbyCombo;
exports.fixIndividualCardACL = fixIndividualCardACL;
exports.fixIndividualACL = fixIndividualACL;
exports.setACL = setACL;
exports.getACLorDefault = getACLorDefault;
exports.getACL = getACL;
exports.getProspectiveHolder = getProspectiveHolder;
const ns_1 = __importDefault(require("../ns"));
const solid_logic_1 = require("solid-logic");
const utils = __importStar(require("../utils"));
const debug = __importStar(require("../debug"));
const rdflib_1 = require("rdflib");
const kb = solid_logic_1.solidLogicSingleton.store;
/**
 * Take the "default" ACL and convert it into the equivalent ACL
 * which the resource would have had. Return it as a new separate store.
 * The "defaultForNew" predicate is also accepted, as a deprecated
 * synonym for "default".
 */
function adoptACLDefault(doc, aclDoc, defaultResource, defaultACLDoc) {
    const ACL = ns_1.default.acl;
    const isContainer = doc.uri.slice(-1) === '/'; // Give default for all directories
    const defaults = kb
        .each(undefined, ACL('default'), defaultResource, defaultACLDoc)
        .concat(kb.each(undefined, ACL('defaultForNew'), defaultResource, defaultACLDoc));
    const proposed = defaults.reduce((accumulatedStatements, da) => accumulatedStatements
        .concat(kb.statementsMatching(da, ns_1.default.rdf('type'), ACL('Authorization'), defaultACLDoc))
        .concat(kb.statementsMatching(da, ACL('agent'), undefined, defaultACLDoc))
        .concat(kb.statementsMatching(da, ACL('agentClass'), undefined, defaultACLDoc))
        .concat(kb.statementsMatching(da, ACL('agentGroup'), undefined, defaultACLDoc))
        .concat(kb.statementsMatching(da, ACL('origin'), undefined, defaultACLDoc))
        .concat(kb.statementsMatching(da, ACL('originClass'), undefined, defaultACLDoc))
        .concat(kb.statementsMatching(da, ACL('mode'), undefined, defaultACLDoc))
        .concat((0, rdflib_1.st)(da, ACL('accessTo'), doc, defaultACLDoc))
        .concat(isContainer ? (0, rdflib_1.st)(da, ACL('default'), doc, defaultACLDoc) : []), []);
    const kb2 = (0, rdflib_1.graph)(); // Potential - derived is kept apart
    proposed.forEach(st => kb2.add(move(st.subject), move(st.predicate), move(st.object), (0, rdflib_1.sym)(aclDoc.uri)));
    return kb2;
    function move(symbol) {
        const y = defaultACLDoc.uri.length; // The default ACL file
        return (0, rdflib_1.sym)(symbol.uri.slice(0, y) === defaultACLDoc.uri
            ? aclDoc.uri + symbol.uri.slice(y)
            : symbol.uri);
    }
}
/**
 * Read and canonicalize the ACL for x in aclDoc
 *
 * Accumulate the access rights which each agent or class has
 */
function readACL(doc, aclDoc, kb2 = kb, getDefaults = false) {
    const auths = getDefaults
        ? getDefaultsFallback(kb2, ns_1.default)
        : kb2.each(undefined, ns_1.default.acl('accessTo'), doc);
    const ACL = ns_1.default.acl;
    const ac = {
        agent: {},
        agentClass: {},
        agentGroup: {},
        origin: {},
        originClass: {}
    };
    Object.keys(ac).forEach(pred => {
        auths.forEach(function (a) {
            kb2.each(a, ACL('mode')).forEach(function (mode) {
                kb2.each(a, ACL(pred)).forEach(function (agent) {
                    ac[pred][agent.uri] = ac[pred][agent.uri] || {};
                    ac[pred][agent.uri][mode.uri] = a; // could be "true" but leave pointer just in case
                });
            });
        });
    });
    return ac;
    function getDefaultsFallback(kb, ns) {
        return kb
            .each(undefined, ns.acl('default'), doc)
            .concat(kb.each(undefined, ns.acl('defaultForNew'), doc));
    }
}
/**
 * Compare two ACLs
 */
function sameACL(a, b) {
    const contains = function (a, b) {
        for (const pred in {
            agent: true,
            agentClass: true,
            agentGroup: true,
            origin: true,
            originClass: true
        }) {
            if (a[pred]) {
                for (const agent in a[pred]) {
                    for (const mode in a[pred][agent]) {
                        if (!b[pred][agent] || !b[pred][agent][mode]) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    };
    return contains(a, b) && contains(b, a);
}
/**
 * Union N ACLs
 */
function ACLunion(list) {
    const b = list[0];
    let a, ag;
    for (let k = 1; k < list.length; k++) {
        ;
        ['agent', 'agentClass', 'agentGroup', 'origin', 'originClass'].forEach(function (pred) {
            a = list[k];
            if (a[pred]) {
                for (ag in a[pred]) {
                    for (const mode in a[pred][ag]) {
                        if (!b[pred][ag])
                            b[pred][ag] = [];
                        b[pred][ag][mode] = true;
                    }
                }
            }
        });
    }
    return b;
}
/**
 * Merge ACLs lists from things to form union
 */
function loadUnionACL(subjectList, callbackFunction) {
    const aclList = [];
    const doList = function (list) {
        if (list.length) {
            const doc = list.shift().doc();
            getACLorDefault(doc, function (ok, p2, targetDoc, targetACLDoc, defaultHolder, defaultACLDoc) {
                const defa = !p2;
                if (!ok || !defaultHolder || !defaultACLDoc)
                    return callbackFunction(ok, targetACLDoc);
                const acl = defa
                    ? readACL(defaultHolder, defaultACLDoc)
                    : readACL(targetDoc, targetACLDoc);
                aclList.push(acl);
                doList(list.slice(1));
            });
        }
        else {
            // all gone
            callbackFunction(true, ACLunion(aclList));
        }
    };
    doList(subjectList);
}
/**
 * Represents these as an RDF graph by combination of modes
 *
 * Each agent can only be in one place in this model, one combination of modes.
 * Combos are like full control, read append, read only etc.
 */
function ACLbyCombination(ac) {
    const byCombo = {};
    ['agent', 'agentClass', 'agentGroup', 'origin', 'originClass'].forEach(function (pred) {
        for (const agent in ac[pred]) {
            const combo = [];
            for (const mode in ac[pred][agent]) {
                combo.push(mode);
            }
            combo.sort();
            const combo2 = combo.join('\n');
            if (!byCombo[combo2])
                byCombo[combo2] = [];
            byCombo[combo2].push([pred, agent]);
        }
    });
    return byCombo;
}
/**
 * Write ACL graph to store from AC
 */
function makeACLGraph(kb, x, ac, aclDoc) {
    const byCombo = ACLbyCombination(ac);
    return makeACLGraphbyCombo(kb, x, byCombo, aclDoc);
}
/**
 * Write ACL graph to store from combo
 */
function makeACLGraphbyCombo(kb, x, byCombo, aclDoc, main, defa) {
    const ACL = ns_1.default.acl;
    for (const combo in byCombo) {
        const pairs = byCombo[combo];
        if (!pairs.length)
            continue; // do not add to store when no agent
        const modeURIs = combo.split('\n');
        let short = modeURIs
            .map(function (u) {
            return u.split('#')[1];
        })
            .join('');
        if (defa && !main)
            short += 'Default'; // don't muddle authorizations
        const a = kb.sym(aclDoc.uri + '#' + short);
        kb.add(a, ns_1.default.rdf('type'), ACL('Authorization'), aclDoc);
        if (main) {
            kb.add(a, ACL('accessTo'), x, aclDoc);
        }
        if (defa) {
            kb.add(a, ACL('default'), x, aclDoc);
        }
        for (let i = 0; i < modeURIs.length; i++) {
            kb.add(a, ACL('mode'), kb.sym(modeURIs[i]), aclDoc);
        }
        for (let i = 0; i < pairs.length; i++) {
            const pred = pairs[i][0];
            const ag = pairs[i][1];
            kb.add(a, ACL(pred), kb.sym(ag), aclDoc);
        }
    }
}
/**
 * Debugging short strings for dumping ACL
 * and possibly in the UI
 */
function ACLToString(ac) {
    return comboToString(ACLbyCombination(ac));
}
/**
 * Convert a [[ComboList]] to a string
 */
function comboToString(byCombo) {
    let str = '';
    for (const combo in byCombo) {
        const modeURIs = combo.split('\n');
        const initials = modeURIs
            .map(function (u) {
            return u.split('#')[1][0];
        })
            .join('');
        str += initials + ':';
        const pairs = byCombo[combo];
        for (let i = 0; i < pairs.length; i++) {
            const pred = pairs[i][0];
            const ag = (0, rdflib_1.sym)(pairs[i][1]);
            str += pred === 'agent' ? '@' : '';
            str += ag.sameTerm(ns_1.default.foaf('Agent')) ? '*' : utils.label(ag);
            if (i < pairs.length - 1)
                str += ',';
        }
        str += ';';
    }
    return '{' + str.slice(0, -1) + '}'; // drop extra semicolon
}
/**
 * Write ACL graph as Turtle
 */
function makeACLString(x, ac, aclDoc) {
    const kb2 = (0, rdflib_1.graph)();
    makeACLGraph(kb2, x, ac, aclDoc);
    return (0, rdflib_1.serialize)(aclDoc, kb2, aclDoc.uri, 'text/turtle') || '';
}
/**
 * Write ACL graph to web
 */
function putACLObject(kb, x, ac, aclDoc, callbackFunction) {
    const byCombo = ACLbyCombination(ac);
    return putACLbyCombo(kb, x, byCombo, aclDoc, callbackFunction);
}
/**
 * Write ACL graph to web from a [[ComboList]]
 */
function putACLbyCombo(kb, x, byCombo, aclDoc, callbackFunction) {
    const kb2 = (0, rdflib_1.graph)();
    makeACLGraphbyCombo(kb2, x, byCombo, aclDoc, true);
    // const str = makeACLString = function(x, ac, aclDoc)
    kb.updater?.put(aclDoc, kb2.statementsMatching(undefined, undefined, undefined, aclDoc), 'text/turtle', function (uri, ok, message) {
        if (!ok) {
            callbackFunction(ok, message);
        }
        else {
            kb.fetcher?.unload(aclDoc);
            makeACLGraphbyCombo(kb, x, byCombo, aclDoc, true);
            kb.fetcher.requested[aclDoc.uri] = 'done'; // missing: save headers
            callbackFunction(ok);
        }
    });
}
/**
 * Fix the ACl for an individual card as a function of the groups it is in
 *
 * All group files must be loaded first
 */
function fixIndividualCardACL(person, log, callbackFunction) {
    const groups = kb.each(undefined, ns_1.default.vcard('hasMember'), person);
    // const doc = person.doc()
    if (groups) {
        fixIndividualACL(person, groups, log, callbackFunction);
    }
    else {
        log('This card is in no groups');
        callbackFunction(true); // fine, no requirements to access. default should be ok
    }
    // @@ if no groups, then use default for People container or the book top container.?
}
/**
 * This function is used by [[fixIndividualCardACL]]
 */
function fixIndividualACL(item, subjects, log, callbackFunction) {
    log = log || debug.log;
    const doc = item.doc();
    getACLorDefault(doc, function (ok, exists, targetDoc, targetACLDoc, defaultHolder, defaultACLDoc) {
        if (!ok || !defaultHolder || !defaultACLDoc)
            return callbackFunction(false, targetACLDoc); // ie message
        const ac = exists
            ? readACL(targetDoc, targetACLDoc)
            : readACL(defaultHolder, defaultACLDoc);
        loadUnionACL(subjects, function (ok, union) {
            if (!ok)
                return callbackFunction(false, union);
            if (sameACL(union, ac)) {
                log('Nice - same ACL. no change ' + utils.label(item) + ' ' + doc);
            }
            else {
                log('Group ACLs differ for ' + utils.label(item) + ' ' + doc);
                // log("Group ACLs: " + makeACLString(targetDoc, union, targetACLDoc))
                // log((exists ? "Previous set" : "Default") + " ACLs: " +
                // makeACLString(targetDoc, ac, targetACLDoc))
                putACLObject(kb, targetDoc, union, targetACLDoc, callbackFunction);
            }
        });
    });
}
/**
 * Set an ACL
 */
function setACL(docURI, aclText, callbackFunction) {
    const aclDoc = kb.any(docURI, solid_logic_1.ACL_LINK); // @@ check that this get set by web.js
    if (!kb.fetcher) {
        throw new Error('Store has no fetcher');
    }
    if (aclDoc) {
        // Great we already know where it is
        kb.fetcher
            .webOperation('PUT', aclDoc.value, {
            data: aclText,
            contentType: 'text/turtle'
        })
            .then((res) => {
            callbackFunction(res.ok, res.error || '');
        }); // @@@ check params
    }
    else {
        kb.fetcher.nowOrWhenFetched(docURI, undefined, function (ok, body) {
            if (!ok)
                return callbackFunction(ok, 'Gettting headers for ACL: ' + body);
            const aclDoc = kb.any(docURI, solid_logic_1.ACL_LINK); // @@ check that this get set by web.js
            if (!aclDoc) {
                // complainIfBad(false, "No Link rel=ACL header for " + docURI)
                callbackFunction(false, 'No Link rel=ACL header for ' + docURI);
            }
            else {
                if (!kb.fetcher) {
                    throw new Error('Store has no fetcher');
                }
                kb.fetcher
                    .webOperation('PUT', aclDoc.value, {
                    data: aclText,
                    contentType: 'text/turtle'
                })
                    .then((res) => {
                    callbackFunction(res.ok, res.error || '');
                });
            }
        });
    }
}
/**
 * Get ACL file or default if necessary
 *
 * @param callbackFunction  Will be called in the following ways, in the following cases:
 * * `callbackFunction(true, true, doc, aclDoc)` if the ACL did exist
 * * `callbackFunction(true, false, doc, aclDoc, defaultHolder, defaultACLDoc)` if the ACL file did not exist but a default did
 * * `callbackFunction(false, false, status, message)` when there was an error getting the original
 * * `callbackFunction(false, true, status, message)` when there was an error getting the default
 */
function getACLorDefault(doc, callbackFunction) {
    getACL(doc, function (ok, status, aclDoc, message) {
        const ACL = ns_1.default.acl;
        if (!ok)
            return callbackFunction(false, false, status, message);
        // Recursively search for the ACL file which gives default access
        const tryParent = function (uri) {
            if (uri.slice(-1) === '/') {
                uri = uri.slice(0, -1);
            }
            const right = uri.lastIndexOf('/');
            const left = uri.indexOf('/', uri.indexOf('//') + 2);
            if (left > right) {
                return callbackFunction(false, true, 404, 'Found no ACL resource');
            }
            uri = uri.slice(0, right + 1);
            const doc2 = (0, rdflib_1.sym)(uri);
            getACL(doc2, function (ok, status, defaultACLDoc) {
                if (!ok) {
                    return callbackFunction(false, true, status, `( No ACL pointer ${uri} ${status})${defaultACLDoc}`);
                }
                else if (status === 403) {
                    return callbackFunction(false, true, status, `( default ACL file FORBIDDEN. Stop.${uri})`);
                }
                else if (status === 404) {
                    return tryParent(uri);
                }
                else if (status !== 200) {
                    return callbackFunction(false, true, status, `Error status '${status}' searching for default for ${doc2}`);
                }
                // 200
                // statusBlock.textContent += (" ACCESS set at " + uri + ". End search.")
                const defaults = kb
                    .each(undefined, ACL('default'), kb.sym(uri), defaultACLDoc)
                    .concat(kb.each(undefined, ACL('defaultForNew'), kb.sym(uri), defaultACLDoc));
                if (!defaults.length) {
                    return tryParent(uri); // Keep searching
                }
                const defaultHolder = kb.sym(uri);
                return callbackFunction(true, false, doc, aclDoc, defaultHolder, defaultACLDoc);
            });
        }; // tryParent
        if (!ok) {
            return callbackFunction(false, false, status, `Error accessing Access Control information for ${doc}) ${message}`);
        }
        else if (status === 404) {
            tryParent(doc.uri); //  @@ construct default one - the server should do that
        }
        else if (status === 403) {
            return callbackFunction(false, false, status, `(Sharing not available to you)${message}`);
        }
        else if (status !== 200) {
            return callbackFunction(false, false, status, `Error ${status} accessing Access Control information for ${doc}: ${message}`);
        }
        else {
            // 200
            return callbackFunction(true, true, doc, aclDoc);
        }
    }); // Call to getACL
}
/**
 * Calls back `(ok, status, acldoc, message)` as follows
 *
 * * `(false, 900, errormessage)` if no link header
 * * `(true, 403, documentSymbol, fileaccesserror)` if not authorized
 * * `(true, 404, documentSymbol, fileaccesserror)` if does not exist
 * * `(true, 200, documentSymbol)` if file exists and read OK
 */
function getACL(doc, callbackFunction) {
    if (!kb.fetcher) {
        throw new Error('kb has no fetcher');
    }
    kb.fetcher.nowOrWhenFetched(doc, undefined, function (ok, body) {
        if (!ok) {
            return callbackFunction(ok, `Can't get headers to find ACL for ${doc}: ${body}`);
        }
        const aclDoc = kb.any(doc, solid_logic_1.ACL_LINK); // @@ check that this get set by web.js
        if (!aclDoc) {
            callbackFunction(false, 900, `No Link rel=ACL header for ${doc}`);
        }
        else {
            if (!kb.fetcher) {
                throw new Error('kb has no fetcher');
            }
            if (kb.fetcher.nonexistent[aclDoc.value]) {
                return callbackFunction(true, 404, aclDoc, `ACL file ${aclDoc} does not exist.`);
            }
            kb.fetcher.nowOrWhenFetched(aclDoc, undefined, function (ok, message, response) {
                if (!ok) {
                    callbackFunction(true, response.status, aclDoc, `Can't read Access Control File ${aclDoc}: ${message}`);
                }
                else {
                    callbackFunction(true, 200, aclDoc);
                }
            });
        }
    });
}
/**
 * Calls [[getACLorDefault]] and then (?)
 */
async function getProspectiveHolder(targetDirectory) {
    return new Promise((resolve, reject) => getACLorDefault((0, rdflib_1.sym)(targetDirectory), (ok, isDirectACL, targetDoc, targetACLDoc, defaultHolder) => {
        if (ok) {
            return resolve((isDirectACL ? targetDoc : defaultHolder));
        }
        return reject(new Error(`Error loading ${targetDirectory}`));
    }));
}
//# sourceMappingURL=acl.js.map