Просмотр исходного кода

Ticket #5438 - GitHub integration. GitHub Apps.

AntonLV 5 месяцев назад
Родитель
Сommit
093a107273

+ 27 - 28
modules/boonex/github/classes/BxGitHubApi.php

@@ -43,14 +43,15 @@ class BxGitHubApi extends BxDol
         $this->_bInit = false;
     }
 
-    public function init($iProfileId)
+    public function init($sToken)
     {
-        $aSettings = $this->_oModule->_oDb->getSettings(['sample' => 'profile_id', 'profile_id' => $iProfileId]);
-        if(empty($aSettings) || !is_array($aSettings) || empty($aSettings['pat']))
+        if(!$sToken)
             return false;
 
-        $this->_sToken = $aSettings['pat'];
+        $this->_sToken = $sToken;
         $this->_bInit = true;
+
+        return true;
     }
 
     public function authenticate()
@@ -65,16 +66,23 @@ class BxGitHubApi extends BxDol
 
     public function getRepositories($sUsername)
     {
+        $aRepositories = [];
         if(!$this->authenticate())
-            return [];
+            return $aRepositories;
+
+        try {
+            $aRepositories = $this->_oClient->api('user')->repositories($sUsername);
+        } catch (Exception $ex) {
+            $this->_processException('Get Repositories:', $oException);
+        }
 
-        return $this->_oClient->api('user')->repositories($sUsername);
+        return $aRepositories;
     }
 
