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