<?php

/*
 * RCMCardDAV - CardDAV plugin for Roundcube webmail
 *
 * Copyright (C) 2011-2022 Benjamin Schieder <rcmcarddav@wegwerf.anderdonau.de>,
 *                         Michael Stilkerich <ms@mike2k.de>
 *
 * This file is part of RCMCardDAV.
 *
 * RCMCardDAV is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * RCMCardDAV is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with RCMCardDAV. If not, see <https://www.gnu.org/licenses/>.
 */

declare(strict_types=1);

namespace MStilkerich\RCMCardDAV;

use Exception;
use Psr\Log\{LoggerInterface,LogLevel};
use MStilkerich\CardDavClient\{Account,WebDavResource};
use MStilkerich\CardDavClient\Services\{Discovery,Sync};
use MStilkerich\CardDavClient\XmlElements\ElementNames as XmlEN;
use MStilkerich\RCMCardDAV\Db\{Database, AbstractDatabase};
use MStilkerich\RCMCardDAV\Frontend\{RcmInterface, AdminSettings, AddressbookManager, Utils};
use rcube;
use rcube_cache;

/**
 * This class is intended as a central point to inject dependencies to infrastructure classes.
 *
 * This allows replacing dependencies in unit tests with mock objects.
 *
 * @psalm-import-type AccountSettings from AddressbookManager
 */
class Config
{
    /** @var ?Config The single instance of this class - can be exchanged by tests */
    public static $inst;

    /** @var ?RcmInterface Adapter to roundcube */
    protected $rc;

    /**
     * The admin settings configured in the config.inc.php file.
     * @var AdminSettings
     */
    protected $admPrefs;

    /** @var LoggerInterface */
    protected $logger;

    /** @var LoggerInterface */
    protected $httpLogger;

    /** @var AbstractDatabase */
    protected $db;

    /** @var ?rcube_cache */
    protected $cache;

    public static function inst(): Config
    {
        if (!isset(self::$inst)) {
            self::$inst = new Config();
        }

        return self::$inst;
    }

    public function __construct()
    {
        $this->logger = new RoundcubeLogger("carddav", LogLevel::ERROR);
        $this->httpLogger = new RoundcubeLogger("carddav_http", LogLevel::ERROR);
        $this->admPrefs = new AdminSettings(__DIR__ . '/../config.inc.php', $this->logger, $this->httpLogger);

        $rcube = rcube::get_instance();
        $this->db = new Database($this->logger, $rcube->get_dbh());
    }

    public function db(): AbstractDatabase
    {
        return $this->db;
    }

    public function logger(): LoggerInterface
    {
        return $this->logger;
    }

    public function httpLogger(): LoggerInterface
    {
        return $this->httpLogger;
    }

    public function rc(?RcmInterface $rc = null): RcmInterface
    {
        if (!isset($this->rc)) {
            if (isset($rc)) {
                $this->rc = $rc;
            } else {
                throw new Exception("Roundcube adapter not set");
            }
        }

        return $this->rc;
    }

    public function admPrefs(): AdminSettings
    {
        return $this->admPrefs;
    }

    /**
     * Returns a handle to the roundcube cache for the user.
     *
     * Note: this must be called when the user is already logged on, specifically it must not be called
     * during the constructor of the plugin.
     */
    public function cache(): rcube_cache
    {
        if (!isset($this->cache)) {
            // TODO make TTL and cache type configurable
            $rcube = rcube::get_instance();
            $this->cache = $rcube->get_cache("carddav", "db", "1w");
        }

        if (!isset($this->cache)) {
            throw new Exception("Attempt to request cache where not available yet");
        }

        return $this->cache;
    }

    public function makeDiscoveryService(): Discovery
    {
        return new Discovery();
    }

    public function makeSyncService(): Sync
    {
        return new Sync();
    }

    public function makeWebDavResource(string $uri, Account $account, bool $knownAbook = false): WebDavResource
    {
        $resType = $knownAbook ? [XmlEN::RESTYPE_ABOOK] : null;
        return WebDavResource::createInstance($uri, $account, $resType);
    }

    /**
     * Creates an Account object from the credentials in rcmcarddav.
     *
     * Particularly, this takes care of setting up the credentials information properly.
     *
     * @param AccountSettings $accountCfg
     */
    public static function makeAccount(array $accountCfg): Account
    {
        $password = Utils::replacePlaceholdersPassword($accountCfg['password'] ?? '');

        if ($password == "%b") {
            $acctoken = false;
            if (isset($_SESSION['oauth_token']) && is_array($_SESSION['oauth_token'])) {
                $rcube = rcube::get_instance();
                $acctoken = $rcube->decrypt((string) $_SESSION['password']);

                if ($acctoken !== false) {
                    // authorization stored in SESSION[password] is like "Bearer <token>", so cut the token type
                    $token_type = ((string) $_SESSION['oauth_token']['token_type']) . " ";
                    $token_type_len = strlen($token_type);
                    if (strncmp($acctoken, $token_type, $token_type_len) === 0) {
                        $acctoken = substr($acctoken, $token_type_len);
                    }
                }
            }

            if ($acctoken === false) {
                throw new Exception("OAUTH2 bearer authentication requested, but no token available in roundcube");
            }
            $httpOptions = [ 'bearertoken' => $acctoken ];
        } else {
            $username = Utils::replacePlaceholdersUsername($accountCfg['username'] ?? '');
            $httpOptions = [ 'username' => $username, 'password' => $password ];
        }

        $httpOptions['preemptive_basic_auth'] = (bool) ($accountCfg['preemptive_basic_auth'] ?? false);
        $httpOptions['verify'] = !((bool) ($accountCfg['ssl_noverify'] ?? false));

        $discUrl  = Utils::replacePlaceholdersUrl($accountCfg['discovery_url'] ?? '');
        return new Account($discUrl, $httpOptions);
    }
}

// vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120
