1<?php
2
3namespace dokuwiki\plugin\twofactor;
4
5use dokuwiki\Extension\ActionPlugin;
6use dokuwiki\Form\Form;
7use dokuwiki\Utf8\PhpString;
8
9/**
10 * Baseclass for all second factor providers
11 */
12abstract class Provider extends ActionPlugin
13{
14    /** @var Settings */
15    protected $settings;
16
17    /** @var string */
18    protected $providerID;
19
20    /** @inheritdoc */
21    public function register(\Doku_Event_Handler $controller)
22    {
23        $controller->register_hook(
24            'PLUGIN_TWOFACTOR_PROVIDER_REGISTER',
25            'AFTER',
26            $this,
27            'registerSelf',
28            null,
29            Manager::EVENT_PRIORITY - 1 // providers first
30        );
31    }
32
33    /**
34     * Register this class as a twofactor provider
35     *
36     * @param \Doku_Event $event
37     * @return void
38     */
39    public function registerSelf(\Doku_Event $event)
40    {
41        $event->data[$this->getProviderID()] = $this;
42    }
43
44    /**
45     * Initializes the provider for the given user
46     * @param string $user Current user
47     */
48    public function init($user)
49    {
50        $this->settings = new Settings($this->getProviderID(), $user);
51    }
52
53    // region Introspection methods
54
55    /**
56     * The ID of this provider
57     *
58     * @return string
59     */
60    public function getProviderID()
61    {
62        if (!$this->providerID) {
63            $this->providerID = $this->getPluginName();
64        }
65
66        return $this->providerID;
67    }
68
69    /**
70     * Pretty Label for this provider
71     *
72     * @return string
73     */
74    public function getLabel()
75    {
76        return PhpString::ucfirst($this->providerID);
77    }
78
79    // endregion
80    // region Configuration methods
81
82    /**
83     * Clear all settings
84     */
85    public function reset()
86    {
87        $this->settings->purge();
88    }
89
90    /**
91     * Has this provider been fully configured and verified by the user and thus can be used
92     * for authentication?
93     *
94     * @return bool
95     */
96    abstract public function isConfigured();
97
98    /**
99     * Render the configuration form
100     *
101     * This method should add the needed form elements to (re)configure the provider.
102     * The contents of the form may change depending on the current settings.
103     *
104     * No submit button should be added - this is handled by the main plugin.
105     *
106     * @param Form $form The initial form to add elements to
107     * @return Form
108     */
109    abstract public function renderProfileForm(Form $form);
110
111    /**
112     * Handle any input data
113     *
114     * @return void
115     */
116    abstract public function handleProfileForm();
117
118    // endregion
119    // region OTP methods
120
121    /**
122     * Create and store a new secret for this provider
123     *
124     * @return string the new secret
125     * @throws \Exception when no suitable random source is available
126     */
127    public function initSecret()
128    {
129        $ga = new GoogleAuthenticator();
130        $secret = $ga->createSecret();
131
132        $this->settings->set('secret', $secret);
133        return $secret;
134    }
135
136    /**
137     * Get the secret for this provider
138     *
139     * @return string
140     */
141    public function getSecret()
142    {
143        return $this->settings->get('secret');
144    }
145
146    /**
147     * Generate an auth code
148     *
149     * @return string
150     * @throws \Exception when no code can be created
151     */
152    public function generateCode()
153    {
154        $secret = $this->settings->get('secret');
155        if (!$secret) throw new \Exception('No secret for provider ' . $this->getProviderID());
156
157        $ga = new GoogleAuthenticator();
158        return $ga->getCode($secret);
159    }
160
161    /**
162     * Check the given code
163     *
164     * @param string $code
165     * @param bool $usermessage should a message about the failed code be shown to the user?
166     * @return bool
167     * @throws \RuntimeException when no code can be created
168     */
169    public function checkCode($code, $usermessage = true)
170    {
171        $secret = $this->settings->get('secret');
172        if (!$secret) throw new \RuntimeException('No secret for provider ' . $this->getProviderID());
173
174        $ga = new GoogleAuthenticator();
175        $ok = $ga->verifyCode($secret, $code, $this->getTolerance());
176        if (!$ok && $usermessage) {
177            msg((Manager::getInstance())->getLang('codefail'), -1);
178        }
179        return $ok;
180    }
181
182    /**
183     * The tolerance to be used when verifying codes
184     *
185     * This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
186     * Different providers may want to use different tolerances by overriding this method.
187     *
188     * @return int
189     */
190    public function getTolerance()
191    {
192        return 2;
193    }
194
195    /**
196     * Transmits the code to the user
197     *
198     * If a provider does not transmit anything (eg. TOTP) simply
199     * return the message.
200     *
201     * @param string $code The code to transmit
202     * @return string Informational message for the user
203     * @throw \Exception when the message can't be sent
204     */
205    abstract public function transmitMessage($code);
206
207    // endregion
208}
209