-    public function getIssues($sUsername, $sRepository, $sState = 'open', $bAuthenticate = true)
+    public function getIssues($sUsername, $sRepository, $sState = 'open')
     {
         $aIssues = [];
-        if($bAuthenticate && !$this->authenticate())
+        if(!$this->authenticate())
             return $aIssues;
 
         try {
@@ -87,10 +95,10 @@ class BxGitHubApi extends BxDol
         return $aIssues;
     }
 
-    public function getIssue($sUsername, $sRepository, $iIssue, $bAuthenticate = true)
+    public function getIssue($sUsername, $sRepository, $iIssue)
     {
         $aIssue = [];
-        if($bAuthenticate && !$this->authenticate())
+        if(!$this->authenticate() || !$iIssue)
             return $aIssue;
 
         try {
@@ -121,7 +129,7 @@ class BxGitHubApi extends BxDol
 
     public function updateIssue($sUsername, $sRepository, $iIssue, $aParams)
     {
-        if(!$this->authenticate() || !$aParams || !is_array($aParams))
+        if(!$this->authenticate() || !$iIssue || !$aParams || !is_array($aParams))
             return false;
 
         $aIssue = false;
@@ -136,10 +144,10 @@ class BxGitHubApi extends BxDol
         return $aIssue;
     }
 
-    public function createLabel($sUsername, $sRepository, $mixedLabel, $bAuthenticate = true)
+    public function createLabel($sUsername, $sRepository, $mixedLabel)
     {
         $aResult = [];
-        if($bAuthenticate && !$this->authenticate())
+        if(!$this->authenticate() || !$mixedLabel)
             return $aResult;
 
         return $this->_oClient->api('issue')->labels()->create($sUsername, $sRepository, is_array($mixedLabel) ? $mixedLabel : [
@@ -147,34 +155,25 @@ class BxGitHubApi extends BxDol
         ]);
     }
     
-    public function addLabel($sUsername, $sRepository, $iIssue, $mixedLabel)
+    public function addLabel($sUsername, $sRepository, $iIssue, $sLabel)
     {
-        if(!$this->authenticate() || !$mixedLabel)
+        if(!$this->authenticate() || !$iIssue || !$sLabel)
             return false;
 
-        return $this->_oClient->api('issue')->labels()->add($sUsername, $sRepository, $iIssue, is_array($mixedLabel) ? $mixedLabel : [
-            'name' => $mixedLabel
-        ]);
+        return $this->_oClient->api('issue')->labels()->add($sUsername, $sRepository, $iIssue, $sLabel);
     }
 
     protected function _processException($sMessage, &$oException)
     {
         $sError = $oException->getMessage();
 
-        $this->_log($sMessage . ' ' . $sError);
+        $this->_logError($sMessage . ' ' . $sError);
 
         return false;
     }
 
-    protected function _log($sContents)
+    protected function _logError($sContents)
     {
-        $CNF = &$this->_oModule->_oConfig->CNF;
-
-        if (is_array($sContents))
-            $sContents = var_export($sContents, true);	
-        else if (is_object($sContents))
-            $sContents = json_encode($sContents);
-
-        bx_log($CNF['OBJECT_LOG_API'], $sContents, BX_LOG_ERR);
+        $this->_oModule->log($sContents, BX_LOG_ERR);
     }
 }

+ 130 - 0
modules/boonex/github/classes/BxGitHubApp.php

@@ -0,0 +1,130 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defdroup    GitHub GitHub
+ * @indroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * 
+ * Register App
+ * https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app
+ * 
+ * Create "Login with GitHub" web flow.
+ * https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-a-login-with-github-button-with-a-github-app
+ * 
+ */
+
+class BxGitHubApp extends BxDol
+{
+    protected $_oModule;
+
+    protected $_sClientId;
+    protected $_sClientSecret;
+
+    protected $_sUrlAuthorize;
+    protected $_sUrlAccessToken;
+
+    protected $_bInit;
+
+    public function __construct(&$oModule)
+    {
+        $this->_oModule = $oModule;
+
+        $this->_sUrlAuthorize = 'https://github.com/login/oauth/authorize';
+        $this->_sUrlAccessToken = 'https://github.com/login/oauth/access_token';
+
+        $this->_bInit = false;
+    }
+
+    public function init($iAppId)
+    {
+        $aApp = $this->_oModule->_oDb->getApps(['sample' => 'id', 'id' => $iAppId]);
+        if(empty($aApp) || !is_array($aApp))
+            return false;
+
+        $this->_sClientId = $aApp['client_id'] ?? false;
+        $this->_sClientSecret = $aApp['client_secret'] ?? false;
+
+        $this->_bInit = $this->_sClientId && $this->_sClientSecret;
+
+        return true;
+    }
+
+    public function getUrlAuthorize()
+    {
+        return bx_append_url_params($this->_sUrlAuthorize, [
+            'client_id' => $this->_sClientId
+        ]);
+    }
+
+    public function getAccessToken($sCode)
+    {        
+        $mixedResponse = $this->_apiCallAccessToken($this->_sUrlAccessToken, [
+            'code' => $sCode
+        ]);
+
+        if($mixedResponse === false || empty($mixedResponse['access_token'])) {
+            $this->_oModule->log(['Get Access Token:', $mixedResponse], BX_LOG_ERR);
+
+            return false;
+        }
+
+        $aResult = [
+            'access_token' => $mixedResponse['access_token']
+        ];
+
+        if(isset($mixedResponse['expires_in'], $mixedResponse['refresh_token'], $mixedResponse['refresh_token_expires_in']))
+            $aResult = array_merge($aResult, [
+                'at_expires_in' => $mixedResponse['expires_in'],
+                'refresh_token' => $mixedResponse['refresh_token'],
+                'rt_expires_in' => $mixedResponse['refresh_token_expires_in']
+            ]);
+
+        return $aResult;
+    }
+
+    public function refreshAccessToken($sRefreshToken)
+    {
+        $mixedResponse = $this->_apiCallAccessToken($this->_sUrlAccessToken, [
+            'grant_type' => 'refresh_token',
+            'refresh_token' => $sRefreshToken
+        ]);
+
+        if($mixedResponse === false || !isset($mixedResponse['access_token'], $mixedResponse['refresh_token'])) {
+            $this->_oModule->log(['Refresh Access Token:', $mixedResponse], BX_LOG_ERR);
+
+            return false;
+        }
+
+        return [
+            'access_token' => $mixedResponse['access_token'],
+            'at_expires_in' => $mixedResponse['expires_in'],
+            'refresh_token' => $mixedResponse['refresh_token'],
+            'rt_expires_in' => $mixedResponse['refresh_token_expires_in']
+        ];
+    }
+
+    protected function _apiCallAccessToken($sEndpoint, $aParams = [])
+    {
+        $sResponse = $this->_apiCall($sEndpoint, array_merge([
+            'client_id' => $this->_sClientId,
+            'client_secret' => $this->_sClientSecret,
+        ], $aParams));
+
+        return !empty($sResponse) ? json_decode($sResponse, true) : false;
+    }
+
+    protected function _apiCall($sEndpoint, $aParams = [], $aHeaders = [], $sMethod = 'post', $aBasicAuth = [], &$sHttpCode = null)
+    {
+        $aHeaders = array_merge([
+            'Accept: application/json',
+        ], $aHeaders);
+
+        return bx_file_get_contents($sEndpoint, $aParams, $sMethod, $aHeaders, $sHttpCode, $aBasicAuth);
+    }
+}

+ 43 - 2
modules/boonex/github/classes/BxGitHubConfig.php

@@ -13,32 +13,73 @@ class BxGitHubConfig extends BxBaseModGeneralConfig
 {
     protected $_oDb;
 
+    protected $_sAuthorize;
+
     public function __construct($aModule)
     {
         parent::__construct($aModule);
 
         $this->CNF = array (
             // database tables
+            'TABLE_APPS' => $aModule['db_prefix'] . 'apps',
+            'TABLE_AUTHORIZATIONS' => $aModule['db_prefix'] . 'authorizations',
             'TABLE_SETTINGS' => $aModule['db_prefix'] . 'settings',
 
             // database fields
             'FIELD_' => '',
 
             // some params
-            'PARAM_' => 'bx_github_',
+            'PARAM_AUTHORIZE' => 'bx_github_authorize',
+
+            // page URIs
+            'URL_SETTINGS' => 'page.php?i=github-settings',
 
             // objects
+            'OBJECT_FORM_APPS' => 'bx_github_form_apps',
+            'OBJECT_FORM_APPS_DISPLAY_ADD' => 'bx_github_form_apps_add',
+            'OBJECT_FORM_APPS_DISPLAY_EDIT' => 'bx_github_form_apps_edit',
             'OBJECT_FORM_SETTINGS' => 'bx_github_form_settings',
             'OBJECT_FORM_SETTINGS_DISPLAY_EDIT' => 'bx_github_form_settings_edit',
+            'OBJECT_GRID_APPS' => 'bx_github_apps',
+            'OBJECT_GRID_AUTHORIZATIONS' => 'bx_github_authorizations',
             'OBJECT_LOG_API' => 'bx_github_api'
         );
+
+        $sHtmlPrefix = str_replace('_', '-', $this->_sName);
+        $this->_aHtmlIds = array_merge($this->_aHtmlIds, [
+            'apps_popup_add' => $sHtmlPrefix . '-popup-apps-add',
+            'apps_popup_edit' => $sHtmlPrefix . '-popup-apps-edit'
+        ]);
     }
 
     public function init(&$oDb)
     {
         $this->_oDb = &$oDb;
 
-        //NOTE: Some settings can be inited here.
+        $this->_sAuthorize = getParam($this->CNF['PARAM_AUTHORIZE']);
+    }
+
+    public function getAuthorize()
+    {
+        return $this->_sAuthorize;
+    }
+
+    public function isAuthorizeByApp()
+    {
+        return $this->_sAuthorize == BX_GITHUB_AUTHORIZE_APP;
+    }
+
+    public function isAuthorizeByPat()
+    {
+        return $this->_sAuthorize == BX_GITHUB_AUTHORIZE_PAT;
+    }
+
+    public function getHtmlIds($sKey = '')
+    {
+        if(empty($sKey))
+            return $this->_aHtmlIds;
+
+        return isset($this->_aHtmlIds[$sKey]) ? $this->_aHtmlIds[$sKey] : '';
     }
 }
 

+ 99 - 0
modules/boonex/github/classes/BxGitHubDb.php

@@ -20,6 +20,105 @@ class BxGitHubDb extends BxDolModuleDb
         $this->_oConfig = $oConfig;
     }
 
+    public function getApps($aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+
+        $sSelectClause = "*";
+        $sWhereClause = $sOrderByClause = "";
+
+        switch($aParams['sample']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = [
+                    'id' => $aParams['id']
+                ];
+
+                $sWhereClause .= " AND `id`=:id";
+                break;
+
+            case 'profile_id':
+                $aMethod['params'][1] = [
+                    'profile_id' => $aParams['profile_id']
+                ];
+
+                $sWhereClause .= " AND `profile_id`=:profile_id";
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_APPS'] . "` WHERE 1 " . $sWhereClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    public function getAuthorizations($aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+
+        $sSelectClause = "*";
+        $sWhereClause = $sOrderByClause = "";
+
+        switch($aParams['sample']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = [
+                    'id' => $aParams['id']
+                ];
+
+                $sWhereClause .= " AND `id`=:id";
+                break;
+
+            case 'profile_app_ids':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = [
+                    'profile_id' => $aParams['profile_id'],
+                    'app_id' => $aParams['app_id']
+                ];
+
+                $sWhereClause .= " AND `profile_id`=:profile_id AND `app_id`=:app_id";
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_AUTHORIZATIONS'] . "` WHERE 1 " . $sWhereClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    public function getAuthorization($iProfileId, $iAppId)
+    {
+        $aAuthorization = $this->getAuthorizations([
+            'sample' => 'profile_app_ids', 
+            'profile_id' => $iProfileId, 
+            'app_id' => $iAppId
+        ]);
+
+        return ($aAuthorization['access_token'] ?? false) ? $aAuthorization : false;
+    }
+
+    public function isAuthorization($iProfileId, $iAppId)
+    {
+        return $this->getAuthorization($iProfileId, $iAppId) !== false;
+    }
+
+    public function insertAuthorization($iProfileId, $iAppId, $aAccessToken)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $iNow = time();
+        $aItems = array_merge(['profile_id', 'app_id', 'added', 'changed'], array_keys($aAccessToken));
+
+        return $this->query("INSERT INTO `" . $CNF['TABLE_AUTHORIZATIONS'] . "`(`" . implode('`, `', $aItems) . "`) VALUES(:" . implode(', :', $aItems) . ") ON DUPLICATE KEY UPDATE `changed`=:changed, " . $this->arrayToSQL($aAccessToken), array_merge([
+            'profile_id' => $iProfileId,
+            'app_id' => $iAppId,
+            'added' => $iNow,
+            'changed' => $iNow
+        ], $aAccessToken));
+    }
+
     public function getSettings($aParams = [])
     {
         $CNF = &$this->_oConfig->CNF;

+ 26 - 0
modules/boonex/github/classes/BxGitHubFormApps.php

@@ -0,0 +1,26 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Payment Payment
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxGitHubFormApps extends BxTemplFormView
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    function __construct($aInfo, $oTemplate = false)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $this->_sModule = 'bx_github';
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+    }
+}
+
+/** @} */

+ 160 - 0
modules/boonex/github/classes/BxGitHubGridApps.php

@@ -0,0 +1,160 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT 
+ * @defgroup    Tasks Tasks
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxGitHubGridApps extends BxBaseModGeneralGrid
+{
+    protected $_iProfileId;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->_sModule = 'bx_github';
+
+        parent::__construct ($aOptions, $oTemplate);
+
+        if(($sFfName = 'profile_id') && ($iValue = bx_get($sFfName)) !== false) 
+            $this->setProfileId($iValue);
+    }
+
+    public function getFormBlockTitleAPI($sAction, $iId = 0)
+    {
+        $sResult = '';
+
+        switch($sAction) {
+            case 'add':
+                $sResult = _t('_bx_github_grid_popup_title_apps_add');
+                break;
+
+            case 'edit':
+                $sResult = _t('_bx_github_grid_popup_title_apps_edit');
+                break;
+        }
+
+        return $sResult;
+    }
+
+    public function getFormCallBackUrlAPI($sAction, $iId = 0)
+    {
+        return '/api.php?r=system/perfom_action_api/TemplServiceGrid/&params[]=&o=' . $this->_sObject . '&a=' . $sAction . '&id=' . $iId;
+    }
+
+    public function setProfileId($iProfileId)
+    {
+        $this->_iProfileId = (int)$iProfileId;
+        $this->_aQueryAppend['profile_id'] = $this->_iProfileId;
+    }
+
+    public function performActionAdd()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sAction = 'add';
+
+        $oForm = $this->_getFormObject($sAction, $CNF['OBJECT_FORM_APPS_DISPLAY_ADD']);
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            if(($iId = (int)$oForm->insert(['profile_id' => $this->_iProfileId, 'added' => time()])) != 0)
+                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
+            else
+                $aRes = ['msg' => _t('_bx_github_err_cannot_perform_action')];
+
+            return echoJson($aRes);
+        }
+
+        if($this->_bIsApi)
+            return $this->getFormBlockAPI($oForm, $sAction);
+
+        $sContent = BxTemplFunctions::getInstance()->popupBox($this->_oModule->_oConfig->getHtmlIds('apps_popup') . $sAction, _t('_bx_github_grid_popup_title_apps_' . $sAction), $this->_oModule->_oTemplate->parseHtmlByName('popup_apps.html', [
+            'form_id' => $oForm->aFormAttrs['id'],
+            'form' => $oForm->getCode(true),
+            'object' => $this->_sObject,
+            'action' => $sAction
+        ]));
+
+        return echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
+    }
+
+    public function performActionEdit()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sAction = 'edit';
+
+        $iId = $this->_getId();
+        $aItem = $this->_oModule->_oDb->getApps(['sample' => 'id', 'id' => $iId]);
+        if(!$aItem)
+            return echoJson([]);
+
+        $oForm = $this->_getFormObject($sAction, $CNF['OBJECT_FORM_APPS_DISPLAY_EDIT'], ['id' => $iId]);
+        $oForm->initChecker($aItem);
+        if($oForm->isSubmittedAndValid()) {
+            if($oForm->update($aItem['id']) !== false)
+                $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
+            else
+                $aRes = ['msg' => _t('_bx_github_err_cannot_perform_action')];
+
+            return echoJson($aRes);
+        }
+
+        if($this->_bIsApi)
+            return $this->getFormBlockAPI($oForm, $sAction, $iId);
+
+        $sContent = BxTemplFunctions::getInstance()->popupBox($this->_oModule->_oConfig->getHtmlIds('apps_popup') . $sAction, _t('_bx_github_grid_popup_title_apps_' . $sAction), $this->_oModule->_oTemplate->parseHtmlByName('popup_apps.html', [
+            'form_id' => $oForm->aFormAttrs['id'],
+            'form' => $oForm->getCode(true),
+            'object' => $this->_sObject,
+            'action' => $sAction
+        ]));
+
+        echoJson(['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]]);
+    }
+
+    protected function _getCellCallbackUrl($mixedValue, $sKey, $aField, $aRow)
+    {
+        $mixedValue = BX_DOL_URL_ROOT . $this->_oModule->_oConfig->getBaseUri() . 'authorize/' . $aRow['id'];
+
+    	return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        $this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString(' AND `profile_id`=?', $this->_iProfileId);
+
+        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+
+    protected function _getFormObject($sAction, $sDisplay, $aParams = [])
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+    	$oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_APPS'], $sDisplay);
+    	$oForm->setId($sDisplay);
+        $oForm->setName($sDisplay);
+        $oForm->setAction(BX_DOL_URL_ROOT . bx_append_url_params('grid.php', array_merge([
+            'o' => $this->_sObject, 
+            'a' => $sAction, 
+            'profile_id' => $this->_iProfileId
+        ], $aParams)));
+
+        return $oForm;
+    }
+
+    protected function _getId()
+    {
+        if(($aIds = bx_get('ids')) && is_array($aIds))
+            return reset($aIds);
+
+        if(($iId = bx_get('id')) !== false) 
+            return (int)$iId;
+
+        return false;
+    }
+}
+
+/** @} */

