protected static function calculate_permission_tree(array $pathroleperms) {
        // Considering each discovered context path as an inflection point, evaluate the user's
        // permission (based on all roles) at each point.
        $pathallows = [];
        $mindepth = 1000;
        $maxdepth = 0;
        foreach ($pathroleperms as $path => $roles) {
            $evaluatedroleperms = [];

            // Walk up the tree starting from this path.
            $innerpath = $path;
            while ($innerpath !== '') {
                $roles = $pathroleperms[$innerpath];

                // Evaluate roles at this path level.
                foreach ($roles as $roleid => $perm) {
                    if (!array_key_exists($roleid, $evaluatedroleperms)) {
                        $evaluatedroleperms[$roleid] = $perm;
                    } else {
                        // The existing one is at a more specific level so it takes precedence
                        // UNLESS this is a prohibit.
                        if ($perm == CAP_PROHIBIT) {
                            $evaluatedroleperms[$roleid] = $perm;
                        }
                    }
                }

                // Go up to next path level (if any).
                do {
                    $innerpath = substr($innerpath, 0, strrpos($innerpath, '/'));
                    if ($innerpath === '') {
                        // No higher level data.
                        break;
                    }
                } while (!array_key_exists($innerpath, $pathroleperms));
            }

            // If we have an allow from any role, and no prohibits, then user can access this path,
            // else not.
            $allow = false;
            foreach ($evaluatedroleperms as $perm) {
                if ($perm == CAP_ALLOW) {
                    $allow = true;
                } else if ($perm == CAP_PROHIBIT) {
                    $allow = false;
                    break;
                }
            }

            // Store the result based on path and depth so that we can process in depth order in
            // the next step.
            $depth = strlen(preg_replace('~[^/]~', '', $path));
            $mindepth = min($depth, $mindepth);
            $maxdepth = max($depth, $maxdepth);
            $pathallows[$depth][$path] = $allow;
        }

        // Organise into a tree structure, processing in depth order so that we have ancestors
        // set up before we encounter their children.
        $root = (object)['allow' => false, 'path' => null, 'children' => []];
        $nodesbypath = [];
        for ($depth = $mindepth; $depth <= $maxdepth; $depth++) {
            // Skip any missing depth levels.
            if (!array_key_exists($depth, $pathallows)) {
                continue;
            }
            foreach ($pathallows[$depth] as $path => $allow) {
                // Value for new tree node.
                $leaf = (object)['allow' => $allow, 'path' => $path, 'children' => []];

                // Try to find a place to join it on if there is one.
                $ancestorpath = $path;
                $found = false;
                while ($ancestorpath) {
                    $ancestorpath = substr($ancestorpath, 0, strrpos($ancestorpath, '/'));
                    if (array_key_exists($ancestorpath, $nodesbypath)) {
                        $found = true;
                        break;
                    }
                }

                if ($found) {
                    $nodesbypath[$ancestorpath]->children[] = $leaf;
                } else {
                    $root->children[] = $leaf;
                }
                $nodesbypath[$path] = $leaf;
            }
        }

        return $root;
    }