1<?php
2/**
3 * Federated Login for DokuWiki - helper class
4 *
5 * Enables your DokuWiki to provide users with
6 * Hybrid OAuth + OpenId federated login.
7 *
8 * @copyright  2012 Aoi Karasu
9 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
10 * @link       http://www.dokuwiki.org/plugin:fedauth
11 * @author     Aoi Karasu <aoikarasu@gmail.com>
12 */
13
14// must be run within Dokuwiki
15if (!defined('DOKU_INC')) die();
16
17if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
18if (!defined('FEDAUTH_PLUGIN')) define('FEDAUTH_PLUGIN', DOKU_PLUGIN . 'fedauth/');
19
20require_once(DOKU_PLUGIN.'action.php');
21
22global $conf;
23
24// define cookie and session id, append server port when securecookie is configured
25if (!defined('FEDAUTH_COOKIE')) define('FEDAUTH_COOKIE', 'DWFA'.md5(DOKU_REL.(($conf['securecookie'])?$_SERVER['SERVER_PORT']:'')));
26
27if (!defined('USER_CMD_SCOPE')) define('USER_CMD_SCOPE', 'usr');
28
29class action_plugin_fedauth extends DokuWiki_Action_Plugin {
30
31    var $provid = '';
32    var $cmd = '';
33    var $handler = null;
34
35    var $cookie = null;
36
37    var $providers = null;
38
39    var $functions = array('select','signin','signedin','remove'); // require a provider id
40    var $commands = array('add','login','logout','manage','register'); // don't require a provider id
41
42    /**
43     * Returns the plugin meta information.
44     */
45    function getInfo() {
46        return array(
47            'author' => 'Aoi Karasu',
48            'email'  => 'aoikarasu@gmail.com',
49            'date'   => '2012-06-09',
50            'name'   => 'Federated Login Plugin',
51            'desc'   => 'Functions to handle user identity related DokuWiki actions',
52            'url'    => 'http://www.dokuwiki.org/plugin:fedauth',
53        );
54    }
55
56    /**
57     * Registers the event handlers.
58     */
59    function register(&$controller)
60    {
61        $controller->register_hook('HTML_LOGINFORM_OUTPUT',         'AFTER',  $this, 'handle_login_form',     array());
62        $controller->register_hook('HTML_UPDATEPROFILEFORM_OUTPUT', 'AFTER',  $this, 'handle_profile_form',   array());
63        $controller->register_hook('ACTION_ACT_PREPROCESS',         'BEFORE', $this, 'handle_act_preprocess', array());
64        $controller->register_hook('TPL_ACT_UNKNOWN',               'BEFORE', $this, 'handle_act_unknown',    array());
65        $controller->register_hook('AUTH_LOGIN_CHECK',              'BEFORE', $this, 'handle_login_check',    array());
66    }
67
68    /**
69     * Validates federated login credentials.
70     *
71     * This method is crucial to keep the user logged in, when authorized via fedauth.
72     * If the fedauth cookie is found, it is validated against the session data
73     * and timeout value. When valid, user stays logged in. Otherwise a request
74     * is issued to the authorization service stored in the cookie, to authorize
75     * the user again. The default authorization check via auth_login() is suppressed
76     * and $_SESSION[DOKU_COOKIE]['auth'] is never set - there is no need for it.
77     *
78     * If the cookie is not found, the default authorization is not suppressed and
79     * will proceed as usual (unless other plugins influence it).
80     *
81     * IMPORTANT:
82     * There is an uresolved problem with AJAX calls, however. For typical GET and POST
83     * calls the timestamp of last successful authorization is validated against the
84     * $conf['auth_security_timeout'] variable. On timeout an authorization request
85     * is reissued to the authorization service. This involves HTTP redirections that
86     * don't work with AJAX calls that eventually hang.
87     *
88     * For the time beeing the timeout check is skipped for AJAX calls and the user
89     * stays logged in as long as other fedauth cookie data is valid.
90     *
91     * Dokuwiki built-in AJAX handler 'lib/exe/ajax.php' expects $_GET['call'] or
92     * $_POST['call'] to be set. Thus the action_plugin_fedauth::isAjaxCall() use
93     * this condition to recognize an AJAX call. Custom AJAX handlers should be
94     * implemented the same way as the built-in one, or at least implment setting
95     * of a dummy value before any invocations of Dokuwiki functions are made, eg.
96     * $_GET['call'] = 'dummy';
97     *
98     * @param object $event event data
99     * @param array $param additional parameters
100     */
101    function handle_login_check(&$event, $param) {
102        global $conf, $USERINFO;
103
104        // standard dokuwiki auhtorization in progress, escape
105        if (!empty($event->data['user'])) return;
106
107        require_once(FEDAUTH_PLUGIN . "classes/fa_cookie.class.php");
108        $this->cookie = new fa_cookie();
109
110        // fedauth signed-in complete or logout in progress; requires $this->cookie to be set,
111        // however an escape is a must to prevent unwanted behavior (auth loop, blocked logout)
112        if ((!empty($_REQUEST['fa']['signedin']) && ($_REQUEST['mode'] != 'add')) ||
113            ($_REQUEST['do'] == 'logout')) return;
114
115        if ($cdata = $this->cookie->get()) {
116//        msg( "<pre>".print_r($cdata, true)."</pre>");
117            $user = $cdata['user'];
118            $session = $_SESSION[DOKU_COOKIE]['fedauth'];
119            // remove temp data, if any
120            if (isset($_SESSION[DOKU_COOKIE]['fedauth']['tmpr'])) {
121                unset($_SESSION[DOKU_COOKIE]['fedauth']['tmpr']);
122            }
123//        msg( "<pre>".print_r($session, true)."</pre>");
124            // refer to Dokuwiki's 'inc/auth/basic.class.php' for detailed information
125            // on useSessionCache() method and the purpose of @filemtime() condition
126            if (isset($session) &&
127                ($session['time'] >= @filemtime($conf['cachedir'] . '/sessionpurge')) &&
128                (($session['time'] >= time() - $conf['auth_security_timeout']) || $this->isLibExe() || $this->isAjaxCall()) &&
129                ($session['user'] == $user) &&
130                ($session['prid'] == $cdata['prid']) &&
131                ($session['stok'] == $cdata['stok']) &&
132                ($session['buid'] == auth_browseruid())
133            ) {
134                // cookie and session ok - keep user logged-in
135//            msg( "<pre>".print_r($session['rq'], true)."</pre>");
136                $_SERVER['REMOTE_USER'] = $user;
137                $USERINFO = $session['info'];
138                $event->preventDefault();
139                if (isset($session['sgin'])) {
140                   // redirected from authorization service, display welcome message
141                   msg('login successful');
142                   unset($_SESSION[DOKU_COOKIE]['fedauth']['sgin']);
143                }
144                if ($session['stor']) {
145                   // restore request values from saved before authorization interception
146                   $this->_restoreRequestData();
147                }
148                return;
149            }
150//print('<pre>'.print_r($GLOBALS, true).'</pre>'); return;
151            // perform fedauth auhtorization based on cookie data
152            $this->_ensure_providers_loaded();
153            if ($pro = $this->providers->get($cdata['prid'])) {
154                global $ID;
155                $ID = getID();
156                $this->_require_user_infrastructure();
157                // load command class and process the signin command
158                $this->handler =& load_handler_class($this, 'signin', USER_CMD_SCOPE, $cdata['prid'], 'login');
159                $result = $this->handler->callService($pro, $cdata['svcd'], true);
160                if (is_array($result) && empty($_REQUEST['ajax'])) {
161                    msg($result['msg'], $result['code']);
162                }
163                return;
164            }
165            // provider not found
166            msg('cookie found but provider is unknown or disabled');
167            $this->cookie->clean();
168        }
169        else if (isset($_SESSION[DOKU_COOKIE]['fedauth']['tmpr'])) {
170            // temporary fedauth data set, user authenticated but does not have local account
171            if ($_REQUEST['do'] == 'register') {
172                // if user navigated to standard register page, redirect to fedauth one
173                send_redirect(wl(getID(), 'do=fedauth', true, '&') . '&fa[register]');
174            }
175            if (!isset($_REQUEST['fa']['register'])) {
176                // display account creation reminder
177                $msg = $this->getLang('registernow');
178                $msg = str_replace('@PROVID@', $_SESSION[DOKU_COOKIE]['fedauth']['tmpr']['prnm'], $msg);
179                $msg = str_replace('@REGURL@', wl(getID(), 'do=fedauth', true, '&').'&fa[register]', $msg);
180                msg($msg, 2);
181            }
182        }
183        // no fedauth cookie nor temp login, do nothing fedauth related
184        // unless any other plugin takes over, auth_login() comes into play
185    }
186
187    /**
188     * Handles federated login action preprocess for fedauth, login, logout
189     * and profile actions. Loads required classes and authoriation providers
190     * configuration, processes request variables to route commands and
191     * optionally parameters to proper classes. Finally performs process()
192     * method for the selected command and prints result message (if any)
193     * using Dokuwiki message system, the msg() function.
194     */
195    function handle_act_preprocess(&$event, $param) {
196        if ($event->data != 'login' && $event->data != 'logout' &&
197            $event->data != 'profile' && $event->data != 'fedauth') {
198            return;
199        }
200
201        // require infrastructure classes
202        $this->_require_user_infrastructure();
203        $this->_ensure_no_temp_data();
204        $user = $_SERVER['REMOTE_USER'];
205
206        // load providers configuration
207        $this->_ensure_providers_loaded();
208
209        // get command array and emulate logout command for dokuwiki logout action
210        $fa = ($event->data != 'logout') ? $_REQUEST['fa'] : array('logout' => null);
211        // read command arrray
212        if (is_array($fa)) {
213            $this->cmd = key($fa);
214            $this->provid = is_array($fa[$this->cmd]) ? key($fa[$this->cmd]) : null;
215        } else {
216            $this->cmd = $fa;
217            $this->provid = null;
218        }
219
220        $defaultcmd = empty($user) ? 'login' : 'manage';
221
222        // validate command array
223        if (in_array($this->cmd, $this->commands)) {
224            $this->provid = '';
225        } else if (!in_array($this->cmd, $this->functions) || !$this->providers->get($this->provid)) {
226            // NOTE: would be nicer if redirected to wiki's login page on empty username
227            $this->cmd = $defaultcmd;
228            $this->provid = '';
229        } else if ($this->cmd == 'select') {
230            $this->cmd = 'login';
231        }
232
233        // takeover fedauth action handling
234        if ($event->data == 'fedauth') {
235            $event->stopPropagation();
236            $event->preventDefault();
237
238            // manage and signedin commands do not require checking the security token
239            if ($this->cmd != 'manage' && $this->cmd != 'signedin') {
240                if (($this->cmd != 'login' || $this->provid != '') && !checkSecurityToken()) {
241                    $this->cmd = $defaultcmd;
242                    $this->provid = '';
243                }
244            }
245        }
246
247        // load command class and process the command
248        $this->handler =& load_handler_class($this, $this->cmd, USER_CMD_SCOPE, $this->provid, 'login');
249        $result = $this->handler->process();
250        if (is_array($result) && empty($_REQUEST['ajax'])) {
251            msg($result['msg'], $result['code']);
252        }
253    }
254
255    /**
256     * Handles unknown action preprocess.
257     */
258    function handle_act_unknown(&$event, $param) {
259        // mandatory check since other plugins' actions trigger this as well
260        if ($event->data != 'fedauth') {
261             return;
262        }
263
264        // enable direct access to language strings
265        $this->setupLocale();
266        $event->stopPropagation();
267        $event->preventDefault();
268        $this->handler->html();
269    }
270
271    /**
272     * Handles the login form rendering.
273     */
274    function handle_login_form(&$event, $param) {
275        $this->setupLocale();
276        $this->handler->html();
277    }
278
279    /**
280     * Handles the profile form rendering.
281     */
282    function handle_profile_form(&$event, $param) {
283        global $ID;
284
285        print '<p>' . '<a href="' . wl($ID, 'do=fedauth', true, '&') . '">' . $this->getLang('mylogins') . '</a></p>';
286    }
287
288    /**
289     * Removes temporary fedauth data in case user authenticated with unassigned
290     * identity while not being logged in, and later on he did log in using
291     * different credentials instead of creating a new account.
292     */
293    function _ensure_no_temp_data() {
294        if (!empty($_SERVER['REMOTE_USER']) && isset($_SESSION[DOKU_COOKIE]['fedauth']['tmpr'])) {
295            @session_start(); // make session writable
296            unset($_SESSION[DOKU_COOKIE]['fedauth']['tmpr']);
297        }
298    }
299
300    function _ensure_providers_loaded() {
301        if ($this->providers == null) {
302            if ($helper =& plugin_load('helper', 'fedauth')) {
303                $this->providers = $helper->getProviders();
304            }
305        }
306    }
307
308    function _require_user_infrastructure() {
309        require_once(FEDAUTH_PLUGIN . 'common.php');
310        require_once(FEDAUTH_PLUGIN . "classes/fa_base.class.php");
311        require_once(FEDAUTH_PLUGIN . "classes/fa_service.class.php");
312        require_once(FEDAUTH_PLUGIN . "classes/usr/fa_login.usr.class.php");
313    }
314
315    /**
316     * Restores arbitrarily selected variables from the pre-authorization
317     * reqest and stored in a session variable.
318     */
319    function _restoreRequestData() {
320        global $ACT;
321
322        $_REQUEST = $_SESSION[DOKU_COOKIE]['fedauth']['stor']['rq'];
323        $_GET     = $_SESSION[DOKU_COOKIE]['fedauth']['stor']['gt'];
324        $_POST    = $_SESSION[DOKU_COOKIE]['fedauth']['stor']['pt'];
325        $_FILES   = $_SESSION[DOKU_COOKIE]['fedauth']['stor']['fs'];
326        $HTTP_RAW_POST_DATA = $_SESSION[DOKU_COOKIE]['fedauth']['stor']['rp'];
327        // deprecated vars are subject to remove
328        $HTTP_GET_VARS      = $_SESSION[DOKU_COOKIE]['fedauth']['stor']['hg'];
329        $HTTP_POST_VARS     = $_SESSION[DOKU_COOKIE]['fedauth']['stor']['hp'];
330        $HTTP_POST_FILES    = $_SESSION[DOKU_COOKIE]['fedauth']['stor']['hf'];
331
332        unset($_SESSION[DOKU_COOKIE]['fedauth']['stor']);
333
334        if (isset($_REQUEST['do'])) {
335            $ACT = $_REQUEST['do'];
336        }
337    }
338
339    /**
340     * Detects standard Dokuwiki AJAX call.
341     */
342    function isAjaxCall() {
343        return (isset($_POST['call']) || isset($_GET['call']));
344    }
345
346   /**
347    * Detects Dokuwiki PHP script files that do not route thru doku.php
348    */
349   function isLibExe() {
350       return (strpos($_SERVER['PHP_SELF'], DOKU_REL . 'lib/exe/') === 0);
351   }
352
353} /* action_plugin_federate */
354
355/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
356