diff --git a/apps/user_ldap/appinfo/info.xml b/apps/user_ldap/appinfo/info.xml index a1b5cf28758c8..ba7090fa26e0a 100644 --- a/apps/user_ldap/appinfo/info.xml +++ b/apps/user_ldap/appinfo/info.xml @@ -50,6 +50,7 @@ A user logs into Nextcloud with their LDAP or AD credentials, and is granted acc OCA\User_LDAP\Command\CheckUser OCA\User_LDAP\Command\CreateEmptyConfig OCA\User_LDAP\Command\DeleteConfig + OCA\User_LDAP\Command\PromoteGroup OCA\User_LDAP\Command\ResetGroup OCA\User_LDAP\Command\ResetUser OCA\User_LDAP\Command\Search diff --git a/apps/user_ldap/composer/composer/autoload_classmap.php b/apps/user_ldap/composer/composer/autoload_classmap.php index c9001d3d59ad6..408c26bf4e5bf 100644 --- a/apps/user_ldap/composer/composer/autoload_classmap.php +++ b/apps/user_ldap/composer/composer/autoload_classmap.php @@ -14,6 +14,7 @@ 'OCA\\User_LDAP\\Command\\CheckUser' => $baseDir . '/../lib/Command/CheckUser.php', 'OCA\\User_LDAP\\Command\\CreateEmptyConfig' => $baseDir . '/../lib/Command/CreateEmptyConfig.php', 'OCA\\User_LDAP\\Command\\DeleteConfig' => $baseDir . '/../lib/Command/DeleteConfig.php', + 'OCA\\User_LDAP\\Command\\PromoteGroup' => $baseDir . '/../lib/Command/PromoteGroup.php', 'OCA\\User_LDAP\\Command\\ResetGroup' => $baseDir . '/../lib/Command/ResetGroup.php', 'OCA\\User_LDAP\\Command\\ResetUser' => $baseDir . '/../lib/Command/ResetUser.php', 'OCA\\User_LDAP\\Command\\Search' => $baseDir . '/../lib/Command/Search.php', diff --git a/apps/user_ldap/composer/composer/autoload_static.php b/apps/user_ldap/composer/composer/autoload_static.php index 53d7bcb648140..b34829f8a7e0e 100644 --- a/apps/user_ldap/composer/composer/autoload_static.php +++ b/apps/user_ldap/composer/composer/autoload_static.php @@ -29,6 +29,7 @@ class ComposerStaticInitUser_LDAP 'OCA\\User_LDAP\\Command\\CheckUser' => __DIR__ . '/..' . '/../lib/Command/CheckUser.php', 'OCA\\User_LDAP\\Command\\CreateEmptyConfig' => __DIR__ . '/..' . '/../lib/Command/CreateEmptyConfig.php', 'OCA\\User_LDAP\\Command\\DeleteConfig' => __DIR__ . '/..' . '/../lib/Command/DeleteConfig.php', + 'OCA\\User_LDAP\\Command\\PromoteGroup' => __DIR__ . '/..' . '/../lib/Command/PromoteGroup.php', 'OCA\\User_LDAP\\Command\\ResetGroup' => __DIR__ . '/..' . '/../lib/Command/ResetGroup.php', 'OCA\\User_LDAP\\Command\\ResetUser' => __DIR__ . '/..' . '/../lib/Command/ResetUser.php', 'OCA\\User_LDAP\\Command\\Search' => __DIR__ . '/..' . '/../lib/Command/Search.php', diff --git a/apps/user_ldap/lib/Command/PromoteGroup.php b/apps/user_ldap/lib/Command/PromoteGroup.php new file mode 100644 index 0000000000000..7ec1806433269 --- /dev/null +++ b/apps/user_ldap/lib/Command/PromoteGroup.php @@ -0,0 +1,128 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\Group_Proxy; +use OCP\IGroup; +use OCP\IGroupManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + +class PromoteGroup extends Command { + + public function __construct( + private IGroupManager $groupManager, + private Group_Proxy $backend + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:promote-group') + ->setDescription('declares the specified group as admin group (only one is possible per LDAP configuration)') + ->addArgument( + 'group', + InputArgument::REQUIRED, + 'the group ID in Nextcloud or a group name' + ) + ->addOption( + 'yes', + 'y', + InputOption::VALUE_NONE, + 'do not ask for confirmation' + ); + } + + protected function formatGroupName(IGroup $group): string { + $idLabel = ''; + if ($group->getGID() !== $group->getDisplayName()) { + $idLabel = sprintf(' (Group ID: %s)', $group->getGID()); + } + return sprintf('%s%s', $group->getDisplayName(), $idLabel); + } + + protected function promoteGroup(IGroup $group, InputInterface $input, OutputInterface $output): void { + $access = $this->backend->getLDAPAccess($group->getGID()); + $currentlyPromotedGroupId = $access->connection->ldapAdminGroup; + if ($currentlyPromotedGroupId === $group->getGID()) { + $output->writeln('The specified group is already promoted'); + return; + } + + if ($input->getOption('yes') === false) { + $currentlyPromotedGroup = $this->groupManager->get($currentlyPromotedGroupId); + $demoteLabel = ''; + if ($currentlyPromotedGroup instanceof IGroup && $this->backend->groupExists($currentlyPromotedGroup->getGID())) { + $groupNameLabel = $this->formatGroupName($currentlyPromotedGroup); + $demoteLabel = sprintf('and demote %s ', $groupNameLabel); + } + + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $q = new Question(sprintf('Promote %s to the admin group %s(y|N)? ', $this->formatGroupName($group), $demoteLabel)); + $input->setOption('yes', $helper->ask($input, $output, $q) === 'y'); + } + if ($input->getOption('yes') === true) { + $access->connection->setConfiguration(['ldapAdminGroup' => $group->getGID()]); + $access->connection->saveConfiguration(); + $output->writeln(sprintf('Group %s was promoted', $group->getDisplayName())); + } else { + $output->writeln('Group promotion cancelled'); + } + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $groupInput = (string)$input->getArgument('group'); + $group = $this->groupManager->get($groupInput); + + if ($group instanceof IGroup && $this->backend->groupExists($group->getGID())) { + $this->promoteGroup($group, $input, $output); + return 0; + } + + $groupCandidates = $this->backend->getGroups($groupInput, 20); + foreach ($groupCandidates as $gidCandidate) { + $group = $this->groupManager->get($gidCandidate); + if ($group !== null + && $this->backend->groupExists($group->getGID()) // ensure it is an LDAP group + && ($group->getGID() === $groupInput + || $group->getDisplayName() === $groupInput) + ) { + $this->promoteGroup($group, $input, $output); + return 0; + } + } + + $output->writeln('No matching group found'); + return 1; + } + +} diff --git a/apps/user_ldap/lib/Configuration.php b/apps/user_ldap/lib/Configuration.php index ef64f75a9ef60..3dda4e3ebbad4 100644 --- a/apps/user_ldap/lib/Configuration.php +++ b/apps/user_ldap/lib/Configuration.php @@ -133,6 +133,7 @@ class Configuration { 'ldapAttributeRole' => null, 'ldapAttributeHeadline' => null, 'ldapAttributeBiography' => null, + 'ldapAdminGroup' => '', ]; public function __construct(string $configPrefix, bool $autoRead = true) { @@ -488,6 +489,7 @@ public function getDefaults(): array { 'ldap_attr_role' => '', 'ldap_attr_headline' => '', 'ldap_attr_biography' => '', + 'ldap_admin_group' => '', ]; } @@ -563,6 +565,7 @@ public function getConfigTranslationArray(): array { 'ldap_attr_role' => 'ldapAttributeRole', 'ldap_attr_headline' => 'ldapAttributeHeadline', 'ldap_attr_biography' => 'ldapAttributeBiography', + 'ldap_admin_group' => 'ldapAdminGroup', ]; return $array; } diff --git a/apps/user_ldap/lib/Connection.php b/apps/user_ldap/lib/Connection.php index f90add9ef9e65..05cd3d10698b5 100644 --- a/apps/user_ldap/lib/Connection.php +++ b/apps/user_ldap/lib/Connection.php @@ -82,6 +82,7 @@ * @property string ldapAttributeRole * @property string ldapAttributeHeadline * @property string ldapAttributeBiography + * @property string ldapAdminGroup */ class Connection extends LDAPUtility { /** diff --git a/apps/user_ldap/lib/Group_LDAP.php b/apps/user_ldap/lib/Group_LDAP.php index 70dca6a40cab6..999dc403c2fa0 100644 --- a/apps/user_ldap/lib/Group_LDAP.php +++ b/apps/user_ldap/lib/Group_LDAP.php @@ -48,6 +48,7 @@ use OC\ServerNotAvailableException; use OCA\User_LDAP\User\OfflineUser; use OCP\Cache\CappedMemoryCache; +use OCP\Group\Backend\IIsAdminBackend; use OCP\GroupInterface; use OCP\Group\Backend\IDeleteGroupBackend; use OCP\Group\Backend\IGetDisplayNameBackend; @@ -57,7 +58,7 @@ use Psr\Log\LoggerInterface; use function json_decode; -class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend { +class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend, IDeleteGroupBackend, IIsAdminBackend { protected bool $enabled = false; /** @var CappedMemoryCache $cachedGroupMembers array of users with gid as key */ @@ -1227,6 +1228,7 @@ protected function filterValidGroups(array $listOfGroups): array { public function implementsActions($actions): bool { return (bool)((GroupInterface::COUNT_USERS | GroupInterface::DELETE_GROUP | + GroupInterface::IS_ADMIN | $this->groupPluginManager->getImplementedActions()) & $actions); } @@ -1392,4 +1394,18 @@ public function getDisplayName(string $gid): string { $this->access->connection->writeToCache($cacheKey, $displayName); return $displayName; } + + /** + * @throws ServerNotAvailableException + */ + public function isAdmin(string $uid): bool { + if (!$this->enabled) { + return false; + } + $ldapAdminGroup = $this->access->connection->ldapAdminGroup; + if ($ldapAdminGroup === '') { + return false; + } + return $this->inGroup($uid, $ldapAdminGroup); + } } diff --git a/apps/user_ldap/lib/Group_Proxy.php b/apps/user_ldap/lib/Group_Proxy.php index 70fda715d5e24..a9261f197e065 100644 --- a/apps/user_ldap/lib/Group_Proxy.php +++ b/apps/user_ldap/lib/Group_Proxy.php @@ -30,12 +30,13 @@ use OCP\Group\Backend\IDeleteGroupBackend; use OCP\Group\Backend\IGetDisplayNameBackend; +use OCP\Group\Backend\IIsAdminBackend; use OCP\Group\Backend\INamedBackend; use OCP\GroupInterface; use OCP\IConfig; use OCP\IUserManager; -class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend { +class Group_Proxy extends Proxy implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend, INamedBackend, IDeleteGroupBackend, IIsAdminBackend { private $backends = []; private ?Group_LDAP $refBackend = null; private Helper $helper; @@ -347,4 +348,8 @@ public function getBackendName(): string { public function searchInGroup(string $gid, string $search = '', int $limit = -1, int $offset = 0): array { return $this->handleRequest($gid, 'searchInGroup', [$gid, $search, $limit, $offset]); } + + public function isAdmin(string $uid): bool { + return $this->handleRequest($uid, 'isAdmin', [$uid]); + } } diff --git a/build/integration/ldap_features/openldap-numerical-id.feature b/build/integration/ldap_features/openldap-numerical-id.feature index 4ea63823295fc..75eb68271927a 100644 --- a/build/integration/ldap_features/openldap-numerical-id.feature +++ b/build/integration/ldap_features/openldap-numerical-id.feature @@ -66,3 +66,31 @@ Scenario: Test LDAP group membership with intermediate groups not matching filte | 50194 | 1 | | 59376 | 1 | | 59463 | 1 | + +Scenario: Test LDAP admin group mapping, empowered user + Given modify LDAP configuration + | ldapBaseGroups | ou=NumericGroups,dc=nextcloud,dc=ci | + | ldapGroupFilter | (objectclass=groupOfNames) | + | ldapGroupMemberAssocAttr | member | + | ldapAdminGroup | 3001 | + | useMemberOfToDetectMembership | 1 | + And cookies are reset + # alice, part of the promoted group + And Logging in using web as "92379" + And sending "GET" to "/cloud/groups" + And sending "GET" to "/cloud/groups/2000/users" + And Sending a "GET" to "/index.php/settings/admin/overview" with requesttoken + Then the HTTP status code should be "200" + +Scenario: Test LDAP admin group mapping, regular user (no access) + Given modify LDAP configuration + | ldapBaseGroups | ou=NumericGroups,dc=nextcloud,dc=ci | + | ldapGroupFilter | (objectclass=groupOfNames) | + | ldapGroupMemberAssocAttr | member | + | ldapAdminGroup | 3001 | + | useMemberOfToDetectMembership | 1 | + And cookies are reset + # gustaf, not part of the promoted group + And Logging in using web as "59376" + And Sending a "GET" to "/index.php/settings/admin/overview" with requesttoken + Then the HTTP status code should be "403" diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index aa6617c4b6d94..1f8ac08c4f770 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -62,6 +62,7 @@ class LoginController extends Controller { public const LOGIN_MSG_INVALIDPASSWORD = 'invalidpassword'; public const LOGIN_MSG_USERDISABLED = 'userdisabled'; + public const LOGIN_MSG_CSRFCHECKFAILED = 'csrfCheckFailed'; private IUserManager $userManager; private IConfig $config; @@ -311,7 +312,7 @@ public function tryLogin(Chain $loginChain, $user, $user, $redirect_url, - $this->l10n->t('Please try again') + self::LOGIN_MSG_CSRFCHECKFAILED ); } diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue index f4307c5ff16f8..f29991a209564 100644 --- a/core/src/components/login/LoginForm.vue +++ b/core/src/components/login/LoginForm.vue @@ -32,6 +32,11 @@ type="warning"> {{ t('core', 'Please contact your administrator.') }} + + {{ t('core', 'Please try again.') }} +
@@ -186,6 +191,9 @@ export default { apacheAuthFailed() { return this.errors.indexOf('apacheAuthFailed') !== -1 }, + csrfCheckFailed() { + return this.errors.indexOf('csrfCheckFailed') !== -1 + }, internalException() { return this.errors.indexOf('internalexception') !== -1 }, diff --git a/dist/core-login.js b/dist/core-login.js index 8680ceaabf5b9..096b3ac434f4c 100644 --- a/dist/core-login.js +++ b/dist/core-login.js @@ -1,3 +1,3 @@ /*! For license information please see core-login.js.LICENSE.txt */ -(()=>{var e,o,i,a={74839:(e,o,i)=>{"use strict";var a=i(20144),r=i(69183),s=i(19755),l=i.n(s),c=i(79753),d=i(25108);const u={},p=[];var f=i(18181),h=i(64024),m=i(25108);const g={updatableNotification:null,getDefaultNotificationFunction:null,setDefault(e){this.getDefaultNotificationFunction=e},hide(e,t){f.default.isFunction(e)&&(t=e,e=void 0),e?(e.each((function(){l()(this)[0].toastify?l()(this)[0].toastify.hideToast():m.error("cannot hide toast because object is not set"),this===this.updatableNotification&&(this.updatableNotification=null)})),t&&t.call(),this.getDefaultNotificationFunction&&this.getDefaultNotificationFunction()):m.error("Missing argument $row in OC.Notification.hide() call, caller needs to be adjusted to only dismiss its own notification")},showHtml(e,t){(t=t||{}).isHTML=!0,t.timeout=t.timeout?t.timeout:h.Rl;const n=(0,h.PV)(e,t);return n.toastElement.toastify=n,l()(n.toastElement)},show(e,t){(t=t||{}).timeout=t.timeout?t.timeout:h.Rl;const n=(0,h.PV)(function(e){return e.toString().split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}(e),t);return n.toastElement.toastify=n,l()(n.toastElement)},showUpdate(e){return this.updatableNotification&&this.updatableNotification.hideToast(),this.updatableNotification=(0,h.PV)(e,{timeout:h.Rl}),this.updatableNotification.toastElement.toastify=this.updatableNotification,l()(this.updatableNotification.toastElement)},showTemporary(e,t){(t=t||{}).timeout=t.timeout||h.TN;const n=(0,h.PV)(e,t);return n.toastElement.toastify=n,l()(n.toastElement)},isHidden:()=>!l()("#content").find(".toastify").length},v=f.default.throttle((()=>{g.showTemporary(t("core","Connection to server lost"))}),7e3,{trailing:!1});let w=!1;const y={enableDynamicSlideToggle(){w=!0},showAppSidebar:function(e){(e||l()("#app-sidebar")).removeClass("disappear").show(),l()("#app-content").trigger(new(l().Event)("appresized"))},hideAppSidebar:function(e){(e||l()("#app-sidebar")).hide().addClass("disappear"),l()("#app-content").trigger(new(l().Event)("appresized"))}};function b(e,t,n){"post"!==e&&"delete"!==e||!Se.PasswordConfirmation.requiresPasswordConfirmation()?(n=n||{},l().ajax({type:e.toUpperCase(),url:(0,c.generateOcsUrl)("apps/provisioning_api/api/v1/config/apps")+t,data:n.data||{},success:n.success,error:n.error})):Se.PasswordConfirmation.requirePasswordConfirmation(_.bind(b,this,e,t,n))}const C=window.oc_appconfig||{},A={getValue:function(e,t,n,o){!function(e,t,n,o){(o=o||{}).data={defaultValue:n},b("get","/"+e+"/"+t,o)}(e,t,n,{success:o})},setValue:function(e,t,n){!function(e,t,n,o){(o=o||{}).data={value:n},b("post","/"+e+"/"+t,o)}(e,t,n)},getApps:function(e){!function(e){b("get","",e)}({success:e})},getKeys:function(e,t){!function(e,t){b("get","/"+e,t)}(e,{success:t})},deleteKey:function(e,t){!function(e,t,n){b("delete","/"+e+"/"+t,void 0)}(e,t)}};var P=i(25108);const k=void 0!==window._oc_appswebroots&&window._oc_appswebroots;var x=i(72316),T=i.n(x),O=i(76591),E=i(25108);const j={create:"POST",update:"PROPPATCH",patch:"PROPPATCH",delete:"DELETE",read:"PROPFIND"};function L(e,t){if(f.default.isArray(e))return f.default.map(e,(function(e){return L(e,t)}));var n={href:e.href};return f.default.each(e.propStat,(function(e){if("HTTP/1.1 200 OK"===e.status)for(var o in e.properties){var i=o;o in t&&(i=t[o]),n[i]=e.properties[o]}})),n.id||(n.id=S(n.href)),n}function S(e){var t=e.indexOf("?");t>0&&(e=e.substr(0,t));var n,o=e.split("/");do{n=o[o.length-1],o.pop()}while(!n&&o.length>0);return n}function N(e){return e>=200&&e<=299}function I(e,t,n,o){return e.propPatch(t.url,function(e,t){var n,o={};for(n in e){var i=t[n],a=e[n];i||(E.warn('No matching DAV property for property "'+n),i=n),(f.default.isBoolean(a)||f.default.isNumber(a))&&(a=""+a),o[i]=a}return o}(n.changed,t.davProperties),o).then((function(e){N(e.status)?f.default.isFunction(t.success)&&t.success(n.toJSON()):f.default.isFunction(t.error)&&t.error(e)}))}const U=T().noConflict();Object.assign(U,{davCall:(e,t)=>{var n=new O.dav.Client({baseUrl:e.url,xmlNamespaces:f.default.extend({"DAV:":"d","http://owncloud.org/ns":"oc"},e.xmlNamespaces||{})});n.resolveUrl=function(){return e.url};var o=f.default.extend({"X-Requested-With":"XMLHttpRequest",requesttoken:OC.requestToken},e.headers);return"PROPFIND"===e.type?function(e,t,n,o){return e.propFind(t.url,f.default.values(t.davProperties)||[],t.depth,o).then((function(e){if(N(e.status)){if(f.default.isFunction(t.success)){var n=f.default.invert(t.davProperties),o=L(e.body,n);t.depth>0&&o.shift(),t.success(o)}}else f.default.isFunction(t.error)&&t.error(e)}))}(n,e,0,o):"PROPPATCH"===e.type?I(n,e,t,o):"MKCOL"===e.type?function(e,t,n,o){return e.request(t.type,t.url,o,null).then((function(i){N(i.status)?I(e,t,n,o):f.default.isFunction(t.error)&&t.error(i)}))}(n,e,t,o):function(e,t,n,o){return o["Content-Type"]="application/json",e.request(t.type,t.url,o,t.data).then((function(e){if(N(e.status)){if(f.default.isFunction(t.success)){if("PUT"===t.type||"POST"===t.type||"MKCOL"===t.type){var o=e.body||n.toJSON(),i=e.xhr.getResponseHeader("Content-Location");return"POST"===t.type&&i&&(o.id=S(i)),void t.success(o)}if(207===e.status){var a=f.default.invert(t.davProperties);t.success(L(e.body,a))}else t.success(e.body)}}else f.default.isFunction(t.error)&&t.error(e)}))}(n,e,t,o)},davSync:(e=>(t,n,o)=>{var i={type:j[t]||t},a=n instanceof e.Collection;if("update"===t&&(n.hasInnerCollection?i.type="MKCOL":(n.usePUT||n.collection&&n.collection.usePUT)&&(i.type="PUT")),o.url||(i.url=f.default.result(n,"url")||function(){throw new Error('A "url" property or function must be specified')}()),null!=o.data||!n||"create"!==t&&"update"!==t&&"patch"!==t||(i.data=JSON.stringify(o.attrs||n.toJSON(o))),"PROPFIND"!==i.type&&(i.processData=!1),"PROPFIND"===i.type||"PROPPATCH"===i.type){var r=n.davProperties;!r&&n.model&&(r=n.model.prototype.davProperties),r&&(f.default.isFunction(r)?i.davProperties=r.call(n):i.davProperties=r),i.davProperties=f.default.extend(i.davProperties||{},o.davProperties),f.default.isUndefined(o.depth)&&(o.depth=a?1:0)}var s=o.error;o.error=function(e,t,n){o.textStatus=t,o.errorThrown=n,s&&s.call(o.context,e,t,n)};var l=o.xhr=e.davCall(f.default.extend(i,o),n);return n.trigger("request",n,l,o),l})(U)});const $=U;var F=i(65358);const R=window._oc_config||{};var M=i(25108);const B=x.Model.extend({defaults:{fullName:"",lastMessage:"",actions:[],hasOneAction:!1,hasTwoActions:!1,hasManyActions:!1},initialize:function(){0===this.get("actions").length?this.set("hasOneAction",!0):1===this.get("actions").length?(this.set("hasTwoActions",!0),this.set("secondAction",this.get("actions")[0])):this.set("hasManyActions",!0);const e=this.get("fullName");this.get("avatar")&&e&&this.set("avatarLabel",t("core","Avatar of {fullName}",{fullName:e}))}}),z=x.Collection.extend({model:B}),D=x.View.extend({_collection:void 0,_subViews:[],tagName:"ul",initialize:function(e){this._collection=e.collection},render:function(){var e=this;return e.$el.html(""),e._subViews=[],e._collection.forEach((function(t){var n=new q({model:t});n.render(),e.$el.append(n.$el),n.on("toggle:actionmenu",e._onChildActionMenuToggle,e),e._subViews.push(n)})),e},_onChildActionMenuToggle:function(e){this._subViews.forEach((function(t){t.trigger("parent:toggle:actionmenu",e)}))}}),q=x.View.extend({className:"contact",tagName:"li",_template:void 0,_model:void 0,_actionMenuShown:!1,events:{"click .icon-more":"_onToggleActionsMenu"},contactTemplate:i(10944),template:function(e){return this.contactTemplate(e)},initialize:function(e){this._model=e.model,this.on("parent:toggle:actionmenu",this._onOtherActionMenuOpened,this)},render:function(){return this.$el.html(this.template({contact:this._model.toJSON()})),this.delegateEvents(),this.$("div.avatar").imageplaceholder(this._model.get("fullName")),this},_onToggleActionsMenu:function(){this._actionMenuShown=!this._actionMenuShown,this._actionMenuShown?this.$(".menu").show():this.$(".menu").hide(),this.trigger("toggle:actionmenu",this.$el)},_onOtherActionMenuOpened:function(e){this.$el.is(e)||(this._actionMenuShown=!1,this.$(".menu").hide())}}),H=x.View.extend({_loadingTemplate:void 0,_errorTemplate:void 0,_contentTemplate:void 0,_contactsTemplate:void 0,_contacts:void 0,_searchTerm:"",events:{"input #contactsmenu-search":"_onSearch"},templates:{loading:i(95386),error:i(20421),menu:i(66115),list:i(34083)},_onSearch:f.default.debounce((function(e){var t=this.$("#contactsmenu-search").val();t!==this._searchTerm&&(this.trigger("search",this.$("#contactsmenu-search").val()),this._searchTerm=t)}),700),loadingTemplate:function(e){return this.templates.loading(e)},errorTemplate:function(e){return this.templates.error(f.default.extend({couldNotLoadText:t("core","Could not load your contacts")},e))},contentTemplate:function(e){return this.templates.menu(f.default.extend({searchContactsText:t("core","Search contacts …")},e))},contactsTemplate:function(e){return this.templates.list(f.default.extend({noContactsFoundText:t("core","No contacts found"),showAllContactsText:t("core","Show all contacts …"),contactsAppMgmtText:t("core","Install the Contacts app")},e))},initialize:function(e){this.options=e},showLoading:function(e){this.render(),this._contacts=void 0,this.$(".content").html(this.loadingTemplate({loadingText:e}))},showError:function(){this.render(),this._contacts=void 0,this.$(".content").html(this.errorTemplate())},showContacts:function(e,t){this._contacts=e.contacts,this.render({contacts:e.contacts});var n=new D({collection:e.contacts});n.render(),this.$(".content").html(this.contactsTemplate({contacts:e.contacts,searchTerm:t,contactsAppEnabled:e.contactsAppEnabled,contactsAppURL:Se.generateUrl("/apps/contacts"),canInstallApp:Se.isUserAdmin(),contactsAppMgmtURL:Se.generateUrl("/settings/apps/social/contacts")})),this.$("#contactsmenu-contacts").html(n.$el)},render:function(e){var t=this.$("#contactsmenu-search").val();return this.$el.html(this.contentTemplate(e)),this.$("#contactsmenu-search").val(t),this.$("#contactsmenu-search").focus(),this}}),Y=function(e){this.initialize(e)};Y.prototype={$el:void 0,_view:void 0,_contactsPromise:void 0,initialize:function(e){this.$el=l()(e.el),this._view=new H({el:this.$el}),this._view.on("search",(function(e){this.loadContacts(e)}),this)},_getContacts:function(e){var t=Se.generateUrl("/contactsmenu/contacts");return Promise.resolve(l().ajax(t,{method:"POST",data:{filter:e}}))},loadContacts:function(e){var n=this;return n._contactsPromise||(n._contactsPromise=n._getContacts(e)),f.default.isUndefined(e)||""===e?n._view.showLoading(t("core","Loading your contacts …")):n._view.showLoading(t("core","Looking for {term} …",{term:e})),n._contactsPromise.then((function(t){t.contacts=new z(t.contacts),n._view.showContacts(t,e)}),(function(e){n._view.showError(),M.error("There was an error loading your contacts",e)})).then((function(){delete n._contactsPromise})).catch(M.error.bind(this))}};const K=Y,Z=document.getElementsByTagName("head")[0].getAttribute("data-user"),V=document.getElementsByTagName("head")[0].getAttribute("data-user-displayname"),G=void 0!==Z&&Z;var W=i(25108);const J={Search:class{constructor(){OC.debug&&W.warn("OCA.Search is deprecated. Please use the unified search API instead")}}},Q=e=>"click"===e.type||"keydown"===e.type&&"Enter"===e.key;var X=i(51819),ee=i(25108);const te={YES_NO_BUTTONS:70,OK_BUTTONS:71,FILEPICKER_TYPE_CHOOSE:1,FILEPICKER_TYPE_MOVE:2,FILEPICKER_TYPE_COPY:3,FILEPICKER_TYPE_COPY_MOVE:4,FILEPICKER_TYPE_CUSTOM:5,dialogsCounter:0,alert:function(e,t,n,o){this.message(e,t,"alert",te.OK_BUTTON,n,o)},info:function(e,t,n,o){this.message(e,t,"info",te.OK_BUTTON,n,o)},confirm:function(e,t,n,o){return this.message(e,t,"notice",te.YES_NO_BUTTONS,n,o)},confirmDestructive:function(e,t,n,o,i){return this.message(e,t,"none",n,o,void 0===i||i)},confirmHtml:function(e,t,n,o){return this.message(e,t,"notice",te.YES_NO_BUTTONS,n,o,!0)},prompt:function(e,n,o,i,a,r){return l().when(this._getMessageTemplate()).then((function(s){var c="oc-dialog-"+te.dialogsCounter+"-content",d="#"+c,u=s.octemplate({dialog_name:c,title:n,message:e,type:"notice"}),p=l()("");p.attr("type",r?"password":"text").attr("id",c+"-input").attr("placeholder",a);var h=l()("