<?php
/**
 * CodeIgniter Controller
 *
 * @category CodeIgniter
 * @license /_licenses/untermStrich/agb_normal.pdf
 * @author Christian Koller
 * @copyright 2009 untermStrich
 * @package controllers
 * @version $Id: start.php 16153 2018-07-11 09:58:46Z christian $
*/

require_once APPPATH.'us_libraries/vendor/autoload.php';
require_once APPPATH.'us_libraries/UsGenericProvider.php';

/**
 * The oauth controller for untermStrich
 */
class Oauth extends My_Controller
{
    public function __construct()
    {
        $this->in_simple_mode = true;
        parent::__construct();

        $this->load->library('Oauth_tools', '', 'oauth_tools');
    }
    
    /**
     * Get config
     * 
     * @return  array
     */
    private function _get_config()
    {
        $use_account = $this->uri->segment(2);
        $oauth2account = $this->session->flashdata('oauth2account');
        
        if (!empty($use_account)) {
            $config = $this->oauth_tools->get_config(intval($use_account));
        } elseif (!empty($oauth2account)) {
            $config = $this->oauth_tools->get_config(intval($oauth2account));
        }
        
        if (!isset($config)) {
            echo '<h2>Error: Missing account information</h2>';
            die();
        }
        
        $this->session->set_flashdata('oauth2account', $config['usAccountId']);
        
        return $config;
    }
    
