From de820dc0d19c7179d59a14ed8d253f9aa03fbbc8 Mon Sep 17 00:00:00 2001
From: yflory <yann.flory@xwiki.com>
Date: Mon, 17 Feb 2020 14:07:29 +0100
Subject: [PATCH] Split properties modal into 2 modals

---
 www/common/common-ui-elements.js | 646 ++----------------------------
 www/common/drive-ui.js           |  97 +++--
 www/common/inner/access.js       | 658 +++++++++++++++++++++++++++++++
 3 files changed, 740 insertions(+), 661 deletions(-)
 create mode 100644 www/common/inner/access.js

diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index 8aba07c18..67c4518fa 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -100,588 +100,8 @@ define([
         };
     };
 
-    var createOwnerModal = function (common, data) {
-        var friends = common.getFriends(true);
-        var sframeChan = common.getSframeChannel();
-        var priv = common.getMetadataMgr().getPrivateData();
-        var user = common.getMetadataMgr().getUserData();
-        var edPublic = priv.edPublic;
-        var channel = data.channel;
-        var owners = data.owners || [];
-        var pending_owners = data.pending_owners || [];
-        var teams = priv.teams;
-        var teamOwner = data.teamId;
-
-        var redrawAll = function () {};
-
-        var div1 = h('div.cp-usergrid-user.cp-share-column.cp-ownership');
-        var div2 = h('div.cp-usergrid-user.cp-share-column.cp-ownership');
-        var $div1 = $(div1);
-        var $div2 = $(div2);
-
-        // Remove owner column
-        var drawRemove = function (pending) {
-            var _owners = {};
-            var o = (pending ? pending_owners : owners) || [];
-            o.forEach(function (ed) {
-                var f;
-                Object.keys(friends).some(function (c) {
-                    if (friends[c].edPublic === ed) {
-                        f = friends[c];
-                        return true;
-                    }
-                });
-                Object.keys(teams).some(function (id) {
-                    if (teams[id].edPublic === ed) {
-                        f = teams[id];
-                        f.teamId = id;
-                    }
-                });
-                if (ed === edPublic) {
-                    f = f || user;
-                    if (f.name) { f.edPublic = edPublic; }
-                }
-                _owners[ed] = f || {
-                    displayName: Messages._getKey('owner_unknownUser', [ed]),
-                    edPublic: ed,
-                };
-            });
-            var msg = pending ? Messages.owner_removePendingText
-                        : Messages.owner_removeText;
-            var removeCol = UIElements.getUserGrid(msg, {
-                common: common,
-                large: true,
-                data: _owners,
-                noFilter: true
-            }, function () {
-            });
-            var $div = $(removeCol.div);
-            // When clicking on the remove button, we check the selected users.
-            // If you try to remove yourself, we'll display an additional warning message
-            var btnMsg = pending ? Messages.owner_removePendingButton : Messages.owner_removeButton;
-            var removeButton = h('button.no-margin', btnMsg);
-            $(removeButton).click(function () {
-                // Check selection
-                var $sel = $div.find('.cp-usergrid-user.cp-selected');
-                var sel = $sel.toArray();
-                if (!sel.length) { return; }
-                var me = false;
-                var toRemove = sel.map(function (el) {
-                    var ed = $(el).attr('data-ed');
-                    if (!ed) { return; }
-                    if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; }
-                    if (ed === edPublic && !teamOwner) { me = true; }
-                    return ed;
-                }).filter(function (x) { return x; });
-                NThen(function (waitFor) {
-                    var msg = me ? Messages.owner_removeMeConfirm : Messages.owner_removeConfirm;
-                    UI.confirm(msg, waitFor(function (yes) {
-                        if (!yes) {
-                            waitFor.abort();
-                            return;
-                        }
-                    }));
-                }).nThen(function (waitFor) {
-                    // Send the command
-                    sframeChan.query('Q_SET_PAD_METADATA', {
-                        channel: channel,
-                        command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS',
-                        value: toRemove,
-                        teamId: teamOwner
-                    }, waitFor(function (err, res) {
-                        err = err || (res && res.error);
-                        if (err) {
-                            waitFor.abort();
-                            redrawAll();
-                            var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden
-                                                                          : Messages.error;
-                            return void UI.warn(text);
-                        }
-                        UI.log(Messages.saved);
-                    }));
-                }).nThen(function (waitFor) {
-                    sel.forEach(function (el) {
-                        var curve = $(el).attr('data-curve');
-                        var friend = curve === user.curvePublic ? user : friends[curve];
-                        if (!friend) { return; }
-                        common.mailbox.sendTo("RM_OWNER", {
-                            channel: channel,
-                            title: data.title,
-                            pending: pending
-                        }, {
-                            channel: friend.notifications,
-                            curvePublic: friend.curvePublic
-                        }, waitFor());
-                    });
-                }).nThen(function () {
-                    redrawAll();
-                });
-            });
-            $div.append(h('p', removeButton));
-            return $div;
-        };
-
-        // Add owners column
-        var drawAdd = function () {
-            var $div = $(h('div.cp-share-column'));
-            var _friends = JSON.parse(JSON.stringify(friends));
-            Object.keys(_friends).forEach(function (curve) {
-                if (owners.indexOf(_friends[curve].edPublic) !== -1 ||
-                    pending_owners.indexOf(_friends[curve].edPublic) !== -1 ||
-                    !_friends[curve].notifications) {
-                    delete _friends[curve];
-                }
-            });
-            var addCol = UIElements.getUserGrid(Messages.owner_addText, {
-                common: common,
-                large: true,
-                data: _friends
-            }, function () {
-                //console.log(arguments);
-            });
-            $div.append(addCol.div);
-
-            var teamsData = Util.tryParse(JSON.stringify(priv.teams)) || {};
-            Object.keys(teamsData).forEach(function (id) {
-                var t = teamsData[id];
-                t.teamId = id;
-                if (owners.indexOf(t.edPublic) !== -1 || pending_owners.indexOf(t.edPublic) !== -1) {
-                    delete teamsData[id];
-                }
-            });
-            var teamsList = UIElements.getUserGrid(Messages.owner_addTeamText, {
-                common: common,
-                large: true,
-                noFilter: true,
-                data: teamsData
-            }, function () {});
-            $div.append(teamsList.div);
-
-            // When clicking on the add button, we get the selected users.
-            var addButton = h('button.no-margin', Messages.owner_addButton);
-            $(addButton).click(function () {
-                // Check selection
-                var $sel = $div.find('.cp-usergrid-user.cp-selected');
-                var sel = $sel.toArray();
-                if (!sel.length) { return; }
-                var toAdd = sel.map(function (el) {
-                    var curve = $(el).attr('data-curve');
-                    // If the pad is woned by a team, we can transfer ownership to ourselves
-                    if (curve === user.curvePublic && teamOwner) { return priv.edPublic; }
-                    var friend = friends[curve];
-                    if (!friend) { return; }
-                    return friend.edPublic;
-                }).filter(function (x) { return x; });
-                var toAddTeams = sel.map(function (el) {
-                    var team = teamsData[$(el).attr('data-teamid')];
-                    if (!team || !team.edPublic) { return; }
-                    return {
-                        edPublic: team.edPublic,
-                        id: $(el).attr('data-teamid')
-                    };
-                }).filter(function (x) { return x; });
-
-                NThen(function (waitFor) {
-                    var msg = Messages.owner_addConfirm;
-                    UI.confirm(msg, waitFor(function (yes) {
-                        if (!yes) {
-                            waitFor.abort();
-                            return;
-                        }
-                    }));
-                }).nThen(function (waitFor) {
-                    // Add one of our teams as an owner
-                    if (toAddTeams.length) {
-                        // Send the command
-                        sframeChan.query('Q_SET_PAD_METADATA', {
-                            channel: channel,
-                            command: 'ADD_OWNERS',
-                            value: toAddTeams.map(function (obj) { return obj.edPublic; }),
-                            teamId: teamOwner
-                        }, waitFor(function (err, res) {
-                            err = err || (res && res.error);
-                            if (err) {
-                                waitFor.abort();
-                                redrawAll();
-                                var text = err === "INSUFFICIENT_PERMISSIONS" ?
-                                        Messages.fm_forbidden : Messages.error;
-                                return void UI.warn(text);
-                            }
-                            var isTemplate = priv.isTemplate || data.isTemplate;
-                            toAddTeams.forEach(function (obj) {
-                                sframeChan.query('Q_STORE_IN_TEAM', {
-                                    href: data.href || data.rohref,
-                                    password: data.password,
-                                    path: isTemplate ? ['template'] : undefined,
-                                    title: data.title || '',
-                                    teamId: obj.id
-                                }, waitFor(function (err) {
-                                    if (err) { return void console.error(err); }
-                                }));
-                            });
-                        }));
-                    }
-                }).nThen(function (waitFor) {
-                    // Offer ownership to a friend
-                    if (toAdd.length) {
-                        // Send the command
-                        sframeChan.query('Q_SET_PAD_METADATA', {
-                            channel: channel,
-                            command: 'ADD_PENDING_OWNERS',
-                            value: toAdd,
-                            teamId: teamOwner
-                        }, waitFor(function (err, res) {
-                            err = err || (res && res.error);
-                            if (err) {
-                                waitFor.abort();
-                                redrawAll();
-                                var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden
-                                                                              : Messages.error;
-                                return void UI.warn(text);
-                            }
-                        }));
-                    }
-                }).nThen(function (waitFor) {
-                    sel.forEach(function (el) {
-                        var curve = $(el).attr('data-curve');
-                        var friend = curve === user.curvePublic ? user : friends[curve];
-                        if (!friend) { return; }
-                        common.mailbox.sendTo("ADD_OWNER", {
-                            channel: channel,
-                            href: data.href,
-                            password: data.password,
-                            title: data.title
-                        }, {
-                            channel: friend.notifications,
-                            curvePublic: friend.curvePublic
-                        }, waitFor());
-                    });
-                }).nThen(function () {
-                    redrawAll();
-                    UI.log(Messages.saved);
-                });
-            });
-            $div.append(h('p', addButton));
-            return $div;
-        };
-
-        redrawAll = function (md) {
-            var todo = function (obj) {
-                if (obj && obj.error) { return; }
-                owners = obj.owners || [];
-                pending_owners = obj.pending_owners || [];
-                $div1.empty();
-                $div2.empty();
-                $div1.append(drawRemove(false)).append(drawRemove(true));
-                $div2.append(drawAdd());
-            };
-
-            if (md) { return void todo(md); }
-            common.getPadMetadata({
-                channel: data.channel
-            }, todo);
-        };
-
-        $div1.append(drawRemove(false)).append(drawRemove(true));
-        $div2.append(drawAdd());
-
-        var handler = sframeChan.on('EV_RT_METADATA', function (md) {
-            if (!$div1.length) {
-                return void handler.stop();
-            }
-            owners = md.owners || [];
-            pending_owners = md.pending_owners || [];
-            redrawAll(md);
-        });
-
-        // Create modal
-        var link = h('div.cp-share-columns', [
-            div1,
-            div2
-            /*drawRemove()[0],
-            drawAdd()[0]*/
-        ]);
-        var linkButtons = [{
-            className: 'cancel',
-            name: Messages.filePicker_close,
-            onClick: function () {},
-            keys: [27]
-        }];
-        return UI.dialog.customModal(link, {buttons: linkButtons});
-    };
-
-    var getRightsProperties = function (common, data, cb) {
-        var $div = $('<div>');
-        if (!data) { return void cb(void 0, $div); }
-
-        var draw = function () {
-            var $d = $('<div>');
-            var priv = common.getMetadataMgr().getPrivateData();
-            var user = common.getMetadataMgr().getUserData();
-            var edPublic = priv.edPublic;
-            var owned = false;
-            var _owners = {};
-            if (data.owners && data.owners.length) {
-                if (data.owners.indexOf(edPublic) !== -1) {
-                    owned = true;
-                } else {
-                    Object.keys(priv.teams || {}).some(function (id) {
-                        var team = priv.teams[id] || {};
-                        if (team.viewer) { return; }
-                        if (data.owners.indexOf(team.edPublic) === -1) { return; }
-                        owned = id;
-                        return true;
-                    });
-                }
-                var strangers = 0;
-                data.owners.forEach(function (ed) {
-                    // If a friend is an owner, add their name to the list
-                    // otherwise, increment the list of strangers
-
-                    // Our edPublic? print "Yourself"
-                    if (ed === edPublic) {
-                        _owners[ed] = {
-                            selected: true,
-                            name: user.name,
-                            avatar: user.avatar
-                        };
-                        return;
-                    }
-                    // One of our teams? print the team name
-                    if (Object.keys(priv.teams || {}).some(function (id) {
-                        var team = priv.teams[id] || {};
-                        if (team.edPublic !== ed) { return; }
-                        _owners[ed] = {
-                            name: team.name,
-                            avatar: team.avatar
-                        };
-                        return true;
-                    })) {
-                        return;
-                    }
-                    // One of our friends? print the friend name
-                    if (Object.keys(priv.friends || {}).some(function (c) {
-                        var friend = priv.friends[c] || {};
-                        if (friend.edPublic !== ed || c === 'me') { return; }
-                        _owners[friend.edPublic] = {
-                            name: friend.displayName,
-                            avatar: friend.avatar
-                        };
-                        return true;
-                    })) {
-                        return;
-                    }
-                    // Otherwise it's a stranger
-                    strangers++;
-                });
-                if (strangers) {
-                    _owners['stangers'] = {
-                        name: Messages._getKey('properties_unknownUser', [strangers]),
-                    };
-                }
-            }
-            var _ownersGrid = UIElements.getUserGrid(Messages.creation_owners, {
-                common: common,
-                noSelect: true,
-                data: _owners,
-                large: true
-            }, function () {});
-            if (_ownersGrid && Object.keys(_owners).length) {
-                $d.append(_ownersGrid.div);
-            } else {
-                $d.append([
-                    h('label', Messages.creation_owners),
-                ]);
-                $d.append(UI.dialog.selectable(Messages.creation_noOwner, {
-                    id: 'cp-app-prop-owners',
-                }));
-
-            }
-
-            var parsed;
-            if (data.href || data.roHref) {
-                parsed = Hash.parsePadUrl(data.href || data.roHref);
-            }
-            if (owned && parsed.hashData.type === 'pad') {
-                var manageOwners = h('button.no-margin', Messages.owner_openModalButton);
-                $(manageOwners).click(function () {
-                    data.teamId = typeof(owned) !== "boolean" ? owned : undefined;
-                    var modal = createOwnerModal(common, data);
-                    UI.openCustomModal(modal, {
-                        wide: true,
-                    });
-                });
-                $d.append(h('p', manageOwners));
-            }
-
-            if (!data.noExpiration) {
-                var expire = Messages.creation_expireFalse;
-                if (data.expire && typeof (data.expire) === "number") {
-                    expire = new Date(data.expire).toLocaleString();
-                }
-                $('<label>', {'for': 'cp-app-prop-expire'}).text(Messages.creation_expiration)
-                    .appendTo($d);
-                $d.append(UI.dialog.selectable(expire, {
-                    id: 'cp-app-prop-expire',
-                }));
-            }
-
-            if (!data.noPassword) {
-                var hasPassword = data.password;
-                var $pwLabel = $('<label>', {'for': 'cp-app-prop-password'}).text(Messages.creation_passwordValue)
-                                .hide().appendTo($d);
-                var password = UI.passwordInput({
-                    id: 'cp-app-prop-password',
-                    readonly: 'readonly'
-                });
-                var $password = $(password).hide();
-                var $pwInput = $password.find('.cp-password-input');
-                $pwInput.val(data.password).click(function () {
-                    $pwInput[0].select();
-                });
-                $d.append(password);
-
-                if (hasPassword) {
-                    $pwLabel.show();
-                    $password.css('display', 'flex');
-                }
-
-                // In the properties, we should have the edit href if we know it.
-                // We should know it because the pad is stored, but it's better to check...
-                if (!data.noEditPassword && owned && data.href) { // FIXME SHEET fix password change for sheets
-                    var sframeChan = common.getSframeChannel();
-
-                    var isOO = parsed.type === 'sheet';
-                    var isFile = parsed.hashData.type === 'file';
-                    var isSharedFolder = parsed.type === 'drive';
-
-                    var changePwTitle = Messages.properties_changePassword;
-                    var changePwConfirm = isFile ? Messages.properties_confirmChangeFile : Messages.properties_confirmChange;
-                    if (!hasPassword) {
-                        changePwTitle = Messages.properties_addPassword;
-                        changePwConfirm = isFile ? Messages.properties_confirmNewFile : Messages.properties_confirmNew;
-                    }
-                    $('<label>', {'for': 'cp-app-prop-change-password'})
-                        .text(changePwTitle).appendTo($d);
-                    var newPassword = UI.passwordInput({
-                        id: 'cp-app-prop-change-password',
-                        style: 'flex: 1;'
-                    });
-                    var passwordOk = h('button', Messages.properties_changePasswordButton);
-                    var changePass = h('span.cp-password-change-container', [
-                        newPassword,
-                        passwordOk
-                    ]);
-                    var pLocked = false;
-                    $(passwordOk).click(function () {
-                        var newPass = $(newPassword).find('input').val();
-                        if (data.password === newPass ||
-                            (!data.password && !newPass)) {
-                            return void UI.alert(Messages.properties_passwordSame);
-                        }
-                        if (pLocked) { return; }
-                        pLocked = true;
-                        UI.confirm(changePwConfirm, function (yes) {
-                            if (!yes) { pLocked = false; return; }
-                            $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'}));
-                            var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' :
-                                        (isOO ? 'Q_OO_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE');
-
-                            // If this is a file password change, register to the upload events:
-                            // * if there is a pending upload, ask if we shoudl interrupt
-                            // * display upload progress
-                            var onPending;
-                            var onProgress;
-                            if (isFile) {
-                                onPending = sframeChan.on('Q_BLOB_PASSWORD_CHANGE_PENDING', function (data, cb) {
-                                    onPending.stop();
-                                    UI.confirm(Messages.upload_uploadPending, function (yes) {
-                                        cb({cancel: yes});
-                                    });
-                                });
-                                onProgress = sframeChan.on('EV_BLOB_PASSWORD_CHANGE_PROGRESS', function (data) {
-                                    if (typeof (data) !== "number") { return; }
-                                    var p = Math.round(data);
-                                    $(passwordOk).text(p + '%');
-                                });
-                            }
-
-                            sframeChan.query(q, {
-                                teamId: typeof(owned) !== "boolean" ? owned : undefined,
-                                href: data.href,
-                                password: newPass
-                            }, function (err, data) {
-                                $(passwordOk).text(Messages.properties_changePasswordButton);
-                                pLocked = false;
-                                if (err || data.error) {
-                                    console.error(err || data.error);
-                                    return void UI.alert(Messages.properties_passwordError);
-                                }
-                                UI.findOKButton().click();
-
-                                $pwLabel.show();
-                                $password.css('display', 'flex');
-                                $pwInput.val(newPass);
-
-                                // If the current document is a file or if we're changing the password from a drive,
-                                // we don't have to reload the page at the end.
-                                // Tell the user the password change was successful and abort
-                                if (isFile || priv.app !== parsed.type) {
-                                    if (onProgress && onProgress.stop) { onProgress.stop(); }
-                                    $(passwordOk).text(Messages.properties_changePasswordButton);
-                                    var alertMsg = data.warning ? Messages.properties_passwordWarningFile
-                                                                : Messages.properties_passwordSuccessFile;
-                                    return void UI.alert(alertMsg, undefined, {force: true});
-                                }
-
-                                // Pad password changed: update the href
-                                // Use hidden hash if needed (we're an owner of this pad so we know it is stored)
-                                var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']);
-                                var href = (priv.readOnly && data.roHref) ? data.roHref : data.href;
-                                if (useUnsafe === false) {
-                                    var newParsed = Hash.parsePadUrl(href);
-                                    var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass);
-                                    var newHash = Hash.getHiddenHashFromKeys(parsed.type, newSecret, {});
-                                    href = Hash.hashToHref(newHash, parsed.type);
-                                }
-
-                                if (data.warning) {
-                                    return void UI.alert(Messages.properties_passwordWarning, function () {
-                                        common.gotoURL(href);
-                                    }, {force: true});
-                                }
-                                return void UI.alert(Messages.properties_passwordSuccess, function () {
-                                    if (!isSharedFolder) {
-                                        common.gotoURL(href);
-                                    }
-                                }, {force: true});
-                            });
-                        });
-                    });
-                    $d.append(changePass);
-                }
-            }
-            return $d;
-        };
-
-        var sframeChan = common.getSframeChannel();
-        var handler = sframeChan.on('EV_RT_METADATA', function (md) {
-            if (!$div.length) {
-                handler.stop();
-                return;
-            }
-            md = JSON.parse(JSON.stringify(md));
-            data.owners = md.owners;
-            data.expire = md.expire;
-            data.pending_owners = md.pending_owners;
-            $div.empty();
-            $div.append(draw());
-        });
-        $div.append(draw());
-
-        cb(void 0, $div);
-    };
-    var getPadProperties = function (common, data, cb) {
+    var getPadProperties = function (common, data, opts, cb) {
+        opts = opts || {};
         var $d = $('<div>');
         if (!data) { return void cb(void 0, $d); }
 
@@ -695,7 +115,7 @@ define([
             }));
         }
 