+ 100 - 0
modules/boonex/github/classes/BxGitHubGridAuthorizations.php

@@ -0,0 +1,100 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT 
+ * @defgroup    Tasks Tasks
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxGitHubGridAuthorizations extends BxBaseModGeneralGrid
+{
+    protected $_iProfileId;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->_sModule = 'bx_github';
+
+        parent::__construct ($aOptions, $oTemplate);
+
+        if(($sFfName = 'profile_id') && ($iValue = bx_get($sFfName)) !== false) 
+            $this->setProfileId($iValue);
+    }
+
+    public function setProfileId($iProfileId)
+    {
+        $this->_iProfileId = (int)$iProfileId;
+        $this->_aQueryAppend['profile_id'] = $this->_iProfileId;
+    }
+
+    public function performActionRefresh()
+    {
+        $iId = $this->_getId();
+
+        $aItem = $this->_oModule->_oDb->getAuthorizations(['sample' => 'id', 'id' => $iId]);
+        if(!$aItem)
+            return echoJson([]);
+
+        if($this->_oModule->refreshAccessToken($this->_iProfileId, $aItem['app_id'], $aItem['refresh_token']) !== false)
+            $aRes = ['grid' => $this->getCode(false), 'blink' => $iId];
+        else
+            $aRes = ['msg' => _t('_bx_github_err_cannot_perform_action')];
+
+        return echoJson($aRes);
+    }
+
+    protected function _getCellAppId($mixedValue, $sKey, $aField, $aRow)
+    {
+        if($mixedValue)
+            $mixedValue = $aRow['app_title'] ?: _t('_undefined');
+
+    	return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellAdded($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_getCellDefaultDateTime($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellChanged($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_getCellDefaultDateTime($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellAtExpiresIn($mixedValue, $sKey, $aField, $aRow)
+    {
+        if(!$mixedValue)
+            return parent::_getCellDefault(_t('_never'), $sKey, $aField, $aRow);
+
+        return $this->_getCellDefaultDateTime($aRow['changed'] + $mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellDefaultDateTime($mixedValue, $sKey, $aField, $aRow)
+    {
+        if($this->_bIsApi)
+            return ['type' => 'time', 'data' => $mixedValue];
+
+        return parent::_getCellDefault(bx_time_js($mixedValue, BX_FORMAT_DATE_TIME, true), $sKey, $aField, $aRow);
+    }
+
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        $this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString(' AND `tan`.`profile_id`=?', $this->_iProfileId);
+
+        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+
+    protected function _getId()
+    {
+        if(($aIds = bx_get('ids')) && is_array($aIds))
+            return reset($aIds);
+
+        if(($iId = bx_get('id')) !== false) 
+            return (int)$iId;
+
+        return false;
+    }    
+}
+
+/** @} */

+ 258 - 28
modules/boonex/github/classes/BxGitHubModule.php

@@ -9,6 +9,9 @@
  * @{
  */
 
+define('BX_GITHUB_AUTHORIZE_APP', 'app');
+define('BX_GITHUB_AUTHORIZE_PAT', 'pat');
+
 class BxGitHubModule extends BxBaseModGeneralModule
 {
     protected $_iLoggedId;
@@ -20,21 +23,137 @@ class BxGitHubModule extends BxBaseModGeneralModule
         parent::__construct($aModule);
 
         $this->_oConfig->init($this->_oDb);
-        
+
         $this->_iLoggedId = bx_get_logged_profile_id();
 
-        bx_import('Api', $aModule);
-        $this->_oApi = new BxGitHubApi($this);
-        $this->_oApi->init($this->_iLoggedId);
+        $this->_oApi = $this->getObjectApi();
+        /*
+         * Below authorization using profile ID only is used 
+         * when "Personal Access Token" authorization is enabled.
+         */
+        if($this->_iLoggedId && ($sAccessToken = $this->getAccessToken($this->_iLoggedId)) !== false)
+            $this->_oApi->init($sAccessToken);
     }
 
     /**
      * ACTION METHODS
      */
+    public function actionAuthorize($iAppId)
+    {
+        $CNF = &$this->_oConfig->CNF;
 
+        if(($oApp = $this->getObjectApp($iAppId)) !== false && ($sCode = bx_get('code')) !== false) {
+            $aAccessToken = $oApp->getAccessToken($sCode);
+            if($aAccessToken !== false) {
+                $this->_oDb->insertAuthorization($this->_iLoggedId, $iAppId, $aAccessToken);
+
+                header('Location: ' . BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($CNF['URL_SETTINGS']));
+                exit;
+            }
+        }
+
+        return $this->_oTemplate->displayErrorOccured();
+    }
+    
     /**
      * SERVICE METHODS
      */
+    public function serviceGetOptionsAuthorize()
+    {
+        $aResult = [];
+        foreach(['pat', 'app'] as $sItem)
+            $aResult[] = [
+                'key' => $sItem, 
+                'value' => _t('_bx_github_option_authorize_' . $sItem)
+            ];
+
+        return $aResult;
+    }
+
+    public function serviceGetApps($iProfileId)
+    {
+        if(!$this->_oConfig->isAuthorizeByApp())
+            return false;
+
+        if(!$iProfileId)
+            return [];
+
+        return $this->_oDb->getApps(['sample' => 'profile_id', 'profile_id' => $iProfileId]);
+    }
+
+    public function serviceRefreshAccessToken($iAppId, $iProfileId = 0)
+    {
+        if(!$this->_oConfig->isAuthorizeByApp())
+            return false;
+
+        if(!$iProfileId && !($iProfileId = $this->_iLoggedId))
+            return false;
+
+        return $this->refreshAccessToken($iProfileId, $iAppId);
+    }
+
+    public function serviceGetBlockAuthorize($iAppId, $iProfileId = 0)
+    {
+        if(!$this->_oConfig->isAuthorizeByApp())
+            return '';
+
+        if(!$iProfileId && !($iProfileId = $this->_iLoggedId))
+            return '';
+
+        return $this->_oTemplate->getBlockAuthorize($iAppId, $iProfileId);
+    }
+
+    public function serviceGetBlockApps($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId || ($iProfileId != $this->_iLoggedId && !$this->_isAdministrator()))
+            $iProfileId = $this->_iLoggedId;
+
+        if(!$iProfileId)
+            return MsgBox(_t('_sys_txt_access_denied'));
+
+        if(($mixedCheck = $this->checkAllowedAddApp($iProfileId)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox($mixedCheck);
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_APPS']);
+        if(!$oGrid)
+            return $this->_bIsApi ? [] : '';
+
+        $oGrid->setProfileId($iProfileId);
+
+        if($this->_bIsApi)
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+
+        return $oGrid->getCode();
+    }
+
+    public function serviceGetBlockAuthorizations($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId || ($iProfileId != $this->_iLoggedId && !$this->_isAdministrator()))
+            $iProfileId = $this->_iLoggedId;
+
+        if(!$iProfileId)
+            return MsgBox(_t('_sys_txt_access_denied'));
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_AUTHORIZATIONS']);
+        if(!$oGrid)
+            return $this->_bIsApi ? [] : '';
+
+        $oGrid->setProfileId($iProfileId);
+
+        if($this->_bIsApi)
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+
+        return $oGrid->getCode();
+    }
+
     public function serviceGetBlockSettings($iProfileId = 0)
     {
         if(!$iProfileId || ($iProfileId != $this->_iLoggedId && !$this->_isAdministrator()))
@@ -43,77 +162,188 @@ class BxGitHubModule extends BxBaseModGeneralModule
         if(!$iProfileId)
             return MsgBox(_t('_sys_txt_access_denied'));
 
-        return $this->_oTemplate->getBlockSettings($iProfileId);
+        return $this->_oTemplate->getBlockSettingsClient($iProfileId);
     }
 
-    public function serviceGetRepositories($sUsername, $iProfileId = 0)
+    public function serviceGetRepositories($sUsername, $mixedAuth)
     {
-        if($iProfileId && $iProfileId != $this->_iLoggedId)
-            $this->_oApi->init($iProfileId);
+        $this->_authenticate($mixedAuth);
 
         return $this->_oApi->getRepositories($sUsername);
     }
 
-    public function serviceGetIssues($sUsername, $sRepository, $sState = 'open')
+    public function serviceGetIssues($sUsername, $sRepository, $mixedAuth, $sState = 'open')
     {
+        $this->_authenticate($mixedAuth);
+
         return $this->_oApi->getIssues($sUsername, $sRepository, $sState);
     }
 
-    public function serviceGetIssue($sUsername, $sRepository, $iIssue)
+    public function serviceGetIssue($sUsername, $sRepository, $mixedAuth, $iIssue)
     {
+        $this->_authenticate($mixedAuth);
+
         return $this->_oApi->getIssue($sUsername, $sRepository, $iIssue);
     }
 
-    public function serviceCreateIssue($sUsername, $sRepository, $sTitle, $sText = '', $aParams = [], $iProfileId = 0)
+    public function serviceCreateIssue($sUsername, $sRepository, $mixedAuth, $sTitle, $sText = '', $aParams = [])
     {
-        if($iProfileId && $iProfileId != $this->_iLoggedId)
-            $this->_oApi->init($iProfileId);
+        $this->_authenticate($mixedAuth);
 
         return $this->_oApi->createIssue($sUsername, $sRepository, $sTitle, $sText, $aParams);
     }
 
-    public function serviceUpdateIssue($sUsername, $sRepository, $iIssue, $aParams, $iProfileId = 0)
+    public function serviceUpdateIssue($sUsername, $sRepository, $mixedAuth, $iIssue, $aParams)
     {
-        if($iProfileId && $iProfileId != $this->_iLoggedId)
-            $this->_oApi->init($iProfileId);
+        $this->_authenticate($mixedAuth);
 
         return $this->_oApi->updateIssue($sUsername, $sRepository, $iIssue, $aParams);
     }
 
-    public function serviceCloseIssue($sUsername, $sRepository, $iIssue, $iProfileId = 0)
+    public function serviceCloseIssue($sUsername, $sRepository, $mixedAuth, $iIssue)
     {
-        if($iProfileId && $iProfileId != $this->_iLoggedId)
-            $this->_oApi->init($iProfileId);
+        $this->_authenticate($mixedAuth);
 
         return $this->_oApi->updateIssue($sUsername, $sRepository, $iIssue, ['state' => 'closed']);
     }
 
-    public function serviceReopenIssue($sUsername, $sRepository, $iIssue, $iProfileId = 0)
+    public function serviceReopenIssue($sUsername, $sRepository, $mixedAuth, $iIssue)
     {
-        if($iProfileId && $iProfileId != $this->_iLoggedId)
-            $this->_oApi->init($iProfileId);
+        $this->_authenticate($mixedAuth);
 
         return $this->_oApi->updateIssue($sUsername, $sRepository, $iIssue, ['state' => 'open']);
     }
 
-    public function serviceCreateLabel($sUsername, $sRepository, $mixedLabel, $iProfileId = 0)
+    public function serviceCreateLabel($sUsername, $sRepository, $mixedAuth, $mixedLabel)
     {
-        if($iProfileId && $iProfileId != $this->_iLoggedId)
-            $this->_oApi->init($iProfileId);
+        $this->_authenticate($mixedAuth);
 
         return $this->_oApi->createLabel($sUsername, $sRepository, $mixedLabel);
     }
 
-    public function serviceAddLabel($sUsername, $sRepository, $iIssue, $mixedLabel, $iProfileId = 0)
+    public function serviceAddLabel($sUsername, $sRepository, $mixedAuth, $iIssue, $mixedLabel)
     {
-        if($iProfileId && $iProfileId != $this->_iLoggedId)
-            $this->_oApi->init($iProfileId);
+        $this->_authenticate($mixedAuth);
 
         return $this->_oApi->addLabel($sUsername, $sRepository, $iIssue, $mixedLabel);
     }
+
     /*
      * COMMON METHODS
      */
+    public function getObjectApi()
+    {
+        bx_import('Api', $this->_aModule);
+        return new BxGitHubApi($this);
+    }
+
+    public function getObjectApp($iAppId)
+    {
+        bx_import('App', $this->_aModule);
+        $oApp = new BxGitHubApp($this);
+        return $oApp->init($iAppId) ? $oApp : false;
+    }
+
+    public function getAccessToken($iProfileId, $iAppId = 0)
+    {
+        $sToken = '';
+
+        if($this->_oConfig->isAuthorizeByApp()) {
+            if(!$iAppId)
+                return false;
+
+            $aAuthorization = $this->_oDb->getAuthorization($iProfileId, $iAppId);
+            if($aAuthorization === false)
+                return false;
+
+            if(($iExpiresIn = (int)$aAuthorization['at_expires_in']) != 0 && ((int)$aAuthorization['changed'] + $iExpiresIn) < time()) {
+                $this->refreshAccessToken($iProfileId, $iAppId, $aAuthorization['refresh_token']);
+
+                $aAuthorization = $this->_oDb->getAuthorization($iProfileId, $iAppId);
+                if($aAuthorization === false)
+                    return false;
+            }
+
+            $sToken = $aAuthorization['access_token'];
+        }
+        else {
+            $aSettings = $this->_oDb->getSettings(['sample' => 'profile_id', 'profile_id' => $iProfileId]);
+            if(empty($aSettings) || !is_array($aSettings) || empty($aSettings['pat']))
+                return false;
+
+            $sToken = $aSettings['pat'];
+        }
+
+        return $sToken;
+    }
+
+    public function refreshAccessToken($iProfileId, $iAppId, $sRefreshToken = '')
+    {
+        $oApp = $this->getObjectApp($iAppId);
+        if(!$oApp)
+            return false;
+
+        if(!$sRefreshToken) {
+            $aAuthorization = $this->_oDb->getAuthorizations([
+                'sample' => 'profile_app_ids',
+                'profile_id' => $iProfileId,
+                'app_id' => $iAppId
+            ]);
+            if(!$aAuthorization || !is_array($aAuthorization) || empty($aAuthorization['refresh_token']))
+                return false;
+
+            $sRefreshToken = $aAuthorization['refresh_token'];
+        }
+
+        $aAccessToken = $oApp->refreshAccessToken($sRefreshToken);
+        if(!$aAccessToken)
+            return false;
+
+        return $this->_oDb->insertAuthorization($iProfileId, $iAppId, $aAccessToken) !== false;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedAddApp($iProfileId, $isPerformAction = false)
+    {
+        $aCheck = checkActionModule($iProfileId, 'add app', $this->getName(), $isPerformAction);
+
+        return $aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED ? $aCheck[CHECK_ACTION_MESSAGE] : CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function log($sContents, $iLevel = BX_LOG_DEBUG)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if (is_array($sContents))
+            $sContents = var_export($sContents, true);	
+        else if (is_object($sContents))
+            $sContents = json_encode($sContents);
+
+        bx_log($CNF['OBJECT_LOG_API'], $sContents, $iLevel);
+    }
+
+    protected function _authenticate($mixedAuth)
+    {
+        if(!$mixedAuth)
+            return;
+
+        $iProfileId = $iAppId = 0;
+        if(is_array($mixedAuth) && ($iAuthParams = count($mixedAuth)))
+            if($iAuthParams == 2)
+                list($iProfileId, $iAppId) = $mixedAuth;
+            else
+                $iProfileId = (int)reset($mixedAuth);
+        else
+            $iProfileId = (int)$mixedAuth;
+
+        if(($iProfileId && $iProfileId != $this->_iLoggedId) || $iAppId) {
+            $sAccessToken = $this->getAccessToken($iProfileId, $iAppId);
+            if($sAccessToken !== false)
+                $this->_oApi->init($sAccessToken);
+        }
+    }
 }
 
 /** @} */

+ 27 - 1
modules/boonex/github/classes/BxGitHubTemplate.php

@@ -16,7 +16,33 @@ class BxGitHubTemplate extends BxDolModuleTemplate
         parent::__construct($oConfig, $oDb);
     }
 
-    public function getBlockSettings($iProfileId)
+    public function getBlockAuthorize($iAppId, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oApp = $this->_oModule->getObjectApp($iAppId);
+        if(!$oApp)
+            return '';
+
+        $bAuthorized = $this->_oDb->isAuthorization($iProfileId, $iAppId);
+
+        return $this->parseHtmlByName('authorize.html', [
+            'bx_if:show_authorize' => [
+                'condition' => !$bAuthorized,
+                'content' => [
+                    'authorize_url' => $oApp->getUrlAuthorize()
+                ]
+            ],
+            'bx_if:show_authorized' => [
+                'condition' => $bAuthorized,
+                'content' => [
+                    'manage_url' => BxDolPermalinks::getInstance()->permalink($CNF['URL_SETTINGS'])
+                ]
+            ]
+        ]);
+    }
+
+    public function getBlockSettingsClient($iProfileId)
     {
         $CNF = &$this->_oConfig->CNF;
 

+ 46 - 5
modules/boonex/github/install/langs/en.xml

@@ -2,11 +2,24 @@
 <resources name="en" flag="gb" title="English">
     <string name="_bx_github"><![CDATA[GitHub]]></string>
 
-    <string name="_bx_github_option_"><![CDATA[]]></string>
+    <string name="_bx_github_option_authorize"><![CDATA[Authorize using]]></string>
+    <string name="_bx_github_option_authorize_pat"><![CDATA[Personal access token (PAT)]]></string>
+    <string name="_bx_github_option_authorize_app"><![CDATA[Login with GitHub (APP)]]></string>
+
+    <string name="_bx_github_form_apps"><![CDATA[Apps]]></string>
+    <string name="_bx_github_form_apps_display_add"><![CDATA[Add apps]]></string>
+    <string name="_bx_github_form_apps_display_edit"><![CDATA[Edit apps]]></string>
+    <string name="_bx_github_form_apps_input_sys_title"><![CDATA[Title]]></string>
+    <string name="_bx_github_form_apps_input_title"><![CDATA[Title]]></string>
+    <string name="_bx_github_form_apps_input_sys_client_id"><![CDATA[Client ID]]></string>
+    <string name="_bx_github_form_apps_input_client_id"><![CDATA[Client ID]]></string>
+    <string name="_bx_github_form_apps_input_sys_client_secret"><![CDATA[Client Secret]]></string>
+    <string name="_bx_github_form_apps_input_client_secret"><![CDATA[Client Secret]]></string>
+    <string name="_bx_github_form_apps_input_sys_do_submit"><![CDATA[Submit]]></string>
+    <string name="_bx_github_form_apps_input_do_submit"><![CDATA[Submit]]></string>
 
     <string name="_bx_github_form_settings"><![CDATA[Settings]]></string>
-    <string name="_bx_github_form_settings_display_edit"><![CDATA[Edit Settings]]></string>
-    <string name="_bx_github_form_settings_input_sys_profile_id"><![CDATA[Profile ID]]></string>
+    <string name="_bx_github_form_settings_display_edit"><![CDATA[Edit settings]]></string>
     <string name="_bx_github_form_settings_input_sys_pat"><![CDATA[Access token]]></string>
     <string name="_bx_github_form_settings_input_pat"><![CDATA[Access token]]></string>
     <string name="_bx_github_form_settings_input_sys_do_submit"><![CDATA[Submit]]></string>
@@ -15,14 +28,42 @@
 
     <string name="_bx_github_page_title_sys_settings"><![CDATA[Settings]]></string>
     <string name="_bx_github_page_title_settings"><![CDATA[GitHub Settings]]></string>
-    <string name="_bx_github_page_block_title_settings"><![CDATA[GitHub Settings]]></string>
+    <string name="_bx_github_page_block_title_apps"><![CDATA[GitHub Apps for repository provider]]></string>
+    <string name="_bx_github_page_block_title_authorizations"><![CDATA[GitHub Apps' authorizations for repository user]]></string>
+    <string name="_bx_github_page_block_title_settings"><![CDATA[GitHub Settings for repository user]]></string>
+    <string name="_bx_github_page_title_sys_authorize"><![CDATA[Authorize]]></string>
+    <string name="_bx_github_page_title_authorize"><![CDATA[GitHub Authorize]]></string>
+    <string name="_bx_github_page_block_title_authorize"><![CDATA[GitHub Authorize]]></string>
 
     <string name="_bx_github_menu_item_title_system_settings"><![CDATA[GitHub Settings]]></string>
     <string name="_bx_github_menu_item_title_settings"><![CDATA[GitHub]]></string>
 
+    <string name="_bx_github_grid_column_title_apps_title"><![CDATA[Title]]></string>
+    <string name="_bx_github_grid_column_title_apps_client_id"><![CDATA[Client ID]]></string>
+    <string name="_bx_github_grid_column_title_apps_callback_url"><![CDATA[Callback URL]]></string>
+    <string name="_bx_github_grid_action_title_apps_add"><![CDATA[Add]]></string>
+    <string name="_bx_github_grid_action_title_apps_edit"><![CDATA[Edit]]></string>
+    <string name="_bx_github_grid_action_title_apps_delete"><![CDATA[Delete]]></string>
+    <string name="_bx_github_grid_popup_title_apps_add"><![CDATA[Add App]]></string>
+    <string name="_bx_github_grid_popup_title_apps_edit"><![CDATA[Edit App]]></string>
+
+    <string name="_bx_github_grid_column_title_auth_app_id"><![CDATA[GitHub App]]></string>
+    <string name="_bx_github_grid_column_title_auth_added"><![CDATA[Added]]></string>
+    <string name="_bx_github_grid_column_title_auth_changed"><![CDATA[Changed]]></string>
+    <string name="_bx_github_grid_column_title_auth_at_expires_in"><![CDATA[Expires in]]></string>
+    <string name="_bx_github_grid_action_title_apps_refresh"><![CDATA[Refresh access token]]></string>
+    <string name="_bx_github_grid_action_title_auth_delete"><![CDATA[Delete]]></string>
+
+    <string name="_bx_github_acl_action_add_app"><![CDATA[Add GitHub app]]></string>
+
     <string name="_bx_github_log_api"><![CDATA[GitHub API Logs]]></string>
 
     <string name="_bx_github_msg_save_settings"><![CDATA[Settings were successfully saved.]]></string>
     <string name="_bx_github_err_save_settings"><![CDATA[Cannot save settings.]]></string>
-    <string name="_bx_github_txt_"><![CDATA[]]></string>
+    <string name="_bx_github_err_cannot_perform_action"><![CDATA[An error appeared. Please report.]]></string>
+
+    <string name="_bx_github_txt_login"><![CDATA[Login with GitHub]]></string>
+    <string name="_bx_github_txt_authorized"><![CDATA[Authorized]]></string>
+    <string name="_bx_github_txt_manage_here"><![CDATA[Manage your authorizations here:]]></string>
+    <string name="_bx_github_txt_settings"><![CDATA[GitHub settings]]></string>
 </resources>

+ 12 - 1
modules/boonex/github/install/sql/disable.sql

@@ -7,10 +7,21 @@ DELETE FROM `tot`, `toc`, `to` USING `sys_options_types` AS `tot` LEFT JOIN `sys
 
 -- PAGES & BLOCKS
 DELETE FROM `sys_objects_page` WHERE `module` = @sName;
-DELETE FROM `sys_pages_blocks` WHERE `module` = @sName OR `object` IN ('bx_github_settings');
+DELETE FROM `sys_pages_blocks` WHERE `module` = @sName OR `object` IN ('bx_github_settings', 'bx_github_authorize');
 
 
 -- MENU
 DELETE FROM `sys_objects_menu` WHERE `module` = @sName;
 DELETE FROM `sys_menu_sets` WHERE `module` = @sName;
 DELETE FROM `sys_menu_items` WHERE `module` = @sName; -- OR `set_name` IN('');
+
+
+-- ACL
+DELETE `sys_acl_actions`, `sys_acl_matrix` FROM `sys_acl_actions`, `sys_acl_matrix` WHERE `sys_acl_matrix`.`IDAction` = `sys_acl_actions`.`ID` AND `sys_acl_actions`.`Module` = @sName;
+DELETE FROM `sys_acl_actions` WHERE `Module` = @sName;
+
+
+-- GRIDS
+DELETE FROM `sys_objects_grid` WHERE `object` LIKE 'bx_github_%';
+DELETE FROM `sys_grid_fields` WHERE `object` LIKE 'bx_github_%';
+DELETE FROM `sys_grid_actions` WHERE `object` LIKE 'bx_github_%';

+ 78 - 10
modules/boonex/github/install/sql/enable.sql

@@ -3,17 +3,17 @@ SET @sName = 'bx_github';
 
 -- SETTINGS
 
--- SET @iTypeOrder = (SELECT MAX(`order`) FROM `sys_options_types` WHERE `group` = 'modules');
--- INSERT INTO `sys_options_types`(`group`, `name`, `caption`, `icon`, `order`) VALUES 
--- ('modules', @sName, '_bx_github', 'bx_github@modules/boonex/github/|std-icon.svg', IF(ISNULL(@iTypeOrder), 1, @iTypeOrder + 1));
--- SET @iTypeId = LAST_INSERT_ID();
+SET @iTypeOrder = (SELECT MAX(`order`) FROM `sys_options_types` WHERE `group` = 'modules');
+INSERT INTO `sys_options_types`(`group`, `name`, `caption`, `icon`, `order`) VALUES 
+('modules', @sName, '_bx_github', 'bx_github@modules/boonex/github/|std-icon.svg', IF(ISNULL(@iTypeOrder), 1, @iTypeOrder + 1));
+SET @iTypeId = LAST_INSERT_ID();
 
--- INSERT INTO `sys_options_categories` (`type_id`, `name`, `caption`, `order`)
--- VALUES (@iTypeId, @sName, '_bx_github', 10);
--- SET @iCategId = LAST_INSERT_ID();
+INSERT INTO `sys_options_categories` (`type_id`, `name`, `caption`, `order`)
+VALUES (@iTypeId, @sName, '_bx_github', 10);
+SET @iCategId = LAST_INSERT_ID();
 
--- INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `extra`, `check`, `check_error`, `order`) VALUES
--- ('bx_github_badge_persons', '', @iCategId, '_bx_github_option_badge_persons', 'select', 'a:3:{s:6:"module";s:9:"bx_github";s:6:"method";s:17:"get_options_badge";s:6:"params";a:1:{i:0;s:10:"bx_persons";}}', '', '', 10);
+INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `extra`, `check`, `check_error`, `order`) VALUES
+('bx_github_authorize', 'app', @iCategId, '_bx_github_option_authorize', 'select', 'a:2:{s:6:"module";s:9:"bx_github";s:6:"method";s:21:"get_options_authorize";}', '', '', 10);
 
 
 -- PAGES
@@ -23,7 +23,16 @@ INSERT INTO `sys_objects_page`(`object`, `title_system`, `title`, `module`, `lay
 ('bx_github_settings', '_bx_github_page_title_sys_settings', '_bx_github_page_title_settings', @sName, 5, 2147483647, 1, 'github-settings', 'page.php?i=github-settings', '', '', '', 0, 1, 0, 'BxGitHubPageSettings', 'modules/boonex/github/classes/BxGitHubPageSettings.php');
 
 INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES
-('bx_github_settings', 1, @sName, '_bx_github_page_block_title_settings', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:9:"bx_github";s:6:"method";s:18:"get_block_settings";}', 0, 0, 1);
+('bx_github_settings', 1, @sName, '_bx_github_page_block_title_apps', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:9:"bx_github";s:6:"method";s:14:"get_block_apps";}', 0, 0, 1),
+('bx_github_settings', 1, @sName, '_bx_github_page_block_title_authorizations', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:9:"bx_github";s:6:"method";s:24:"get_block_authorizations";}', 0, 0, 2),
+('bx_github_settings', 1, @sName, '_bx_github_page_block_title_settings', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:9:"bx_github";s:6:"method";s:18:"get_block_settings";}', 0, 0, 3);
+
+-- PAGE: authorize
+INSERT INTO `sys_objects_page`(`object`, `title_system`, `title`, `module`, `layout_id`, `visible_for_levels`, `visible_for_levels_editable`, `uri`, `url`, `meta_description`, `meta_keywords`, `meta_robots`, `cache_lifetime`, `cache_editable`, `deletable`, `override_class_name`, `override_class_file`) VALUES
+('bx_github_authorize', '_bx_github_page_title_sys_authorize', '_bx_github_page_title_authorize', @sName, 5, 2147483647, 1, 'github-authorize', 'page.php?i=github-authorize', '', '', '', 0, 1, 0, '', '');
+
+INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `order`) VALUES
+('bx_github_authorize', 1, @sName, '_bx_github_page_block_title_authorize', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:9:"bx_github";s:6:"method";s:19:"get_block_authorize";}', 0, 0, 2);
 
 
 -- MENUS
@@ -32,3 +41,62 @@ INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title`, `designb
 SET @iMoAccountSettings = (SELECT IFNULL(MAX(`order`), 0) FROM `sys_menu_items` WHERE `set_name`='sys_account_settings' AND `order` < 9999 LIMIT 1);
 INSERT INTO `sys_menu_items` (`set_name`, `module`, `name`, `title_system`, `title`, `link`, `onclick`, `target`, `icon`, `addon`, `submenu_object`, `visible_for_levels`, `active`, `copyable`, `editable`, `order`) VALUES
 ('sys_account_settings', @sName, 'github-settings', '_bx_github_menu_item_title_system_settings', '_bx_github_menu_item_title_settings', 'page.php?i=github-settings', '', '_self', 'github', '', '', 2147483646, 1, 0, 1, @iMoAccountSettings + 1);
+
+
+-- ACL
+INSERT INTO `sys_acl_actions` (`Module`, `Name`, `AdditionalParamName`, `Title`, `Desc`, `Countable`, `DisabledForLevels`) VALUES
+(@sName, 'add app', NULL, '_bx_github_acl_action_add_app', '', 1, 3);
+SET @iIdActionAddRepository = LAST_INSERT_ID();
+
+SET @iUnauthenticated = 1;
+SET @iAccount = 2;
+SET @iStandard = 3;
+SET @iUnconfirmed = 4;
+SET @iPending = 5;
+SET @iSuspended = 6;
+SET @iModerator = 7;
+SET @iAdministrator = 8;
+SET @iPremium = 9;
+
+INSERT INTO `sys_acl_matrix` (`IDLevel`, `IDAction`) VALUES
+
+-- add repository
+(@iModerator, @iIdActionAddRepository),
+(@iAdministrator, @iIdActionAddRepository),
+(@iPremium, @iIdActionAddRepository);
+
+
+-- GRIDS: apps
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `visible_for_levels`, `override_class_name`, `override_class_file`) VALUES
+('bx_github_apps', 'Sql', 'SELECT * FROM `bx_github_apps` WHERE 1 ', 'bx_github_apps', 'id', '', '', '', 20, NULL, 'start', '', 'title,client_id', '', 'like', '', '', 2147483647, 'BxGitHubGridApps', 'modules/boonex/github/classes/BxGitHubGridApps.php');
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
+('bx_github_apps', 'checkbox', '_sys_select', '5%', 0, 0, '', 1),
+('bx_github_apps', 'title', '_bx_github_grid_column_title_apps_title', '30%', 0, 16, '', 2),
+('bx_github_apps', 'client_id', '_bx_github_grid_column_title_apps_client_id', '25%', 0, 20, '', 3),
+('bx_github_apps', 'callback_url', '_bx_github_grid_column_title_apps_callback_url', '20%', 0, 0, '', 4),
+('bx_github_apps', 'actions', '', '20%', 0, 0, '', 5);
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('bx_github_apps', 'independent', 'add', '_bx_github_grid_action_title_apps_add', '', 0, 0, 1),
+('bx_github_apps', 'bulk', 'delete', '_bx_github_grid_action_title_apps_delete', '', 0, 1, 1),
+('bx_github_apps', 'single', 'edit', '_bx_github_grid_action_title_apps_edit', 'pencil-alt', 1, 0, 1),
+('bx_github_apps', 'single', 'delete', '_bx_github_grid_action_title_apps_delete', 'remove', 1, 1, 2);
+
+
+-- GRIDS: authorizations
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `visible_for_levels`, `override_class_name`, `override_class_file`) VALUES
+('bx_github_authorizations', 'Sql', 'SELECT `tan`.*, `tap`.`title` AS `app_title` FROM `bx_github_authorizations` AS `tan` INNER JOIN `bx_github_apps` AS `tap` ON `tan`.`app_id`=`tap`.`id`  WHERE 1 ', 'bx_github_authorizations', 'id', '', '', '', 20, NULL, 'start', '', '', '', 'like', '', '', 2147483647, 'BxGitHubGridAuthorizations', 'modules/boonex/github/classes/BxGitHubGridAuthorizations.php');
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
+('bx_github_authorizations', 'checkbox', '_sys_select', '5%', 0, 0, '', 1),
+('bx_github_authorizations', 'app_id', '_bx_github_grid_column_title_auth_app_id', '45%', 0, 0, '', 2),
+('bx_github_authorizations', 'added', '_bx_github_grid_column_title_auth_added', '10%', 0, 0, '', 3),
+('bx_github_authorizations', 'changed', '_bx_github_grid_column_title_auth_changed', '10%', 0, 0, '', 4),
+('bx_github_authorizations', 'at_expires_in', '_bx_github_grid_column_title_auth_at_expires_in', '10%', 0, 0, '', 5),
+('bx_github_authorizations', 'actions', '', '20%', 0, 0, '', 6);
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('bx_github_authorizations', 'bulk', 'delete', '_bx_github_grid_action_title_apps_delete', '', 0, 1, 1),
+('bx_github_authorizations', 'single', 'refresh', '_bx_github_grid_action_title_apps_refresh', 'redo', 1, 0, 1),
+('bx_github_authorizations', 'single', 'delete', '_bx_github_grid_action_title_apps_delete', 'remove', 1, 1, 2);

+ 57 - 8
modules/boonex/github/install/sql/install.sql

@@ -1,33 +1,82 @@
 SET @sName = 'bx_github';
 
 
+-- TABLE: apps
+CREATE TABLE IF NOT EXISTS `bx_github_apps` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `profile_id` int(11) unsigned NOT NULL DEFAULT 0,
+  `added` int(11) NOT NULL DEFAULT 0,
+  `title` varchar(128) NOT NULL DEFAULT '',
+  `client_id` varchar(64) NOT NULL DEFAULT '',
+  `client_secret` varchar(64) NOT NULL DEFAULT '',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `app` (`profile_id`, `client_id`)
+);
+
+-- TABLE: authorizations
+CREATE TABLE IF NOT EXISTS `bx_github_authorizations` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `profile_id` int(11) unsigned NOT NULL DEFAULT 0,
+  `app_id` int(11) unsigned NOT NULL DEFAULT 0,
+  `added` int(11) NOT NULL DEFAULT 0,
+  `changed` int(11) NOT NULL DEFAULT 0,
+  `access_token` varchar(128) NOT NULL DEFAULT '',
+  `at_expires_in` int(11) NOT NULL DEFAULT 0,
+  `refresh_token` varchar(128) NOT NULL DEFAULT '',
+  `rt_expires_in` int(11) NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `authorization` (`profile_id`, `app_id`)
+);
+
 -- TABLE: settings
 CREATE TABLE IF NOT EXISTS `bx_github_settings` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
   `profile_id` int(11) unsigned NOT NULL DEFAULT 0,
-  `pat` varchar(255) NOT NULL DEFAULT '',
+  `pat` varchar(64) NOT NULL DEFAULT '',
   PRIMARY KEY (`id`),
   UNIQUE KEY `profile_id` (`profile_id`)
 );
 
 
--- FORMS
+-- FORMS: Apps
+INSERT INTO `sys_objects_form` (`object`, `module`, `title`, `action`, `form_attrs`, `submit_name`, `table`, `key`, `uri`, `uri_title`, `params`, `deletable`, `active`, `override_class_name`, `override_class_file`) VALUES
+('bx_github_form_apps', @sName, '_bx_github_form_apps', '', '', 'do_submit', 'bx_github_apps', 'id', '', '', '', 0, 1, 'BxGitHubFormApps', 'modules/boonex/github/classes/BxGitHubFormApps.php');
+
+INSERT INTO `sys_form_displays` (`display_name`, `module`, `object`, `title`, `view_mode`) VALUES
+('bx_github_form_apps_add', @sName, 'bx_github_form_apps', '_bx_github_form_apps_display_add', 0),
+('bx_github_form_apps_edit', @sName, 'bx_github_form_apps', '_bx_github_form_apps_display_edit', 0);
+
+INSERT INTO `sys_form_inputs` (`object`, `module`, `name`, `value`, `values`, `checked`, `type`, `caption_system`, `caption`, `info`, `required`, `collapsed`, `html`, `attrs`, `attrs_tr`, `attrs_wrapper`, `checker_func`, `checker_params`, `checker_error`, `db_pass`, `db_params`, `editable`, `deletable`) VALUES
+('bx_github_form_apps', @sName, 'title', '', '', 0, 'text', '_bx_github_form_apps_input_sys_title', '_bx_github_form_apps_input_title', '', 0, 0, 0, '', '', '', '', '', '', 'Xss', '', 1, 0),
+('bx_github_form_apps', @sName, 'client_id', '', '', 0, 'text', '_bx_github_form_apps_input_sys_client_id', '_bx_github_form_apps_input_client_id', '', 0, 0, 0, '', '', '', '', '', '', 'Xss', '', 0, 0),
+('bx_github_form_apps', @sName, 'client_secret', '', '', 0, 'text', '_bx_github_form_apps_input_sys_client_secret', '_bx_github_form_apps_input_client_secret', '', 0, 0, 0, '', '', '', '', '', '', 'Xss', '', 0, 0),
+('bx_github_form_apps', @sName, 'do_submit', '_bx_github_form_apps_input_do_submit', '', 0, 'submit', '_bx_github_form_apps_input_sys_do_submit', '', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0);
+
+INSERT INTO `sys_form_display_inputs` (`display_name`, `input_name`, `visible_for_levels`, `active`, `order`) VALUES
+('bx_github_form_apps_add', 'title', 2147483647, 1, 1),
+('bx_github_form_apps_add', 'client_id', 2147483647, 1, 2),
+('bx_github_form_apps_add', 'client_secret', 2147483647, 1, 3),
+('bx_github_form_apps_add', 'do_submit', 2147483647, 1, 4),
+
+('bx_github_form_apps_edit', 'title', 2147483647, 1, 1),
+('bx_github_form_apps_edit', 'client_id', 2147483647, 1, 2),
+('bx_github_form_apps_edit', 'client_secret', 2147483647, 1, 3),
+('bx_github_form_apps_edit', 'do_submit', 2147483647, 1, 4);
+
+-- FORMS: Settings
 INSERT INTO `sys_objects_form` (`object`, `module`, `title`, `action`, `form_attrs`, `submit_name`, `table`, `key`, `uri`, `uri_title`, `params`, `deletable`, `active`, `override_class_name`, `override_class_file`) VALUES
 ('bx_github_form_settings', @sName, '_bx_github_form_settings', '', '', 'do_submit', 'bx_github_settings', 'id', '', '', '', 0, 1, 'BxGitHubFormSettings', 'modules/boonex/github/classes/BxGitHubFormSettings.php');
 
 INSERT INTO `sys_form_displays` (`display_name`, `module`, `object`, `title`, `view_mode`) VALUES
-('bx_github_form_settings_edit', @sName, 'bx_github_form_settings', '_bx_github_form_settings_display_edit', 0);
+('bx_github_form_settings_edit', @sName, 'bx_github_form_settings', '_bx_github_form_settings_display_edit_client_pat', 0);
 
 INSERT INTO `sys_form_inputs` (`object`, `module`, `name`, `value`, `values`, `checked`, `type`, `caption_system`, `caption`, `info`, `required`, `collapsed`, `html`, `attrs`, `attrs_tr`, `attrs_wrapper`, `checker_func`, `checker_params`, `checker_error`, `db_pass`, `db_params`, `editable`, `deletable`) VALUES
-('bx_github_form_settings', @sName, 'profile_id', '0', '', 0, 'hidden', '_bx_github_form_settings_input_sys_profile_id', '', '', 0, 0, 0, '', '', '', '', '', '', 'Int', '', 0, 0),
 ('bx_github_form_settings', @sName, 'pat', '', '', 0, 'text', '_bx_github_form_settings_input_sys_pat', '_bx_github_form_settings_input_pat', '', 0, 0, 0, '', '', '', '', '', '', 'Xss', '', 0, 0),
 ('bx_github_form_settings', @sName, 'do_submit', '_bx_github_form_settings_input_do_submit', '', 0, 'submit', '_bx_github_form_settings_input_sys_do_submit', '', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0);
 
 INSERT INTO `sys_form_display_inputs` (`display_name`, `input_name`, `visible_for_levels`, `active`, `order`) VALUES
-('bx_github_form_settings_edit', 'profile_id', 2147483647, 1, 1),
-('bx_github_form_settings_edit', 'username', 2147483647, 1, 2),
-('bx_github_form_settings_edit', 'pat', 2147483647, 1, 3),
-('bx_github_form_settings_edit', 'do_submit', 2147483647, 1, 4);
+('bx_github_form_settings_edit', 'pat', 2147483647, 1, 1),
+('bx_github_form_settings_edit', 'do_submit', 2147483647, 1, 2);
 
 
 -- LOGS

+ 1 - 1
modules/boonex/github/install/sql/uninstall.sql

@@ -2,7 +2,7 @@ SET @sName = 'bx_github';
 
 
 -- TABLES
-DROP TABLE IF EXISTS `bx_github_settings`;
+DROP TABLE IF EXISTS `bx_github_apps`, `bx_github_authorizations`, `bx_github_settings`;
 
 
 -- FORMS

+ 11 - 0
modules/boonex/github/template/authorize.html

@@ -0,0 +1,11 @@
+<div class="bx-github-authorize">
+    <div class="bx-github-authorize-cnt flex flex-col items-center">
+        <bx_if:show_authorize>
+            <a class="bx-btn" href="__authorize_url__"><bx_text:_bx_github_txt_login /></a>
+        </bx_if:show_authorize>
+        <bx_if:show_authorized>
+            <p class="text-xl font-bold"><bx_text:_bx_github_txt_authorized /></p>
+            <p><bx_text:_bx_github_txt_manage_here /> <a href="__manage_url__"><bx_text:_bx_github_txt_settings /></a></p>
+        </bx_if:show_authorized>
+    </div>
+</div>

+ 19 - 0
modules/boonex/github/template/popup_apps.html

@@ -0,0 +1,19 @@
+<div class="bx-github-apps-form">
+    <div class="bx-github-apps-form-cnt">
+        __form__
+        <script language="javascript">
+            $(document).ready(function () {
+                $("#__form_id__").ajaxForm({ 
+                    dataType: "json",
+                    beforeSubmit: function (formData, jqForm, options) {
+                        bx_loading($("#__form_id__"), true);
+                    },
+                    success: function (data) {
+                        $(".bx-popup-applied:visible").dolPopupHide();
+                        glGrids.__object__.processJson(data, '__action__');
+                    }
+                });
+            });
+        </script>
+    </div>
+</div>