    /**
     * Oauth controller
     * 
     * @return void
     */
    public function index()
    {
        $this->load->model('Oauth_clients_model', 'oauth_clients');
        $this->load->helper('html');
        $this->load->helper('url');
        $this->load->helper('cookie');
        $this->lang->load('basic', $this->web_settings->getLanguage());
        
        $use_mode = $this->uri->segment(3);
        $params = $this->_get_query_param();

        if (get_cookie('oauth_action')==='0365_filepicker' || $this->uri->segment(2)==='0365_filepicker') {
            $js_path = $this->config->item('us_js_path');
            $url = site_url($js_path.'/OneDrive.js');
            echo '<script type="text/javascript" src="'.$url.'"></script>';
            die();
        }

        if ($use_mode==='create_azure_app') {
            $this->_index_create_azure_app();
            return;
        } elseif ($this->uri->segment(2) === 'create_app_auth_done') {
            $this->_create_app_auth_done($params);
            return;
        }

        $config = $this->_get_config();
        
        if ($use_mode==='logout') {
            $keep_token = ($this->uri->segment(4) === '1') ? true : false;
            $this->_index_logout($config, $keep_token);
            return;
        } elseif (isset($params['error'])) {
            $this->_index_oauth_error($params, $config);
            return;
        } elseif (!isset($params['code'])) {
            $staff_id = intval($this->uri->segment(4));
            $staff_id = ($staff_id > 0) ? $staff_id : null;
            $this->session->set_flashdata('oauth2staff_id', $staff_id);
            $this->_index_no_code($config);
            return;
        } else {
            $staff_id = $this->session->flashdata('oauth2staff_id');
            $staff_id = (empty($staff_id)) ? null : $staff_id;
            $this->session->unset_flashdata('oauth2staff_id');
            $this->_index_complete_login($params, $config, $staff_id);
            return;
        }
    }
    /**
     * Create Azure App
     */
    protected function _index_create_azure_app()
    {
        $config = $this->oauth_tools->get_config_create_app();
        $this->_index_no_code($config);
    }
    /**
     * Called after the user authorized for app creation
     * 
     * @param   array       $params         OAuth params
     */
    protected function _create_app_auth_done($params)
    {
        $this->load->model('System_settings_model', 'system_settings', true);
        $this->load->model('Oauth_clients_model', 'oauth_clients');
        $this->load->model('Oauth_tokens_model', 'oauth_tokens');
        $this->load->helper('pdf_converter/o365');

        $onetime = $this->session->flashdata('oauth2_us_onetime');
        if (empty($onetime)) {
            echo '<h2>Error - Invalid state</h2>';
            return;
        }
        $serial = trim($this->installation->get('serial'));
        
        $sec_key1 = hash('sha256', $onetime.$serial, true);
        #$sec_key2 = hash('sha256', $serial.substr($onetime,0,10), true);
        $sec_key3 = hash('sha256', $serial.$onetime, true);
        
        $code = $this->encrypt->sec_decrypt($params['code'], $sec_key1);
        $client_secret = $this->encrypt->sec_decrypt($params['clientsec'], $sec_key3);

        $access_token_options = [
            'code' => $code, 
            'code_verifier' => $this->session->flashdata('oauth2verifier'),
        ];
        $this->session->unset_flashdata('oauth2verifier');

        try {
            $config = $this->oauth_tools->get_config_create_app();
            $config['clientSecret'] = $client_secret;
            $provider = new \League\OAuth2\Client\Provider\UsGenericProvider($config);

            $accessTokenObject = $provider->getAccessToken('authorization_code', $access_token_options);
            $access_token = $accessTokenObject->getToken();

            $graph = new \Microsoft\Graph\Graph();
            $graph->setAccessToken($access_token);

            $client = createGuzzleClient($access_token);
            

            $azure_app_data = $this->system_settings->getAzureAppData();
            #if we have one and it also exists on azure portal we update it (client_secret)
            $app_id = null;
            $recreate_mode = false;
            if (!empty($azure_app_data)) {
                $azure_app_data = json_decode($azure_app_data);
                $app_id = $azure_app_data->app_id;
                $recreate_mode = !$this->oauth_tools->get_application_exists($graph, $client, $app_id); 
            } 
            unset($azure_app_data); // might be update in next steps => have to reload again if required

            $this->oauth_tools->post_patch_applications($graph, $client, ($recreate_mode) ? null : $app_id);

            if (!empty($app_id)) {
                $azure_app_data = $this->system_settings->getAzureAppData();
                if (empty($azure_app_data)) basic_error("No stored azure app data found", null, __CLASS__.'::'.__FUNCTION__, 400);
                $azure_app_data = json_decode($azure_app_data);
                
                $clients = $this->oauth_clients->get_list((object) array('client_id' => $app_id));
                $client_db_ids = array_map(function($client) {
                    return $client->id;
                }, $clients);

                if ($recreate_mode) {
                    $this->oauth_clients->update_client_id($client_db_ids, $azure_app_data->app_id);
                    foreach ($client_db_ids as $client_db_id) {
                        $this->oauth_tokens->delete(null, $client_db_id);
                    }
                } 
                $this->oauth_clients->update_client_secret($client_db_ids, $azure_app_data->secret);
            }
            
        } catch (Exception $e) {
            basic_error($e->getMessage(), null, __CLASS__.'::'.__FUNCTION__, 400);
        }

        $data = array(
            'action'        => 'window.opener.webApp.oauth.onLoginDo();',
            'wait'          => 500,
            'us_version_ue' => urlencode($this->config->item('us_version')),
            'css_path'      => $this->config->item('us_css_path'),
            'js_path'       => $this->config->item('us_js_path'),
            'title'         => $this->lang->line('auth_oauth_login_ok'),
            'account_id'    => '',
            'expires'       => null,
            'expired'       => false,
            'resource_owner'    => '',
        );

        $this->load->view('Basic/startpage_oauth_ok', $data);        
    }

    /**
     * Logout user
     * 
     * @param   array       $config         Config info
     * @param   boolean     $keep_token     Keep the local token or delete it also
     */
    protected function _index_logout(array $config, $keep_token=false)
    {
        $account_id = $config['usAccountId'];

        if ($keep_token===false) $this->oauth_tools->delete($account_id);
        
        switch ($config['usAccountClientType']) {
            case Oauth_clients_model::TYPE_dropbox:
                $logoutUrl = $config['usClientAuthority'] . '/logout'; //Dropbox does not support redirect after logout
                break;
            default:
                $post_logout_redirect_uri = site_url('/simple/oauth_logout_done/'.$account_id); // The logout destination after the user is logged out from their account.
                $logoutUrl = $config['usClientAuthority'] . '/oauth2/logout?post_logout_redirect_uri='.urlencode($post_logout_redirect_uri);
                break;
        }
        
        $data = array(
            'url'           => $logoutUrl,
            'wait'          => 500,
            'us_version_ue' => urlencode($this->config->item('us_version')),
            'css_path'      => $this->config->item('us_css_path'),
            'js_path'       => $this->config->item('us_js_path'),
            'title'         => $this->lang->line('auth_oauth_logout1')
        );
        $this->load->view('Basic/startpage_oauth', $data);
    }
    