-        if (data.roHref) {
+        if (data.roHref && !opts.noReadOnly) {
             $('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d);
             $d.append(UI.dialog.selectable(data.roHref, {
                 id: 'cp-app-prop-rolink',
@@ -839,7 +259,8 @@ define([
 
     };
 
-    var getPropertiesData = function (common, cb) {
+    var getPropertiesData = function (common, opts, cb) {
+        opts = opts || {};
         var data = {};
         NThen(function (waitFor) {
             var base = common.getMetadataMgr().getPrivateData().origin;
@@ -855,46 +276,33 @@ define([
                 Util.extend(data, val);
                 if (data.href) { data.href = base + data.href; }
                 if (data.roHref) { data.roHref = base + data.roHref; }
-            }));
-            common.getPadMetadata(null, waitFor(function (obj) {
-                if (obj && obj.error) { return; }
-                data.owners = obj.owners;
-                data.expire = obj.expire;
-                data.pending_owners = obj.pending_owners;
-            }));
+            }), opts.href);
         }).nThen(function () {
             cb(void 0, data);
         });
     };
-    UIElements.getProperties = function (common, data, cb) {
-        var c1;
-        var c2;
-        var button = [{
-            className: 'primary',
-            name: Messages.okButton,
-            onClick: function () {},
-            keys: [13]
-        }];
+    UIElements.getProperties = function (common, opts, cb) {
+        var data;
+        var content;
         NThen(function (waitFor) {
-            getPadProperties(common, data, waitFor(function (e, c) {
-                c1 = UI.dialog.customModal(c[0], {
-                    buttons: button
-                });
+            getPropertiesData(common, opts, waitFor(function (e, _data) {
+                if (e) {
+                    waitFor.abort();
+                    return void cb(e);
+                }
+                data = _data;
             }));
-            getRightsProperties(common, data, waitFor(function (e, c) {
-                c2 = UI.dialog.customModal(c[0], {
-                    buttons: button
-                });
+        }).nThen(function (waitFor) {
+            getPadProperties(common, data, opts, waitFor(function (e, c) {
+                if (e) {
+                    waitFor.abort();
+                    return void cb(e);
+                }
+                content = c[0];
             }));
         }).nThen(function () {
-            var tabs = UI.dialog.tabs([{
-                title: Messages.fc_prop,
-                content: c1
-            }, {
-                title: Messages.creation_propertiesTitle,
-                content: c2
-            }]);
-            cb (void 0, $(tabs));
+            var modal = UI.alert(content);
+            cb (void 0, modal);
         });
     };
 
@@ -2382,12 +1790,8 @@ define([
                         if (!data) {
                             return void UI.alert(Messages.autostore_notAvailable);
                         }
-                        getPropertiesData(common, function (e, data) {
+                        UIElements.getProperties(common, {}, function (e) {
                             if (e) { return void console.error(e); }
-                            UIElements.getProperties(common, data, function (e, $prop) {
-                                if (e) { return void console.error(e); }
-                                UI.openCustomModal($prop[0]);
-                            });
                         });
                     });
                 });
diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js
index 37a1ad7e6..ce3299af5 100644
--- a/www/common/drive-ui.js
+++ b/www/common/drive-ui.js
@@ -8,6 +8,9 @@ define([
     '/common/common-interface.js',
     '/common/common-constants.js',
     '/common/common-feedback.js',
+
+    '/common/inner/access.js',
+
     '/bower_components/nthen/index.js',
     '/common/hyperscript.js',
     '/common/proxy-manager.js',
@@ -23,6 +26,7 @@ define([
     UI,
     Constants,
     Feedback,
+    Access,
     nThen,
     h,
     ProxyManager,
@@ -79,6 +83,7 @@ define([
     var faColor = 'cptools-palette';
     var faTrash = 'fa-trash';
     var faDelete = 'fa-eraser';
+    var faAccess = 'fa-unlock-alt';
     var faProperties = 'fa-info-circle';
     var faTags = 'fa-hashtag';
     var faUploadFiles = 'cptools-file-upload';
@@ -448,6 +453,10 @@ define([
                     'data-icon': faDelete,
                 }, Messages.fc_remove_sharedfolder)),
                 $separator.clone()[0],
+                h('li', h('a.cp-app-drive-context-access.dropdown-item', {
+                    'tabindex': '-1',
+                    'data-icon': faAccess,
+                }, "ACCESS")), // XXX
                 h('li', h('a.cp-app-drive-context-properties.dropdown-item', {
                     'tabindex': '-1',
                     'data-icon': faProperties,
@@ -1203,7 +1212,7 @@ define([
                         hide.push('savelocal');
                         hide.push('openro');
                         hide.push('openincode');
-                        hide.push('properties');
+                        hide.push('properties', 'access');
                         hide.push('hashtag');
                     }
                     // If we're in the trash, hide restore and properties for non-root elements
@@ -1233,7 +1242,7 @@ define([
                 });
                 if (paths.length > 1) {
                     hide.push('restore');
-                    hide.push('properties');
+                    hide.push('properties', 'access');
                     hide.push('rename');
                     hide.push('openparent');
                     hide.push('hashtag');
@@ -1259,10 +1268,10 @@ define([
                 case 'tree':
                     show = ['open', 'openro', 'openincode', 'expandall', 'collapseall',
                             'color', 'download', 'share', 'savelocal', 'rename', 'delete',
-                            'deleteowned', 'removesf', 'properties', 'hashtag'];
+                            'deleteowned', 'removesf', 'properties', 'access', 'hashtag'];
                     break;
                 case 'default':
-                    show = ['open', 'openro', 'share', 'openparent', 'delete', 'deleteowned', 'properties', 'hashtag'];
+                    show = ['open', 'openro', 'share', 'openparent', 'delete', 'deleteowned', 'properties', 'access', 'hashtag'];
                     break;
                 case 'trashtree': {
                     show = ['empty'];
@@ -3074,9 +3083,8 @@ define([
                             }).appendTo($openDir);
                         }
                         $('<a>').text(Messages.fc_prop).click(function () {
-                            APP.getProperties(r.id, function (e, $prop) {
+                            APP.getProperties(r.id, function (e) {
                                 if (e) { return void logError(e); }
-                                UI.alert($prop[0], undefined, true);
                             });
                         }).appendTo($openDir);
                     }
@@ -3824,12 +3832,11 @@ define([
             }
         };
 
-        var getProperties = APP.getProperties = function (el, cb) {
+        APP.getProperties = function (el, cb) {
             if (!manager.isFile(el) && !manager.isSharedFolder(el)) {
                 return void cb('NOT_FILE');
             }
             //var ro = manager.isReadOnlyFile(el);
-            var base = APP.origin;
             var data;
             if (manager.isSharedFolder(el)) {
                 data = JSON.parse(JSON.stringify(manager.getSharedFolderData(el)));
@@ -3838,42 +3845,42 @@ define([
             }
             if (!data || !(data.href || data.roHref)) { return void cb('INVALID_FILE'); }
 
-            if (data.href) {
-                data.href = base + data.href;
-            }
-            if (data.roHref) {
-                data.roHref = base + data.roHref;
-            }
-
-            if (currentPath[0] === TEMPLATE) {
-                data.isTemplate = true;
-            }
+            var opts = {};
+            opts.href = Hash.getRelativeHref(data.href || data.roHref);
 
             if (manager.isSharedFolder(el)) {
                 var ro = folders[el] && folders[el].version >= 2;
-                if (!ro) { delete data.roHref; }
-                //data.noPassword = true;
-                //data.noEditPassword = true;
-                data.noExpiration = true;
-                // this is here to allow users to check the channel id of a shared folder
-                // we should remove it at some point
-                data.sharedFolder = true;
+                if (!ro) { opts.noReadOnly = true; }
+            }
+            UIElements.getProperties(common, opts, cb);
+        };
+        APP.getAccess = function (el, cb) {
+            if (!manager.isFile(el) && !manager.isSharedFolder(el)) {
+                return void cb('NOT_FILE');
+            }
+            var data;
+            if (manager.isSharedFolder(el)) {
+                data = JSON.parse(JSON.stringify(manager.getSharedFolderData(el)));
+            } else {
+                data = JSON.parse(JSON.stringify(manager.getFileData(el)));
+            }
+            if (!data || !(data.href || data.roHref)) { return void cb('INVALID_FILE'); }
+
+            var opts = {};
+            opts.href = Hash.getRelativeHref(data.href || data.roHref);
+            opts.channel = data.channel;
+
+            // Transfer ownership: templates are stored as templates for other users/teams
+            if (currentPath[0] === TEMPLATE) {
+                opts.isTemplate = true;
             }
 
-            if ((manager.isFile(el) && data.roHref) || manager.isSharedFolder(el)) { // Only for pads!
-                sframeChan.query('Q_GET_PAD_METADATA', {
-                    channel: data.channel
-                }, function (err, val) {
-                    if (!err && !(val && val.error)) {
-                        data.owners = val.owners;
-                        data.expire = val.expire;
-                        data.pending_owners = val.pending_owners;
-                    }
-                    UIElements.getProperties(common, data, cb);
-                });
-                return;
+            // Shared folders: no expiration date
+            if (manager.isSharedFolder(el)) {
+                opts.noExpiration = true;
             }
-            UIElements.getProperties(common, data, cb);
+
+            Access.getAccessModal(common, opts, cb);
         };
 
         if (!APP.loggedIn) {
@@ -4218,9 +4225,19 @@ define([
                     // ANON_SHARED_FOLDER
                     el = manager.find(paths[0].path.slice(1), APP.newSharedFolder);
                 }
-                getProperties(el, function (e, $prop) {
+                APP.getProperties(el, function (e) {
+                    if (e) { return void logError(e); }
+                });
+            }
+            else if ($this.hasClass("cp-app-drive-context-access")) {
+                if (paths.length !== 1) { return; }
+                el = manager.find(paths[0].path);
+                if (paths[0].path[0] === SHARED_FOLDER && APP.newSharedFolder) {
+                    // ANON_SHARED_FOLDER
+                    el = manager.find(paths[0].path.slice(1), APP.newSharedFolder);
+                }
+                APP.getAccess(el, function (e) {
                     if (e) { return void logError(e); }
-                    UI.openCustomModal($prop[0]);
                 });
             }
             else if ($this.hasClass("cp-app-drive-context-hashtag")) {
diff --git a/www/common/inner/access.js b/www/common/inner/access.js
new file mode 100644
index 000000000..139a52d64
--- /dev/null
+++ b/www/common/inner/access.js
@@ -0,0 +1,658 @@
+define([
+    'jquery',
+    '/common/common-util.js',
+    '/common/common-hash.js',
+    '/common/common-interface.js',
+    '/common/common-ui-elements.js',
+    '/common/hyperscript.js',
+    '/customize/messages.js',
+    '/bower_components/nthen/index.js',
+], function ($, Util, Hash, UI, UIElements, h,
+             Messages, nThen) {
+console.log(UI, UIElements, h);
+
+    var Access = {};
+
+    var createOwnerModal = function (common, data) {
+        var friends = common.getFriends(true);
+        var sframeChan = common.getSframeChannel();
+        var priv = common.getMetadataMgr().getPrivateData();
+        var user = common.getMetadataMgr().getUserData();
+        var edPublic = priv.edPublic;
+        var channel = data.channel;
+        var owners = data.owners || [];
+        var pending_owners = data.pending_owners || [];
+        var teams = priv.teams;
+        var teamOwner = data.teamId;
+
+        var redrawAll = function () {};
+
+        var div1 = h('div.cp-usergrid-user.cp-share-column.cp-ownership');
+        var div2 = h('div.cp-usergrid-user.cp-share-column.cp-ownership');
+        var $div1 = $(div1);
+        var $div2 = $(div2);
+
+        // Remove owner column
+        var drawRemove = function (pending) {
+            var _owners = {};
+            var o = (pending ? pending_owners : owners) || [];
+            o.forEach(function (ed) {
+                var f;
+                Object.keys(friends).some(function (c) {
+                    if (friends[c].edPublic === ed) {
+                        f = friends[c];
+                        return true;
+                    }
+                });
+                Object.keys(teams).some(function (id) {
+                    if (teams[id].edPublic === ed) {
+                        f = teams[id];
+                        f.teamId = id;
+                    }
+                });
+                if (ed === edPublic) {
+                    f = f || user;
+                    if (f.name) { f.edPublic = edPublic; }
+                }
+                _owners[ed] = f || {
+                    displayName: Messages._getKey('owner_unknownUser', [ed]),
+                    edPublic: ed,
+                };
+            });
+            var msg = pending ? Messages.owner_removePendingText
+                        : Messages.owner_removeText;
+            var removeCol = UIElements.getUserGrid(msg, {
+                common: common,
+                large: true,
+                data: _owners,
+                noFilter: true
+            }, function () {
+            });
+            var $div = $(removeCol.div);
+            // When clicking on the remove button, we check the selected users.
+            // If you try to remove yourself, we'll display an additional warning message
+            var btnMsg = pending ? Messages.owner_removePendingButton : Messages.owner_removeButton;
+            var removeButton = h('button.no-margin', btnMsg);
+            $(removeButton).click(function () {
+                // Check selection
+                var $sel = $div.find('.cp-usergrid-user.cp-selected');
+                var sel = $sel.toArray();
+                if (!sel.length) { return; }
+                var me = false;
+                var toRemove = sel.map(function (el) {
+                    var ed = $(el).attr('data-ed');
+                    if (!ed) { return; }
+                    if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; }
+                    if (ed === edPublic && !teamOwner) { me = true; }
+                    return ed;
+                }).filter(function (x) { return x; });
+                nThen(function (waitFor) {
+                    var msg = me ? Messages.owner_removeMeConfirm : Messages.owner_removeConfirm;
+                    UI.confirm(msg, waitFor(function (yes) {
+                        if (!yes) {
+                            waitFor.abort();
+                            return;
+                        }
+                    }));
+                }).nThen(function (waitFor) {
+                    // Send the command
+                    sframeChan.query('Q_SET_PAD_METADATA', {
+                        channel: channel,
+                        command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS',
+                        value: toRemove,
+                        teamId: teamOwner
+                    }, waitFor(function (err, res) {
+                        err = err || (res && res.error);
+                        if (err) {
+                            waitFor.abort();
+                            redrawAll();
+                            var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden
+                                                                          : Messages.error;
+                            return void UI.warn(text);
+                        }
+                        UI.log(Messages.saved);
+                    }));
+                }).nThen(function (waitFor) {
+                    sel.forEach(function (el) {
+                        var curve = $(el).attr('data-curve');
+                        var friend = curve === user.curvePublic ? user : friends[curve];
+                        if (!friend) { return; }
+                        common.mailbox.sendTo("RM_OWNER", {
+                            channel: channel,
+                            title: data.title,
+                            pending: pending
+                        }, {
+                            channel: friend.notifications,
+                            curvePublic: friend.curvePublic
+                        }, waitFor());
+                    });
+                }).nThen(function () {
+                    redrawAll();
+                });
+            });
+            $div.append(h('p', removeButton));
+            return $div;
+        };
+
+        // Add owners column
+        var drawAdd = function () {
+            var $div = $(h('div.cp-share-column'));
+            var _friends = JSON.parse(JSON.stringify(friends));
+            Object.keys(_friends).forEach(function (curve) {
+                if (owners.indexOf(_friends[curve].edPublic) !== -1 ||
+                    pending_owners.indexOf(_friends[curve].edPublic) !== -1 ||
+                    !_friends[curve].notifications) {
+                    delete _friends[curve];
+                }
+            });
+            var addCol = UIElements.getUserGrid(Messages.owner_addText, {
+                common: common,
+                large: true,
+                data: _friends
+            }, function () {
+                //console.log(arguments);
+            });
+            $div.append(addCol.div);
+
+            var teamsData = Util.tryParse(JSON.stringify(priv.teams)) || {};
+            Object.keys(teamsData).forEach(function (id) {
+                var t = teamsData[id];
+                t.teamId = id;
+                if (owners.indexOf(t.edPublic) !== -1 || pending_owners.indexOf(t.edPublic) !== -1) {
+                    delete teamsData[id];
+                }
+            });
+            var teamsList = UIElements.getUserGrid(Messages.owner_addTeamText, {
+                common: common,
+                large: true,
+                noFilter: true,
+                data: teamsData
+            }, function () {});
+            $div.append(teamsList.div);
+
+            // When clicking on the add button, we get the selected users.
+            var addButton = h('button.no-margin', Messages.owner_addButton);
+            $(addButton).click(function () {
+                // Check selection
+                var $sel = $div.find('.cp-usergrid-user.cp-selected');
+                var sel = $sel.toArray();
+                if (!sel.length) { return; }
+                var toAdd = sel.map(function (el) {
+                    var curve = $(el).attr('data-curve');
+                    // If the pad is woned by a team, we can transfer ownership to ourselves
+                    if (curve === user.curvePublic && teamOwner) { return priv.edPublic; }
+                    var friend = friends[curve];
+                    if (!friend) { return; }
+                    return friend.edPublic;
+                }).filter(function (x) { return x; });
+                var toAddTeams = sel.map(function (el) {
+                    var team = teamsData[$(el).attr('data-teamid')];
+                    if (!team || !team.edPublic) { return; }
+                    return {
+                        edPublic: team.edPublic,
+                        id: $(el).attr('data-teamid')
+                    };
+                }).filter(function (x) { return x; });
+
+                nThen(function (waitFor) {
+                    var msg = Messages.owner_addConfirm;
+                    UI.confirm(msg, waitFor(function (yes) {
+                        if (!yes) {
+                            waitFor.abort();
+                            return;
+                        }
+                    }));
+                }).nThen(function (waitFor) {
+                    // Add one of our teams as an owner
+                    if (toAddTeams.length) {
+                        // Send the command
+                        sframeChan.query('Q_SET_PAD_METADATA', {
+                            channel: channel,
+                            command: 'ADD_OWNERS',
+                            value: toAddTeams.map(function (obj) { return obj.edPublic; }),
+                            teamId: teamOwner
+                        }, waitFor(function (err, res) {
+                            err = err || (res && res.error);
+                            if (err) {
+                                waitFor.abort();
+                                redrawAll();
+                                var text = err === "INSUFFICIENT_PERMISSIONS" ?
+                                        Messages.fm_forbidden : Messages.error;
+                                return void UI.warn(text);
+                            }
+                            var isTemplate = priv.isTemplate || data.isTemplate;
+                            toAddTeams.forEach(function (obj) {
+                                sframeChan.query('Q_STORE_IN_TEAM', {
+                                    href: data.href || data.rohref,
+                                    password: data.password,
+                                    path: isTemplate ? ['template'] : undefined,
+                                    title: data.title || '',
+                                    teamId: obj.id
+                                }, waitFor(function (err) {
+                                    if (err) { return void console.error(err); }
+                                }));
+                            });
+                        }));
+                    }
+                }).nThen(function (waitFor) {
+                    // Offer ownership to a friend
+                    if (toAdd.length) {
+                        // Send the command
+                        sframeChan.query('Q_SET_PAD_METADATA', {
+                            channel: channel,
+                            command: 'ADD_PENDING_OWNERS',
+                            value: toAdd,
+                            teamId: teamOwner
+                        }, waitFor(function (err, res) {
+                            err = err || (res && res.error);
+                            if (err) {
+                                waitFor.abort();
+                                redrawAll();
+                                var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden
+                                                                              : Messages.error;
+                                return void UI.warn(text);
+                            }
+                        }));
+                    }
+                }).nThen(function (waitFor) {
+                    sel.forEach(function (el) {
+                        var curve = $(el).attr('data-curve');
+                        var friend = curve === user.curvePublic ? user : friends[curve];
+                        if (!friend) { return; }
+                        common.mailbox.sendTo("ADD_OWNER", {
+                            channel: channel,
+                            href: data.href,
+                            password: data.password,
+                            title: data.title
+                        }, {
+                            channel: friend.notifications,
+                            curvePublic: friend.curvePublic
+                        }, waitFor());
+                    });
+                }).nThen(function () {
+                    redrawAll();
+                    UI.log(Messages.saved);
+                });
+            });
+            $div.append(h('p', addButton));
+            return $div;
+        };
+
+        redrawAll = function (md) {
+            var todo = function (obj) {
+                if (obj && obj.error) { return; }
+                owners = obj.owners || [];
+                pending_owners = obj.pending_owners || [];
+                $div1.empty();
+                $div2.empty();
+                $div1.append(drawRemove(false)).append(drawRemove(true));
+                $div2.append(drawAdd());
+            };
+
+            if (md) { return void todo(md); }
+            common.getPadMetadata({
+                channel: data.channel
+            }, todo);
+        };
+
+        $div1.append(drawRemove(false)).append(drawRemove(true));
+        $div2.append(drawAdd());
+
+        var handler = sframeChan.on('EV_RT_METADATA', function (md) {
+            if (!$div1.length) {
+                return void handler.stop();
+            }
+            owners = md.owners || [];
+            pending_owners = md.pending_owners || [];
+            redrawAll(md);
+        });
+
+        // Create modal
+        var link = h('div.cp-share-columns', [
+            div1,
+            div2
+            /*drawRemove()[0],
+            drawAdd()[0]*/
+        ]);
+        var linkButtons = [{
+            className: 'cancel',
+            name: Messages.filePicker_close,
+            onClick: function () {},
+            keys: [27]
+        }];
+        return UI.dialog.customModal(link, {buttons: linkButtons});
+    };
+
+    var getRightsProperties = function (common, data, cb) {
+        var $div = $('<div>');
+        if (!data) { return void cb(void 0, $div); }
+
+        var draw = function () {
+            var $d = $('<div>');
+            var priv = common.getMetadataMgr().getPrivateData();
+            var user = common.getMetadataMgr().getUserData();
+            var edPublic = priv.edPublic;
+            var owned = false;
+            var _owners = {};
+            if (data.owners && data.owners.length) {
+                if (data.owners.indexOf(edPublic) !== -1) {
+                    owned = true;
+                } else {
+                    Object.keys(priv.teams || {}).some(function (id) {
+                        var team = priv.teams[id] || {};
+                        if (team.viewer) { return; }
+                        if (data.owners.indexOf(team.edPublic) === -1) { return; }
+                        owned = id;
+                        return true;
+                    });
+                }
+                var strangers = 0;
+                data.owners.forEach(function (ed) {
+                    // If a friend is an owner, add their name to the list
+                    // otherwise, increment the list of strangers
+
+                    // Our edPublic? print "Yourself"
+                    if (ed === edPublic) {
+                        _owners[ed] = {
+                            selected: true,
+                            name: user.name,
+                            avatar: user.avatar
+                        };
+                        return;
+                    }
+                    // One of our teams? print the team name
+                    if (Object.keys(priv.teams || {}).some(function (id) {
+                        var team = priv.teams[id] || {};
+                        if (team.edPublic !== ed) { return; }
+                        _owners[ed] = {
+                            name: team.name,
+                            avatar: team.avatar
+                        };
+                        return true;
+                    })) {
+                        return;
+                    }
+                    // One of our friends? print the friend name
+                    if (Object.keys(priv.friends || {}).some(function (c) {
+                        var friend = priv.friends[c] || {};
+                        if (friend.edPublic !== ed || c === 'me') { return; }
+                        _owners[friend.edPublic] = {
+                            name: friend.displayName,
+                            avatar: friend.avatar
+                        };
+                        return true;
+                    })) {
+                        return;
+                    }
+                    // Otherwise it's a stranger
+                    strangers++;
+                });
+                if (strangers) {
+                    _owners['stangers'] = {
+                        name: Messages._getKey('properties_unknownUser', [strangers]),
+                    };
+                }
+            }
+            var _ownersGrid = UIElements.getUserGrid(Messages.creation_owners, {
+                common: common,
+                noSelect: true,
+                data: _owners,
+                large: true
+            }, function () {});
+            if (_ownersGrid && Object.keys(_owners).length) {
+                $d.append(_ownersGrid.div);
+            } else {
+                $d.append([
+                    h('label', Messages.creation_owners),
+                ]);
+                $d.append(UI.dialog.selectable(Messages.creation_noOwner, {
+                    id: 'cp-app-prop-owners',
+                }));
+
+            }
+
+            var parsed;
+            if (data.href || data.roHref) {
+                parsed = Hash.parsePadUrl(data.href || data.roHref);
+            }
+            if (owned && parsed.hashData.type === 'pad') {
+                var manageOwners = h('button.no-margin', Messages.owner_openModalButton);
+                $(manageOwners).click(function () {
+                    data.teamId = typeof(owned) !== "boolean" ? owned : undefined;
+                    var modal = createOwnerModal(common, data);
+                    UI.openCustomModal(modal, {
+                        wide: true,
+                    });
+                });
+                $d.append(h('p', manageOwners));
+            }
+
+            if (!data.noExpiration) {
+                var expire = Messages.creation_expireFalse;
+                if (data.expire && typeof (data.expire) === "number") {
+                    expire = new Date(data.expire).toLocaleString();
+                }
+                $('<label>', {'for': 'cp-app-prop-expire'}).text(Messages.creation_expiration)
+                    .appendTo($d);
+                $d.append(UI.dialog.selectable(expire, {
+                    id: 'cp-app-prop-expire',
+                }));
+            }
+
+            if (!data.noPassword) {
+                var hasPassword = data.password;
+                var $pwLabel = $('<label>', {'for': 'cp-app-prop-password'}).text(Messages.creation_passwordValue)
+                                .hide().appendTo($d);
+                var password = UI.passwordInput({
+                    id: 'cp-app-prop-password',
+                    readonly: 'readonly'
+                });
+                var $password = $(password).hide();
+                var $pwInput = $password.find('.cp-password-input');
+                $pwInput.val(data.password).click(function () {
+                    $pwInput[0].select();
+                });
+                $d.append(password);
+
+                if (hasPassword) {
+                    $pwLabel.show();
+                    $password.css('display', 'flex');
+                }
+
+                // In the properties, we should have the edit href if we know it.
+                // We should know it because the pad is stored, but it's better to check...
+                if (!data.noEditPassword && owned && data.href) { // FIXME SHEET fix password change for sheets
+                    var sframeChan = common.getSframeChannel();
+
+                    var isOO = parsed.type === 'sheet';
+                    var isFile = parsed.hashData.type === 'file';
+                    var isSharedFolder = parsed.type === 'drive';
+
+                    var changePwTitle = Messages.properties_changePassword;
+                    var changePwConfirm = isFile ? Messages.properties_confirmChangeFile : Messages.properties_confirmChange;
+                    if (!hasPassword) {
+                        changePwTitle = Messages.properties_addPassword;
+                        changePwConfirm = isFile ? Messages.properties_confirmNewFile : Messages.properties_confirmNew;
+                    }
+                    $('<label>', {'for': 'cp-app-prop-change-password'})
+                        .text(changePwTitle).appendTo($d);
+                    var newPassword = UI.passwordInput({
+                        id: 'cp-app-prop-change-password',
+                        style: 'flex: 1;'
+                    });
+                    var passwordOk = h('button', Messages.properties_changePasswordButton);
+                    var changePass = h('span.cp-password-change-container', [
+                        newPassword,
+                        passwordOk
+                    ]);
+                    var pLocked = false;
+                    $(passwordOk).click(function () {
+                        var newPass = $(newPassword).find('input').val();
+                        if (data.password === newPass ||
+                            (!data.password && !newPass)) {
+                            return void UI.alert(Messages.properties_passwordSame);
+                        }
+                        if (pLocked) { return; }
+                        pLocked = true;
+                        UI.confirm(changePwConfirm, function (yes) {
+                            if (!yes) { pLocked = false; return; }
+                            $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'}));
+                            var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' :
+                                        (isOO ? 'Q_OO_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE');
+
+                            // If this is a file password change, register to the upload events:
+                            // * if there is a pending upload, ask if we shoudl interrupt
+                            // * display upload progress
+                            var onPending;
+                            var onProgress;
+                            if (isFile) {
+                                onPending = sframeChan.on('Q_BLOB_PASSWORD_CHANGE_PENDING', function (data, cb) {
+                                    onPending.stop();
+                                    UI.confirm(Messages.upload_uploadPending, function (yes) {
+                                        cb({cancel: yes});
+                                    });
+                                });
+                                onProgress = sframeChan.on('EV_BLOB_PASSWORD_CHANGE_PROGRESS', function (data) {
+                                    if (typeof (data) !== "number") { return; }
+                                    var p = Math.round(data);
+                                    $(passwordOk).text(p + '%');
+                                });
+                            }
+
+                            sframeChan.query(q, {
+                                teamId: typeof(owned) !== "boolean" ? owned : undefined,
+                                href: data.href,
+                                password: newPass
+                            }, function (err, data) {
+                                $(passwordOk).text(Messages.properties_changePasswordButton);
+                                pLocked = false;
+                                if (err || data.error) {
+                                    console.error(err || data.error);
+                                    return void UI.alert(Messages.properties_passwordError);
+                                }
+                                UI.findOKButton().click();
+
+                                $pwLabel.show();
+                                $password.css('display', 'flex');
+                                $pwInput.val(newPass);
+
+                                // If the current document is a file or if we're changing the password from a drive,
+                                // we don't have to reload the page at the end.
+                                // Tell the user the password change was successful and abort
+                                if (isFile || priv.app !== parsed.type) {
+                                    if (onProgress && onProgress.stop) { onProgress.stop(); }
+                                    $(passwordOk).text(Messages.properties_changePasswordButton);
+                                    var alertMsg = data.warning ? Messages.properties_passwordWarningFile
+                                                                : Messages.properties_passwordSuccessFile;
+                                    return void UI.alert(alertMsg, undefined, {force: true});
+                                }
+
+                                // Pad password changed: update the href
+                                // Use hidden hash if needed (we're an owner of this pad so we know it is stored)
+                                var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']);
+                                var href = (priv.readOnly && data.roHref) ? data.roHref : data.href;
+                                if (useUnsafe === false) {
+                                    var newParsed = Hash.parsePadUrl(href);
+                                    var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass);
+                                    var newHash = Hash.getHiddenHashFromKeys(parsed.type, newSecret, {});
+                                    href = Hash.hashToHref(newHash, parsed.type);
+                                }
+
+                                if (data.warning) {
+                                    return void UI.alert(Messages.properties_passwordWarning, function () {
+                                        common.gotoURL(href);
+                                    }, {force: true});
+                                }
+                                return void UI.alert(Messages.properties_passwordSuccess, function () {
+                                    if (!isSharedFolder) {
+                                        common.gotoURL(href);
+                                    }
+                                }, {force: true});
+                            });
+                        });
+                    });
+                    $d.append(changePass);
+                }
+            }
+            return $d;
+        };
+
+        var sframeChan = common.getSframeChannel();
+        var handler = sframeChan.on('EV_RT_METADATA', function (md) {
+            if (!$div.length) {
+                handler.stop();
+                return;
+            }
+            md = JSON.parse(JSON.stringify(md));
+            data.owners = md.owners;
+            data.expire = md.expire;
+            data.pending_owners = md.pending_owners;
+            $div.empty();
+            $div.append(draw());
+        });
+        $div.append(draw());
+
+        cb(void 0, $div);
+    };
+
+    var getAccessData = function (common, opts, _cb) {
+        var cb = Util.once(Util.mkAsync(_cb));
+        opts = opts || {};
+        var data = {};
+        nThen(function (waitFor) {
+            var base = common.getMetadataMgr().getPrivateData().origin;
+            common.getPadAttribute('', waitFor(function (err, val) {
+                if (err || !val) {
+                    waitFor.abort();
+                    return void cb(err || 'EEMPTY');
+                }
+                if (!val.fileType) {
+                    delete val.owners;
+                    delete val.expire;
+                }
+                Util.extend(data, val);
+                if (data.href) { data.href = base + data.href; }
+                if (data.roHref) { data.roHref = base + data.roHref; }
+            }), opts.href);
+
+            // If this is a file, don't try to look for metadata
+            if (opts.channel && opts.channel.length > 34) { return; }
+            common.getPadMetadata({
+                channel: opts.channel // optional, fallback to current pad
+            }, waitFor(function (obj) {
+                if (obj && obj.error) { console.error(obj.error); return; }
+                data.owners = obj.owners;
+                data.expire = obj.expire;
+                data.pending_owners = obj.pending_owners;
+            }));
+        }).nThen(function () {
+            cb(void 0, data);
+        });
+    };
+    Access.getAccessModal = function (common, opts, cb) {
+        var data;
+        var content;
+        nThen(function (waitFor) {
+            getAccessData(common, opts, waitFor(function (e, _data) {
+                if (e) {
+                    waitFor.abort();
+                    return void cb(e);
+                }
+                data = _data;
+            }));
+        }).nThen(function (waitFor) {
+            getRightsProperties(common, data, waitFor(function (e, c) {
+                if (e) {
+                    waitFor.abort();
+                    return void cb(e);
+                }
+                content = c[0];
+            }));
+        }).nThen(function () {
+            var modal = UI.alert(content);
+            cb (void 0, modal);
+        });
+    };
+
+    return Access;
+});