diff --git a/config/zentaopms.php b/config/zentaopms.php index ca211cb031..0489a3334c 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -289,6 +289,7 @@ $config->openMethods[] = 'gitlab.createtag'; $config->openMethods[] = 'gitlab.deletetag'; $config->openMethods[] = 'gitfox.create'; $config->openMethods[] = 'gitfox.edit'; +$config->openMethods[] = 'gitfox.binduser'; $config->openMethods[] = 'gogs.create'; $config->openMethods[] = 'gogs.edit'; $config->openMethods[] = 'gogs.binduser'; diff --git a/module/action/lang/de.php b/module/action/lang/de.php index 427a23bc95..97e43e80bc 100644 --- a/module/action/lang/de.php +++ b/module/action/lang/de.php @@ -153,6 +153,7 @@ $lang->action->objectTypes['gitlabbranch'] = 'GitLab Branch'; $lang->action->objectTypes['gitlabbranchpriv'] = 'GitLab Protected Branches'; $lang->action->objectTypes['gitlabtag'] = 'GitLab Tag'; $lang->action->objectTypes['gitlabtagpriv'] = 'GitLab Tag Protected'; +$lang->action->objectTypes['gitfoxuser'] = 'GitFox User'; $lang->action->objectTypes['giteauser'] = 'Gitea User'; $lang->action->objectTypes['gogsuser'] = 'Gogs User'; $lang->action->objectTypes['kanbanspace'] = 'Kanban Space'; diff --git a/module/action/lang/en.php b/module/action/lang/en.php index a11143efa2..0bcfd16531 100755 --- a/module/action/lang/en.php +++ b/module/action/lang/en.php @@ -153,6 +153,7 @@ $lang->action->objectTypes['gitlabbranch'] = 'GitLab Branch'; $lang->action->objectTypes['gitlabbranchpriv'] = 'GitLab Protected Branches'; $lang->action->objectTypes['gitlabtag'] = 'GitLab Tag'; $lang->action->objectTypes['gitlabtagpriv'] = 'GitLab Tag Protected'; +$lang->action->objectTypes['gitfoxuser'] = 'GitFox User'; $lang->action->objectTypes['giteauser'] = 'Gitea User'; $lang->action->objectTypes['gogsuser'] = 'Gogs User'; $lang->action->objectTypes['kanbanspace'] = 'Kanban Space'; diff --git a/module/action/lang/fr.php b/module/action/lang/fr.php index 69eb86dadd..89048a31a5 100644 --- a/module/action/lang/fr.php +++ b/module/action/lang/fr.php @@ -153,6 +153,7 @@ $lang->action->objectTypes['gitlabbranch'] = 'GitLab Branch'; $lang->action->objectTypes['gitlabbranchpriv'] = 'GitLab Protected Branches'; $lang->action->objectTypes['gitlabtag'] = 'GitLab Tag'; $lang->action->objectTypes['gitlabtagpriv'] = 'GitLab Tag Protected'; +$lang->action->objectTypes['gitfoxuser'] = 'GitFox User'; $lang->action->objectTypes['giteauser'] = 'Gitea User'; $lang->action->objectTypes['gogsuser'] = 'Gogs User'; $lang->action->objectTypes['kanbanspace'] = 'Kanban Space'; diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php index 23443b7dd2..2a46539161 100755 --- a/module/action/lang/zh-cn.php +++ b/module/action/lang/zh-cn.php @@ -153,6 +153,7 @@ $lang->action->objectTypes['gitlabbranch'] = 'GitLab分支'; $lang->action->objectTypes['gitlabbranchpriv'] = 'GitLab保护分支'; $lang->action->objectTypes['gitlabtag'] = 'GitLab标签'; $lang->action->objectTypes['gitlabtagpriv'] = 'GitLab标签保护'; +$lang->action->objectTypes['gitfoxuser'] = 'GitFox用户'; $lang->action->objectTypes['giteauser'] = 'Gitea用户'; $lang->action->objectTypes['gogsuser'] = 'Gogs用户'; $lang->action->objectTypes['kanbanspace'] = '看板空间'; diff --git a/module/gitfox/config/dtable.php b/module/gitfox/config/dtable.php index bb96edb1a0..1695e13a83 100644 --- a/module/gitfox/config/dtable.php +++ b/module/gitfox/config/dtable.php @@ -50,3 +50,27 @@ $config->gitfox->dtable->fieldList['actions']['sortType'] = false; $config->gitfox->dtable->fieldList['actions']['fixed'] = 'right'; $config->gitfox->dtable->fieldList['actions']['menu'] = array('edit', 'bindUser', 'delete'); $config->gitfox->dtable->fieldList['actions']['list'] = $config->gitfox->actionList; + +$config->gitfox->dtable->bindUser = new stdclass(); +$config->gitfox->dtable->bindUser->fieldList['gitfoxUser']['title'] = $lang->gitfox->gitfoxAccount; +$config->gitfox->dtable->bindUser->fieldList['gitfoxUser']['type'] = 'avatarName'; +$config->gitfox->dtable->bindUser->fieldList['gitfoxUser']['sortType'] = false; +$config->gitfox->dtable->bindUser->fieldList['gitfoxUser']['width'] = 300; + +$config->gitfox->dtable->bindUser->fieldList['gitfoxEmail']['title'] = $lang->gitfox->gitfoxEmail; +$config->gitfox->dtable->bindUser->fieldList['gitfoxEmail']['type'] = 'text'; +$config->gitfox->dtable->bindUser->fieldList['gitfoxEmail']['width'] = 200; + +$config->gitfox->dtable->bindUser->fieldList['email']['title'] = $lang->gitfox->zentaoEmail; +$config->gitfox->dtable->bindUser->fieldList['email']['type'] = 'text'; +$config->gitfox->dtable->bindUser->fieldList['email']['width'] = 200; + +$config->gitfox->dtable->bindUser->fieldList['zentaoUsers']['title'] = array('html' => $lang->gitfox->zentaoAccount . "{$lang->gitfox->accountDesc}"); +$config->gitfox->dtable->bindUser->fieldList['zentaoUsers']['type'] = 'control'; +$config->gitfox->dtable->bindUser->fieldList['zentaoUsers']['control'] = 'picker'; +$config->gitfox->dtable->bindUser->fieldList['zentaoUsers']['width'] = 300; + +$config->gitfox->dtable->bindUser->fieldList['status']['title'] = $lang->gitfox->bindingStatus; +$config->gitfox->dtable->bindUser->fieldList['status']['html'] = true; +$config->gitfox->dtable->bindUser->fieldList['status']['width'] = 100; +$config->gitfox->dtable->bindUser->fieldList['status']['map'] = $lang->gitfox->bindStatus; diff --git a/module/gitfox/control.php b/module/gitfox/control.php index eee55a4497..3121058a57 100644 --- a/module/gitfox/control.php +++ b/module/gitfox/control.php @@ -121,5 +121,76 @@ class gitfox extends control if(is_bool($user)) return $this->send(array('result' => 'fail', 'message' => array('url' => array($this->lang->gitfox->serverFail)))); if(!isset($user[0]->uid)) return $this->send(array('result' => 'fail', 'message' => array('token' => array($this->lang->gitfox->tokenError)))); } + + /** + * Bind gitfox user to zentao users. + * + * @param int $gitfoxID + * @param string $type + * @access public + * @return void + */ + public function bindUser(int $gitfoxID, string $type = 'all') + { + $userPairs = $this->loadModel('user')->getPairs('noclosed|noletter'); + + $user = $this->gitfox->apiGetCurrentUser($gitfoxID); + if(!isset($user->admin) or !$user->admin) return $this->send(array('result' => 'fail', 'message' => $this->lang->gitfox->tokenLimit, 'locate' => $this->createLink('gitfox', 'edit', array('gitfoxID' => $gitfoxID)))); + + $zentaoUsers = $this->dao->select('account,email,realname')->from(TABLE_USER)->where('deleted')->eq('0')->fetchAll('account'); + + if($_POST) + { + $users = $this->post->zentaoUsers; + $gitfoxNames = $this->post->gitfoxUserNames; + + $result = $this->gitfoxZen->checkUserRepeat($users, $userPairs); + if($result['result'] != 'success') return $this->send($result); + + $this->gitfoxZen->bindUsers($gitfoxID, $users, $gitfoxNames, $zentaoUsers); + + if(dao::isError()) return $this->sendError(dao::getError()); + return $this->sendSuccess(array('message' => $this->lang->saveSuccess, 'load' => helper::createLink('space', 'browse'))); + } + + $userList = array(); + $gitfoxUsers = $this->gitfox->apiGetUsers($gitfoxID); + $bindedUsers = $this->loadModel('pipeline')->getUserBindedPairs($gitfoxID, 'gitfox', 'account,openID'); + $matchedResult = $this->gitfox->getMatchedUsers($gitfoxID, $gitfoxUsers, $zentaoUsers); + + foreach($gitfoxUsers as $gitfoxUser) + { + $user = new stdclass(); + $user->email = ''; + $user->status = 'notBind'; + $user->gitfoxID = $gitfoxUser->id; + $user->gitfoxEmail = $gitfoxUser->email; + $user->gitfoxUser = $gitfoxUser->realname . '@' . $gitfoxUser->account; + + $user->zentaoUsers = isset($matchedResult[$gitfoxUser->id]) ? $matchedResult[$gitfoxUser->id]->zentaoAccount : ''; + if($user->zentaoUsers) + { + if(isset($zentaoUsers[$user->zentaoUsers])) $user->email = $zentaoUsers[$user->zentaoUsers]->email; + + if(isset($bindedUsers[$user->zentaoUsers]) && $bindedUsers[$user->zentaoUsers] == $gitfoxUser->id) + { + $user->status = 'binded'; + if(!isset($bindedUsers[$user->zentaoUsers])) $user->status = 'bindedError'; + } + } + + if($type != 'all' && $user->status != $type) continue; + $userList[] = $user; + } + + $this->view->title = $this->lang->gitfox->bindUser; + $this->view->type = $type; + $this->view->gitfoxID = $gitfoxID; + $this->view->recTotal = count($userList); + $this->view->userList = $userList; + $this->view->userPairs = $userPairs; + $this->view->zentaoUsers = $zentaoUsers; + $this->display(); + } } diff --git a/module/gitfox/js/binduser.ui.js b/module/gitfox/js/binduser.ui.js new file mode 100644 index 0000000000..cf44da6dea --- /dev/null +++ b/module/gitfox/js/binduser.ui.js @@ -0,0 +1,49 @@ +window.setUserEmail = function() +{ + let email = ''; + const account = $(this).val(); + if(account && zentaoUsers[account]) email = zentaoUsers[account].email; + + $(this).closest('.dtable-cell').prev().find('.dtable-cell-content').text(email); +} + +window.renderGitfoxUser = function(result, {row}) +{ + const gitfoxID = row.data.gitfoxID; + result.push({html: ``}); + + return result; +} + +window.bindUser = function(e) +{ + const myDTable = $('#table-gitfox-binduser').zui('dtable'); + const formData = myDTable.$.getFormData(); + + var bindData = $('#table-gitfox-binduser').zui('dtable').$.props.data; + var postData = {}; + postData['gitfoxUserNames[]'] = []; + for(i in bindData) + { + postData['gitfoxUserNames[' + bindData[i].gitfoxID + ']'] = bindData[i].gitfoxUser; + postData['zentaoUsers[' + bindData[i].gitfoxID + ']'] = formData['zentaoUsers[' + bindData[i].gitfoxID + ']']; + } + + e.preventDefault(); + e.stopPropagation(); + $.ajaxSubmit({ + url: $.createLink('gitfox', 'bindUser', 'gitfoxID=' + gitfoxID + '&type=' + type), + data: postData + }); +} + +window.afterPageUpdate = function($target, info) +{ + if(info.name === 'dtable') + { + const dtable = zui.DTable.query('#table-gitfox-binduser'); + + /* Clear saved formData. */ + if(dtable) dtable.$.resetFormData(); + } +}; diff --git a/module/gitfox/model.php b/module/gitfox/model.php index 1aee2bddec..df5d532cc6 100644 --- a/module/gitfox/model.php +++ b/module/gitfox/model.php @@ -62,13 +62,20 @@ class gitfoxModel extends model * @access public * @return string|object */ - public function getApiRoot(int $gitfoxID): string|object + public function getApiRoot(int $gitfoxID, bool $sudo = true): string|object { $gitfox = $this->getByID($gitfoxID); if(!$gitfox || $gitfox->type != 'gitfox') return ''; + $sudoParam = ''; + if($sudo == true && !$this->app->user->admin) + { + $openID = $this->loadModel('pipeline')->getOpenIdByAccount($gitfoxID, 'gitfox', $this->app->user->account); + if($openID) $sudoParam = "&sudo={$openID}"; + } + $apiRoot = new stdclass; - $apiRoot->url = rtrim($gitfox->url, '/') . '/api/v1%s'; + $apiRoot->url = rtrim($gitfox->url, '/') . '/api/v1%s' . $sudoParam; $apiRoot->header = array('Authorization: Bearer ' . $gitfox->token); return $apiRoot; @@ -408,4 +415,141 @@ class gitfoxModel extends model if(!$response) dao::$errors[] = false; return false; } + + /** + * 获取最新用户。 + * Get current user. + * + * @param int $gitfoxID + * @access public + * @return object|array|null|false + */ + public function apiGetCurrentUser(int $gitfoxID): object|array|null|false + { + $apiRoot = $this->getApiRoot($gitfoxID); + $url = sprintf($apiRoot->url, "/user"); + return json_decode(commonModel::http($url, null, array(), $apiRoot->header)); + } + + /** + * 获取gitfox用户列表。 + * Get gitfox user list. + * + * @param int $gitfoxID + * @param bool $onlyLinked + * @param string $orderBy + * @access public + * @return array + */ + public function apiGetUsers(int $gitfoxID, bool $onlyLinked = false, string $orderBy = 'id_desc'): array + { + /* GitLab API '/users' can only return 20 users per page in default, so we use a loop to fetch all users. */ + $page = 1; + $perPage = 100; + $response = array(); + $apiRoot = $this->getApiRoot($gitfoxID); + + /* Get order data. */ + $orders = explode('_', $orderBy); + $order = array_pop($orders); + $sort = join('_', $orders); + + while(true) + { + /* Also use `per_page=20` to fetch users in API. Fetch active users only. */ + $url = sprintf($apiRoot->url, "/admin/users") . "?order={$order}&sort={$sort}&page={$page}&limit={$perPage}"; + $httpData = commonModel::http($url, null, array(), $apiRoot->header, 'data', 'GET', 30, true, false); + if(empty($httpData['body'])) break; + + $result = json_decode($httpData['body']); + if(!empty($result) && is_array($result)) + { + $response = array_merge($response, $result); + $page += 1; + + $resultPage = isset($httpData['header']['X-Page']) ? $httpData['header']['X-Page'] : $httpData['header']['x-page']; + $resultTotalPage = isset($httpData['header']['X-Total-Pages']) ? $httpData['header']['X-Total-Pages'] : $httpData['header']['x-total-pages']; + if($resultPage == $resultTotalPage) break; + } + else + { + break; + } + } + + if(!$response) return array(); + + /* Get linked users. */ + $linkedUsers = array(); + if($onlyLinked) $linkedUsers = $this->loadModel('pipeline')->getUserBindedPairs($gitfoxID, 'gitfox', 'openID,account'); + + $users = array(); + foreach($response as $gitfoxUser) + { + if($onlyLinked and !isset($linkedUsers[$gitfoxUser->id])) continue; + + $user = new stdclass; + $user->id = $gitfoxUser->uid; + $user->realname = $gitfoxUser->display_name; + $user->account = $gitfoxUser->uid; + $user->email = zget($gitfoxUser, 'email', ''); + $user->createdAt = zget($gitfoxUser, 'created', ''); + $user->lastActivityOn = zget($gitfoxUser, 'updated', ''); + + $users[] = $user; + } + + return $users; + } + + /** + * 获取匹配的gitfox用户列表。 + * Get matched gitfox users. + * + * @param int $gitfoxID + * @param array $gitfoxUsers + * @param array $zentaoUsers + * @access public + * @return array + */ + public function getMatchedUsers(int $gitfoxID, array $gitfoxUsers, array $zentaoUsers): array + { + $matches = new stdclass; + foreach($gitfoxUsers as $gitfoxUser) + { + foreach($zentaoUsers as $zentaoUser) + { + if($gitfoxUser->email == $zentaoUser->email) $matches->emails[$gitfoxUser->email][] = $zentaoUser->account; + if($gitfoxUser->account == $zentaoUser->account) $matches->accounts[$gitfoxUser->account][] = $zentaoUser->account; + if($gitfoxUser->realname == $zentaoUser->realname) $matches->names[$gitfoxUser->realname][] = $zentaoUser->account; + } + } + + $bindedUsers = $this->loadModel('pipeline')->getUserBindedPairs($gitfoxID, 'gitfox', 'openID,account'); + + $matchedUsers = array(); + foreach($gitfoxUsers as $gitfoxUser) + { + if(isset($bindedUsers[$gitfoxUser->id])) + { + $gitfoxUser->zentaoAccount = $bindedUsers[$gitfoxUser->id]; + $matchedUsers[$gitfoxUser->id] = $gitfoxUser; + continue; + } + + $matchedZentaoUsers = array(); + if(isset($matches->emails[$gitfoxUser->email])) $matchedZentaoUsers = array_merge($matchedZentaoUsers, $matches->emails[$gitfoxUser->email]); + if(isset($matches->names[$gitfoxUser->realname])) $matchedZentaoUsers = array_merge($matchedZentaoUsers, $matches->names[$gitfoxUser->realname]); + if(isset($matches->accounts[$gitfoxUser->account])) $matchedZentaoUsers = array_merge($matchedZentaoUsers, $matches->accounts[$gitfoxUser->account]); + + $matchedZentaoUsers = array_unique($matchedZentaoUsers); + if(count($matchedZentaoUsers) == 1) + { + $gitfoxUser->zentaoAccount = current($matchedZentaoUsers); + $matchedUsers[$gitfoxUser->id] = $gitfoxUser; + } + } + + return $matchedUsers; + } } diff --git a/module/gitfox/ui/binduser.html.php b/module/gitfox/ui/binduser.html.php new file mode 100644 index 0000000000..f8baa00a03 --- /dev/null +++ b/module/gitfox/ui/binduser.html.php @@ -0,0 +1,63 @@ + + * @package gitfox + * @link https://www.zentao.net + */ + +namespace zin; + +/* zin: Define the set::module('compile') feature bar on main menu. */ +featureBar +( + to::leading(array(backBtn(set::icon('back'), set::className('secondary'), $lang->goback))), + set::current($type), + set::link($this->createLink('gitfox', 'binduser', "gitfoxID=$gitfoxID&type={key}")) +); + +/* zin: Define the toolbar on main menu. */ +toolbar(); + +jsVar('zentaoUsers', $zentaoUsers); +jsVar('gitfoxID', $gitfoxID); +jsVar('type', $type); +$config->gitfox->dtable->bindUser->fieldList['gitfoxEmail']['onRenderCell'] = jsRaw('renderGitfoxUser'); +$config->gitfox->dtable->bindUser->fieldList['zentaoUsers']['controlItems'] = $userPairs; +form +( + setID('bindForm'), + setClass('mb-4 h-full'), + set::action(createLink('gitfox', 'bindUser', "gitfoxID={$gitfoxID}")), + set::actions(array()), + on::change('input[name^="zentaoUsers"]', 'setUserEmail'), + dtable + ( + set::cols($config->gitfox->dtable->bindUser->fieldList), + set::data($userList), + set::plugins(array('form')), + set::rowHeight(50), + set::showToolbarOnChecked(false), + set::footer(array('toolbar')), + set::rowKey('gitfoxID'), + set::footToolbar(array( + 'className' => 'w-full form-actions form-group no-label', + 'items' => array( + array( + 'text' => $lang->save, + 'btnType' => 'primary', + 'onClick' => jsRaw("bindUser") + ), + array( + 'text' => $lang->goback, + 'btnType' => 'info', + 'onClick' => jsRaw('() => {goBack()}') + ) + ) + )) + ) +); diff --git a/module/gitfox/zen.php b/module/gitfox/zen.php new file mode 100644 index 0000000000..3f26c98de6 --- /dev/null +++ b/module/gitfox/zen.php @@ -0,0 +1,81 @@ + + * @package gitfox + * @link https://www.zentao.net + */ +class gitfoxZen extends gitfox +{ + /** + * 检查绑定用户是否重复。 + * Check bind user repeat. + * + * @param array $zentaoUsers + * @param array $userPairs + * @access protected + * @return array + */ + protected function checkUserRepeat(array $zentaoUsers, array $userPairs): array + { + $accountList = array(); + $repeatUsers = array(); + foreach($zentaoUsers as $openID => $user) + { + if(empty($user)) continue; + if(isset($accountList[$user])) $repeatUsers[] = zget($userPairs, $user); + $accountList[$user] = $openID; + } + + if(count($repeatUsers)) return array('result' => 'fail', 'message' => sprintf($this->lang->gitfox->bindUserError, join(',', array_unique($repeatUsers)))); + return array('result' => 'success'); + } + + /** + * 绑定用户。 + * Bind user. + * + * @param int $gitfoxID + * @param array $users + * @param array $gitfoxNames + * @param array $zentaoUsers + * @access protected + * @return void + */ + protected function bindUsers(int $gitfoxID, array $users, array $gitfoxNames, array $zentaoUsers): void + { + $user = new stdclass; + $user->providerID = $gitfoxID; + $user->providerType = 'gitfox'; + + $oldUsers = $this->dao->select('*')->from(TABLE_OAUTH)->where('providerType')->eq($user->providerType)->andWhere('providerID')->eq($user->providerID)->fetchAll('openID'); + foreach($users as $openID => $account) + { + $existAccount = isset($oldUsers[$openID]) ? $oldUsers[$openID] : ''; + + if($existAccount and $existAccount->account != $account) + { + $this->dao->delete() + ->from(TABLE_OAUTH) + ->where('openID')->eq($openID) + ->andWhere('providerType')->eq($user->providerType) + ->andWhere('providerID')->eq($user->providerID) + ->exec(); + $this->loadModel('action')->create('gitfoxuser', (int)$openID, 'unbind', '', sprintf($this->lang->gitfox->bindDynamic, $gitfoxNames[$openID], $zentaoUsers[$existAccount->account]->realname)); + } + if(!$existAccount or $existAccount->account != $account) + { + if(!$account) continue; + $user->account = $account; + $user->openID = $openID; + $this->dao->insert(TABLE_OAUTH)->data($user)->exec(); + $this->loadModel('action')->create('gitfoxuser', (int)$openID, 'bind', '', sprintf($this->lang->gitfox->bindDynamic, $gitfoxNames[$openID], $zentaoUsers[$account]->realname)); + } + } + } +} + diff --git a/module/instance/model.php b/module/instance/model.php index acc9e73e85..a91003c3c0 100644 --- a/module/instance/model.php +++ b/module/instance/model.php @@ -964,7 +964,7 @@ class instanceModel extends model if(!common::hasPriv('instance', 'manage')) return false; if($action === 'edit') return true; - if($action == 'bindUser') return in_array($instance->appName, array('GitLab', 'Gitea', 'Gogs')); + if($action == 'bindUser') return in_array($instance->appName, array('GitLab', 'GitFox', 'Gitea', 'Gogs')); if($action == 'ajaxUninstall') return true; return false; } @@ -975,7 +975,7 @@ class instanceModel extends model if($action == 'visit') return !empty($instance->domain) && $this->canDo('visit', $instance); if($action == 'upgrade') return !empty($instance->latestVersion) && in_array($instance->status, array('stopped', 'running')); if($action == 'edit') return false; - if($action == 'bindUser') return $instance->status == 'running' && $instance->appName == 'GitLab'; + if($action == 'bindUser') return $instance->status == 'running' && in_array($instance->appName, array('GitLab', 'GitFox')); return true; } diff --git a/module/space/config.php b/module/space/config.php index 63703ccab6..71eee353ce 100644 --- a/module/space/config.php +++ b/module/space/config.php @@ -1,4 +1,4 @@ space = new stdclass(); -$config->space->zentaoApps = array('GitLab', 'Gitea', 'Gogs', 'Jenkins', 'SonarQube'); +$config->space->zentaoApps = array('GitLab', 'GitFox', 'Gitea', 'Gogs', 'Jenkins', 'SonarQube'); diff --git a/module/space/js/browse.ui.js b/module/space/js/browse.ui.js index 60eae87096..006e0119f5 100644 --- a/module/space/js/browse.ui.js +++ b/module/space/js/browse.ui.js @@ -84,7 +84,7 @@ window.onPageUnmount = function() window.bindUser = function(externalID, appName) { - openUrl($.createLink(appName.toLowerCase(), 'bindUser', 'id=' + externalID)); + loadPage($.createLink(appName.toLowerCase(), 'bindUser', 'id=' + externalID)); } window.editApp = function(externalID, appName)