    /**
     * Show oauth error page
     * 
     * @param   array       $params         OAuth params
     * @param   array       $config         Config info
     */
    protected function _index_oauth_error(array $params, array $config)
    {
        $account_id = $config['usAccountId'];
        
        echo '<h2>Error: '. htmlentities(strip_tags($params['error'])).'</h2>';
        echo '<p>Message:'. htmlentities(strip_tags($params['error_description'])).'</p>';
        echo '<p>Please retry: <a href="' . $config['redirectUri'] . '">' . $config['redirectUri'] . '</a></p>';

        $this->oauth_tools->delete($account_id);
    }
    
    /**
     * No code, start login
     * 
     * @param   array       $config         Config info
     */
    protected function _index_no_code(array $config)
    {
        if (in_array($config['usClientMode'], Oauth_tools::USTRICH_OAUTH_MODES)) {
            
            if ($config['usClientMode']===Oauth_tools::MODE_OAUTH_USTRICHWEB) {
                $url_prefix = 'https://oauth.untermstrich.com/o365wpuboauth/';
            } else {
                $url_prefix = 'https://oauth.untermstrich.com/o365puboauth/';
            }

            switch ($config['usAccountClientType']) {
                case Oauth_clients_model::TYPE_ms_azure_365:
                    $url = $url_prefix.'start_files.php';
                    break;
                case Oauth_clients_model::TYPE_ms_mail:
                    $url = $url_prefix.'start_email.php';
                    break;
                case Oauth_clients_model::TYPE_ms_azure_create_app:
                    $url = $url_prefix.'start_create_azure_app.php';
                    break;
                default:
                    $url = $url_prefix.'start.php';
                    break;
            }

            $onetime = bin2hex(openssl_random_pseudo_bytes(32));
            $this->session->set_flashdata('oauth2_us_onetime', $onetime);
            $this->session->set_flashdata('oauth2state', $config['usClientMode']);

            $serial = trim($this->installation->get('serial'));
            $serial_enc = $this->encrypt->sec_encrypt($serial, hash('sha256', 'w849fb023dh23f084tg08wfhbe', true));

            $ustoken = $config['usUstoken'];
            if (empty($ustoken)) {
                basic_error('This record is a untermStrich App, but misses the ustoken.', null, __CLASS__.'::'.__FUNCTION__, 400);
            }
            $ustoken_enc = $this->encrypt->sec_encrypt($ustoken, hash('sha256', '03fh408tghqf2f83o3hfefh8h34', true));

            $returnurl = $config['usThisRedirectUri'];
            $returnurl_enc = $this->encrypt->sec_encrypt($returnurl, hash('sha256', '0dhvn0z88xmngkidu298gtb76d', true));
            
            $code_verifier = bin2hex(openssl_random_pseudo_bytes(32));
            $code_challenge = $this->_pkce_code_challenge($code_verifier);
            $this->session->set_flashdata('oauth2verifier', $code_verifier);

            $data = array(
                'onetime'       => $onetime,
                'serial'        => $serial_enc,
                'ustoken'       => $ustoken_enc,
                'returnurl'     => $returnurl_enc,
                'challenge'     => $code_challenge,
                'url'           => $url,
                'us_version_ue' => urlencode($this->config->item('us_version')),
                'css_path'      => $this->config->item('us_css_path'),
                'js_path'       => $this->config->item('us_js_path'),
                'title'         => $this->lang->line('basic_connect')
            );
            $this->load->view('Basic/startpage_us_oauth', $data);
        } else {
            $provider = new \League\OAuth2\Client\Provider\UsGenericProvider($config);
            $authorizationUrl = $provider->getAuthorizationUrl(array('state' => us_uniqid('a',false).'____'.base_url()));

            $this->session->set_flashdata('oauth2state', $provider->getState());

            $data = array(
                'url'           => $authorizationUrl,
                'us_version_ue' => urlencode($this->config->item('us_version')),
                'css_path'      => $this->config->item('us_css_path'),
                'js_path'       => $this->config->item('us_js_path'),
                'title'         => $this->lang->line('auth_oauth_login1')
            );
            $this->load->view('Basic/startpage_oauth', $data);
        }
    }
    
