1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Extension\EventHandler;
5use dokuwiki\Extension\Event;
6use dokuwiki\Form\Form;
7use dokuwiki\Action\Exception\ActionException;
8use dokuwiki\Action\Resendpwd;
9
10/**
11 * DokuWiki Plugin passpolicy (Action Component)
12 *
13 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
14 * @author  Andreas Gohr <andi@splitbrain.org>
15 */
16class action_plugin_passpolicy extends ActionPlugin
17{
18    /**
19     * Registers a callback function for a given event
20     *
21     * @param EventHandler $controller DokuWiki's event controller object
22     * @return void
23     */
24    public function register(EventHandler $controller)
25    {
26        $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handleForms');
27        $controller->register_hook('FORM_UPDATEPROFILE_OUTPUT', 'BEFORE', $this, 'handleForms');
28        $controller->register_hook('FORM_RESENDPWD_OUTPUT', 'BEFORE', $this, 'handleForms');
29
30        $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 'BEFORE', $this, 'handleForms');
31        $controller->register_hook('HTML_UPDATEPROFILEFORM_OUTPUT', 'BEFORE', $this, 'handleForms');
32        $controller->register_hook('HTML_RESENDPWDFORM_OUTPUT', 'BEFORE', $this, 'handleForms');
33
34        $controller->register_hook('AUTH_USER_CHANGE', 'BEFORE', $this, 'handlePasschange');
35
36        $controller->register_hook('AUTH_PASSWORD_GENERATE', 'BEFORE', $this, 'handlePassgen');
37
38        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax');
39
40        if ($this->getConf('supressuserhints')) {
41            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleResendPwd');
42            $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handleResendPwdUI');
43        }
44    }
45
46    /**
47     * Handle Ajax for the Password strength check
48     *
49     * @param Event $event
50     * @param $param
51     */
52    public function handleAjax(Event $event, $param)
53    {
54        if ($event->data !== 'plugin_passpolicy') {
55            return;
56        }
57        //no other ajax call handlers needed
58        $event->stopPropagation();
59        $event->preventDefault();
60
61        /* @var $INPUT \Input */
62        global $INPUT;
63        $pass = $INPUT->post->str('pass');
64        $user = $INPUT->post->str('user', $_SERVER['REMOTE_USER']);
65
66        /** @var helper_plugin_passpolicy $passpolicy */
67        $passpolicy = $this->loadHelper('passpolicy');
68        if (!$passpolicy->checkPolicy($pass, $user)) {
69            // passpolicy not matched, throw error
70            echo '0';
71        } else {
72            echo '1';
73        }
74    }
75
76    /**
77     * Print the password policy in forms that allow setting passwords
78     *
79     * @param Event $event event object
80     * @param mixed $param
81     */
82    public function handleForms(Event $event, $param)
83    {
84        if (is_a($event->data, Form::class)) {
85            // applicable to development snapshot 2020-10-13 or later
86            $pos = $event->data->findPositionByAttribute('name', 'passchk');
87        } else {
88            // applicable to 2020-07-29 "Hogfather" and older
89            $pos = $event->data->findElementByAttribute('name', 'passchk');
90        }
91        if (!$pos) return; // no password repeat field found
92
93        /** @var $passpolicy helper_plugin_passpolicy */
94        $passpolicy = plugin_load('helper', 'passpolicy');
95        $html = '<p class="passpolicy_hint">' . $passpolicy->explainPolicy() . '</p>';
96        if (is_a($event->data, Form::class)) {
97            $event->data->addHTML($html, ++$pos);
98        } else {
99            $event->data->insertElement(++$pos, $html);
100        }
101    }
102
103    /**
104     * Check if a new password matches the set password policy
105     *
106     * @param Event $event event object
107     * @param mixed $param
108     */
109    public function handlePasschange(Event $event, $param)
110    {
111        if ($event->data['type'] == 'create') {
112            $user = $event->data['params'][0];
113            $pass = $event->data['params'][1];
114        } elseif ($event->data['type'] == 'modify') {
115            $user = $event->data['params'][0];
116            if (!isset($event->data['params'][1]['pass'])) {
117                return; //password is not changed, nothing to do
118            }
119            $pass = $event->data['params'][1]['pass'];
120        } else {
121            return;
122        }
123
124        /** @var $passpolicy helper_plugin_passpolicy */
125        $passpolicy = plugin_load('helper', 'passpolicy');
126        if (!$passpolicy->checkPolicy($pass, $user)) {
127            // passpolicy not matched, throw error and stop modification
128            msg($this->getLang('badpasspolicy'), -1);
129            $event->preventDefault();
130            $event->stopPropagation();
131        }
132    }
133
134    /**
135     * Replace default password generator by policy aware one
136     *
137     * @param Event $event event object
138     * @param mixed $param
139     * @throws Exception
140     */
141    public function handlePassgen(Event $event, $param)
142    {
143        /** @var $passpolicy helper_plugin_passpolicy */
144        $passpolicy = plugin_load('helper', 'passpolicy');
145
146        $event->data['password'] = $passpolicy->generatePassword($event->data['foruser']);
147        $event->preventDefault();
148    }
149
150    /**
151     * Intercept the resendpwd action
152     *
153     * This supresses all hints on if a user exists or not
154     *
155     * @param Event $event
156     * @param $param
157     */
158    public function handleResendPwd(Event $event, $param)
159    {
160        $act = act_clean($event->data);
161        if ($act != 'resendpwd') return;
162
163        $event->preventDefault();
164
165        $action = new Resendpwd();
166        try {
167            $action->checkPreconditions();
168        } catch (ActionException $ignored) {
169            $event->data = 'show';
170            return;
171        }
172
173        try {
174            $action->preProcess();
175        } catch (ActionException $ignored) {
176        }
177
178        $this->fixResendMessages();
179    }
180
181    /**
182     * Reuse the standard action UI for ResendPwd
183     *
184     * @param Event $event
185     * @param $param
186     */
187    public function handleResendPwdUI(Event $event, $param)
188    {
189        $act = act_clean($event->data);
190        if ($act != 'resendpwd') return;
191        $event->preventDefault();
192        (new Resendpwd())->tplContent();
193    }
194
195    /**
196     * Replaces the resendPwd messages with neutral ones
197     *
198     * @return void
199     */
200    protected function fixResendMessages()
201    {
202        global $MSG;
203        global $lang;
204
205        foreach ((array)$MSG as $key => $info) {
206            if (
207                $info['msg'] == $lang['resendpwdnouser'] || $info['msg'] == $lang['resendpwdconfirm']
208            ) {
209                unset($MSG[$key]);
210                msg($this->getLang('resendpwd'), 1);
211            }
212        }
213    }
214}
215