    /**
     * Create PKCE code challenge
     * 
     * @param   string      $code_verifier      Code verifier
     * @return  string
     */
    private function _pkce_code_challenge(string $code_verifier): string
    {
        $hash = hash('sha256', $code_verifier, true);
        return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
    }
    
    /**
     * Show an error message
     * 
     * @param   array       $config         Config info
     * @param   string      $message        Message
     */
    private function _show_error(array $config, string $message)
    {
        echo '<h2>Error - '.$message.'</h2>';
        echo '<p>Please retry: <a href="' . $config['redirectUri'] . '">' . $config['redirectUri'] . '</a></p>';
    }
    
    /**
     * Complete the login
     * 
     * @param   array       $params         OAuth params
     * @param   array       $config         Config info
     * @param   integer     $staff_id       Optinal staff_id
     */
    protected function _index_complete_login(array $params, array $config, $staff_id=null)
    {
        $step = 'Init';
        if (in_array($config['usClientMode'], Oauth_tools::USTRICH_OAUTH_MODES)) {
            $onetime = $this->session->flashdata('oauth2_us_onetime');
            if (empty($onetime)) {
                $this->_show_error($config, 'Invalid state 2');
                return;
            }
            $serial = trim($this->installation->get('serial'));
            
            $sec_key1 = hash('sha256', $onetime.$serial, true);
            $sec_key2 = hash('sha256', $serial.substr($onetime,0,10), true);
            $sec_key3 = hash('sha256', $serial.$onetime, true);
            
            $code = $this->encrypt->sec_decrypt($params['code'], $sec_key1);
            $sdate = $this->encrypt->sec_decrypt($params['sdate'], $sec_key2);
            
            try {
                $now = new DateTime();
                $c_date_obj = new DateTime($sdate);
                $diff = $now->diff($c_date_obj);

                if (abs($diff->format('%a'))>0) {
                    $this->_show_error($config, 'Invalid response 3.1');
                    return;
                }
            } catch (Exception $exc) {
                $this->_show_error($config, 'Invalid response 3.0');
                return;
            }
            
            if ($config['usClientMode']===Oauth_tools::MODE_OAUTH_USTRICHWEB) {
                $client_secret = $this->encrypt->sec_decrypt($params['clientsec'], $sec_key3);
                $this->oauth_clients->set_internal_client_secret($config['clientId'], $client_secret);
                
                $config = $this->_get_config();
            }
            
            $access_token_options = [
                'code' => $code, 
                'code_verifier' => $this->session->flashdata('oauth2verifier'),
            ];
            $this->session->unset_flashdata('oauth2verifier');
        } else {
            $oauth2state = $this->session->flashdata('oauth2state');
            if (empty($params['state']) || (isset($oauth2state) && $params['state'] !== $oauth2state)) {
                if (isset($oauth2state)) {
                    $this->session->unset_flashdata('oauth2state');
                }
                $this->_show_error($config, 'Invalid state');
                return;
            }
            
            $access_token_options = [
                'code' => $params['code'], 
            ];
        }
        
        try {
            if ($config['usClientMode']!==Oauth_tools::MODE_OAUTH_USTRICH) {
                if (empty($config['clientSecret'])) {
                    throw new Exception('Error - Missing client secret');
                }
            }
            
            $account_id = $config['usAccountId'];

            $provider = new \League\OAuth2\Client\Provider\UsGenericProvider($config);

            $step = 'Call getAccessToken';
            if ($config['usClientMode']===Oauth_tools::MODE_OAUTH_USTRICH) {
                $provider->setOrigin('https://oauth.untermstrich.com');
            }
            $accessTokenObject = $provider->getAccessToken('authorization_code', $access_token_options);

            $step = 'After getAccessToken';
            $this->oauth_tools->set_token($account_id, $accessTokenObject, $staff_id, true);

            $expired = $accessTokenObject->hasExpired();
            $access_expires = $accessTokenObject->getExpires();
            $expires_date = null;
            if (is_numeric($access_expires)) {
                $expires_date = new UsDateTime();
                $expires_date->setTimestamp($access_expires);
            }

            $resource_owner = '';
            try {
                $access_token = $accessTokenObject->getToken();

                $this->load->helper('jwt_decode');
                list($header, $payload, $sig) = us_jwt_decode($access_token);

                if (is_object($payload)) {
                    $resource_owner .= self::_print_item($payload, 'aud');
                    $resource_owner .= self::_print_item($payload, 'iss');
                    $resource_owner .= self::_print_item($payload, 'app_displayname', 'Application Name');
                    $resource_owner .= self::_print_item($payload, 'appid');
                    $resource_owner .= self::_print_item($payload, 'family_name', 'User Family Name');
                    $resource_owner .= self::_print_item($payload, 'given_name', 'User Given Name');
                    $resource_owner .= self::_print_item($payload, 'name', 'User Name');
                    $resource_owner .= self::_print_item($payload, 'scp', 'Scopes');
                    $resource_owner .= self::_print_item($payload, 'unique_name', 'User Unique Name');
                    $resource_owner .= self::_print_item($payload, 'upn');
                }
            } catch (Exception $e) {}

            $data = array(
                'action'        => 'window.opener.webApp.oauth.onLoginDo();',
                'wait'          => 500,
                'us_version_ue' => urlencode($this->config->item('us_version')),
                'css_path'      => $this->config->item('us_css_path'),
                'js_path'       => $this->config->item('us_js_path'),
                'title'         => $this->lang->line('auth_oauth_login_ok'),
                'account_id'    => $account_id,
                'expires'       => $expires_date,
                'expired'       => $expired,
                'resource_owner'    => $resource_owner,
            );

            if ($config['usAccountClientType'] === Oauth_clients_model::TYPE_ms_azure_365_calendar) {
                $this->load->library('O365_calendar_tools', '', 'o365_calendar_tools');
                $stat = $this->o365_calendar_tools->connect_calendar($account_id, $staff_id);
                if ($stat) {
                    $data['calendar_connection_error'] = $stat;
                    $data['wait'] = 10000; //Increase the wait time before closing popup
                }
            }

            $this->load->view('Basic/startpage_oauth_ok', $data);
        } catch (UnexpectedValueException $e) {
            echo '<h2>'.$this->lang->line('auth_oauth_login_fail').'</h2>';
            echo '<p>Error UnexpectedValueException: '.$e->getMessage().'</p>';

            echo '<p>Details:<br><small>';
                echo '<b>Step:</b> '.$step.'<br>';
                echo '<b>Within:</b>'.$e->getFile().' - '.$e->getLine().'<br>';
                echo '<pre style="padding-left: 20px;">';                            
                echo $e->getTraceAsString();
                echo '</pre>';
            echo '</small></p>';

            echo '<p>Please retry: <a href="' . $config['redirectUri'] . '">' . $config['redirectUri'] . '</a></p>';
        } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
            echo '<h2>'.$this->lang->line('auth_oauth_login_fail').'</h2>';
            echo '<p>Error IdentityProviderException: '.$e->getMessage().'</p>';

            $response = $e->getResponseBody();

            echo '<p>Details:<br><small>';
                echo '<b>Step:</b> '.$step.'<br>';
                echo '<b>Response:</b><br>';
                echo '<pre style="padding-left: 20px;">';                            
                var_dump($response);
                echo '</pre>';
            echo '</small></p>';

            echo '<p>Please retry: <a href="' . $config['redirectUri'] . '">' . $config['redirectUri'] . '</a></p>';
        }
    }
    
    /**
     * Print item from object
     * 
     * @param   object  $obj        Object
     * @param   string  $field      Field name to print
     * @param   mixed   $title      Optional title
     * @return  string
     */
    private static function _print_item($obj, $field, $title=false)
    {
        if (isset($obj->$field)) {
            $title = ($title!==false) ? $title : $field;
            return '<em>'.$title.':</em> '.$obj->$field . '<br>';
        }
        return '';
    }
    
    /**
     * Get the current query parameters instead of from $_GET, from REQUEST_URI
     * 
     * @return  array       Query parameters
     */
    private function _get_query_param(): array
    {
        if (!isset($_SERVER['REQUEST_URI']) OR empty($_SERVER['REQUEST_URI'])) {
            return array();
        }
        
        $query = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);
        if (!isset($query) OR empty($query)) {
            return array();
        }
        
        parse_str($query, $reply);
        return $reply;
    }
    
}