1<?php
2
3/**
4 * Class helper_plugin_sfauth
5 *
6 * Represents a single oAuth authenticated SalesForce User
7 */
8class helper_plugin_sfauth extends DokuWiki_Plugin {
9
10    /** @var string current user to authenticate */
11    protected $user = null;
12
13    /** @var array user data for above user */
14    protected $userdata = null;
15
16    /** @var array authentication data for above user */
17    protected $authdata = null;
18
19    /** @var int salesforce instance to use */
20    protected $instance = 1;
21
22    /**
23     * Each Instantiated plugin is it's own user
24     *
25     * @return false
26     */
27    public function isSingleton() {
28        return false;
29    }
30
31    /**
32     * The local URL that handles all the oAuth flow
33     *
34     * This is the URL that has to be configured in Salesforce
35     *
36     * @param int $instance the salesforce configuration instance to use (1 to 3)
37     * @return string
38     */
39    public static function getLoginURL($instance) {
40        $instance = (int) $instance;
41        if($instance < 1 || $instance > 3) $instance = 1;
42        return DOKU_URL . DOKU_SCRIPT . '?do=login&u=sf&p=sf&sf='.$instance;
43    }
44
45    /**
46     * Get the current user
47     *
48     * @return bool|string
49     */
50    public function getUser() {
51        if(is_null($this->user)) return false;
52        if(is_null($this->userdata)) return false;
53        return $this->user;
54    }
55
56    /**
57     * Get the user's data
58     *
59     * @return bool|array
60     */
61    public function getUserData() {
62        if(is_null($this->userdata)) return false;
63        return $this->userdata;
64    }
65
66    /**
67     * Initialize the user object by the given user name
68     *
69     * @param $user
70     * @return bool true if the user was found, false otherwise
71     */
72    public function init_by_user($user) {
73        try {
74            $this->loadFromFile($user);
75            return true;
76        } catch (Exception $e) {
77            return false;
78        }
79    }
80
81    /**
82     * Initialize the user by starting an oAuth flow
83     *
84     * @param int $instance Salesforce config instance
85     * @return bool true if the oAuth flow has completed successfully, false on error
86     */
87    public function init_by_oauth($instance) {
88        global $INPUT;
89        global $ID;
90        $instance = (int) $instance;
91        if($instance < 1 || $instance > 3) $instance = 1;
92        $this->instance = $instance;
93
94        // login directly from Saleforce
95        if($INPUT->get->str('user') && $INPUT->get->str('sessionId')) {
96            if($this->oauth_directlogin($INPUT->get->str('user'), $INPUT->get->str('sessionId'), $INPUT->get->str('instance'))) {
97                if($this->loadUserDataFromSalesForce()) {
98                    if($this->saveToFile()) {
99                        $log = array('message' => 'logged in via Salesforce', 'user' => $this->user);
100                        trigger_event('PLUGIN_LOGLOG_LOG',$log);
101                        msg('Authentication successful', 1);
102                        return true;
103                    }
104                }
105            }
106            msg('Oops! something went wrong.', -1);
107            return false;
108        }
109
110        // oAuth step 2: request auth token
111        if($INPUT->get->str('code')) {
112            if($this->oauth_finish($INPUT->get->str('code'), $instance)) {
113                if($this->loadUserDataFromSalesForce()) {
114                    if($this->saveToFile()) {
115                        $log = array('message' => 'logged in via Salesforce', 'user' => $this->user);
116                        trigger_event('PLUGIN_LOGLOG_LOG',$log);
117                        msg('Authentication successful', 1);
118                        return true;
119                    }
120                }
121            }
122            msg('Oops! something went wrong.', -1);
123            return false;
124        }
125
126        // remember the page start started the login
127        $_SESSION['sfauth_id'] = getID();
128
129        // oAuth step 1: redirect to salesforce
130        $this->oauth_start($this->instance);
131        return false; // will not be reached
132    }
133
134    /**
135     * Execute an API call with the current author
136     */
137    public function apicall($method, $endpoint, $data = array(), $usejson = true) {
138        if(!$this->authdata) throw new Exception('No auth data to make API call');
139
140        $json = new JSON(JSON_LOOSE_TYPE);
141        $url  = $this->authdata['instance_url'] . '/services/data/v24.0' . $endpoint;
142
143        $http                           = new DokuHTTPClient();
144        $http->timeout                  = 30;
145        $http->headers['Authorization'] = $this->authdata['access_token'];
146        $http->headers['Accept']        = 'application/json';
147        $http->headers['X-PrettyPrint'] = '1';
148
149        //$http->debug = 1;
150
151        if($data) {
152            if($usejson) {
153                $data                          = $json->encode($data);
154                $http->headers['Content-Type'] = 'application/json';
155            }
156            // else default to standard POST encoding
157        }
158        $http->sendRequest($url, $data, $method);
159        if(!$http->resp_body) {
160            dbglog('err call' . print_r($http, true), 'sfauth');
161            return false;
162        }
163        $resp = $json->decode($http->resp_body);
164
165        // session expired, request a new one and retry
166        if($resp[0]['errorCode'] == 'INVALID_SESSION_ID') {
167            if($this->oauth_refresh()) {
168                return $this->apicall($method, $endpoint, $data);
169            } else {
170                return false;
171            }
172        }
173
174        if($http->status < 200 || $http->status > 399) {
175            dbglog('err call' . print_r($http, true), 'sfauth');
176            return false;
177        }
178
179        return $resp;
180    }
181
182    /**
183     * Initialize the OAuth process
184     *
185     * by redirecting the user to the login site
186     * @link http://bit.ly/y7WOmy
187     */
188    protected function oauth_start($instance) {
189        global $ID;
190        $instance = (int) $instance;
191        if($instance < 1 || $instance > 3) $instance = 1;
192        $this->instance = $instance;
193
194        $_SESSION['sfauth_redirect'] = $ID; // where wanna go later
195
196        $data = array(
197            'response_type' => 'code',
198            'client_id'     => $this->getConf('consumer key'),
199            'redirect_uri'  => self::getLoginURL($this->instance),
200            'display'       => 'page', // may popup
201        );
202
203        $url = $this->getConf('auth url') . '/services/oauth2/authorize?' . buildURLparams($data, '&');
204        send_redirect($url);
205    }
206
207    /**
208     * Request an authentication code with the given request token
209     *
210     * @param string $code request token
211     * @param int $instance Salesforce instance to authenticate with
212     * @return bool
213     */
214    protected function oauth_finish($code, $instance) {
215        $instance = (int) $instance;
216        if($instance < 1 || $instance > 3) $instance = 1;
217        $this->instance = $instance;
218
219        /*
220         * request the authdata with the code
221         */
222        $data = array(
223            'code'          => $code,
224            'grant_type'    => 'authorization_code',
225            'client_id'     => $this->getIConf('consumer key', $this->instance),
226            'client_secret' => $this->getIConf('consumer secret', $this->instance),
227            'redirect_uri'  => self::getLoginURL($this->instance)
228        );
229
230        $url = $this->getConf('auth url') . '/services/oauth2/token';
231        $http                    = new DokuHTTPClient();
232        $http->headers['Accept'] = 'application/json';
233        $resp                    = $http->post($url, $data);
234
235        if($resp === false) return false;
236
237        $json                 = new JSON(JSON_LOOSE_TYPE);
238        $resp                 = $json->decode($resp);
239        $resp['access_token'] = 'OAuth ' . $resp['access_token'];
240
241        $this->authdata = $resp;
242        return true;
243    }
244
245    /**
246     * request a new auth key
247     */
248    protected function oauth_refresh() {
249        if(!$this->authdata) throw new Exception('No auth data to refresh oauth token');
250        if(!isset($this->authdata['refresh_token'])) {
251            return false;
252        }
253        $data = array(
254            'grant_type'    => 'refresh_token',
255            'refresh_token' => $this->authdata['refresh_token'],
256            'client_id'     => $this->getIConf('consumer key', $this->instance),
257            'client_secret' => $this->getIConf('consumer secret', $this->instance)
258        );
259
260        $url                     = $this->getConf('auth url') . '/services/oauth2/token?' . buildURLparams($data, '&');
261        $http                    = new DokuHTTPClient();
262        $http->headers['Accept'] = 'application/json';
263        $resp                    = $http->post($url, array());
264        if($resp === false) return false;
265        $json = new JSON(JSON_LOOSE_TYPE);
266
267        $resp       = $json->decode($resp);
268        $this->authdata = $resp;
269
270        return $this->saveToFile();
271    }
272
273    /**
274     * Does a direct login by setting the given sessionID as access token
275     *
276     * @param string $user
277     * @param string $sessionId
278     * @param string $instanceurl
279     * @return bool
280     */
281    protected function oauth_directlogin($user, $sessionId, $instanceurl) {
282        $url        = parse_url($instanceurl);
283        $this->authdata = array(
284            'id' => $user,
285            'instance_url' => sprintf('%s://%s', $url['scheme'], $url['host']),
286            'access_token' => 'Bearer ' . $sessionId
287        );
288        return true;
289    }
290
291    /**
292     * Load current user's data into memory cache
293     *
294     * @return bool
295     */
296    protected function loadUserDataFromSalesForce() {
297        global $conf;
298        $id = preg_replace('/^.*\//', '', $this->authdata['id']);
299
300        $resp = $this->apicall('GET', '/sobjects/User/' . rawurlencode($id));
301        if(!$resp) return false;
302
303        $this->userdata = array(
304            'name' => $resp['Name'],
305            'mail' => $resp['Email'],
306            'grps' => explode(';', $resp['DokuWiki_Groups__c']),
307            'sfid' => $resp['Id']
308        );
309
310        // add instance as group and default group
311        $this->userdata['grps'][] = 'salesforce'.$this->instance;
312        $this->userdata['grps'][] = $conf['defaultgroup'];
313
314        $this->userdata['grps'] = array_unique($this->userdata['grps']);
315        $this->userdata['grps'] = array_filter($this->userdata['grps']);
316
317        $this->user = $this->transformMailToId($this->userdata['mail']);
318        return true;
319    }
320
321    /**
322     * Transforms a mail to ID
323     *
324     * @todo put this in the getUser() function
325     * @param $mail
326     * @return mixed
327     */
328    protected function transformMailToId($mail) {
329        if(!strpos($mail, '@')) {
330            return $mail;
331        }
332
333        $ownerDomain = $this->getConf('owner domain');
334        if(empty($ownerDomain)) {
335            return $mail;
336        }
337
338        $newMail = preg_replace('/' . preg_quote('@' . $ownerDomain, '/') . '$/i', '', $mail);
339
340        return $newMail;
341    }
342
343    /**
344     * Load user and auth data from local files
345     *
346     * @param $user
347     * @return bool
348     * @throws Exception
349     */
350    protected function loadFromFile($user) {
351        $userdata = getCacheName($user,'.sfuser');
352        $authdata = getCacheName($user,'.sfauth');
353
354        if(file_exists($userdata)) {
355            $this->userdata = unserialize(io_readFile($userdata, false));
356        } else {
357            throw new Exception('No such user');
358        }
359
360        if(file_exists($authdata)) {
361            $this->authdata = unserialize(io_readFile($authdata, false));
362            $this->instance = $this->authdata['dokuwiki-instance'];
363        } else {
364            throw new Exception('No such user');
365        }
366
367        $this->user = $user;
368        return true;
369    }
370
371    /**
372     * Store user and auth data to local files
373     *
374     * @throws Exception
375     * @return bool
376     */
377    protected function saveToFile() {
378        if(!$this->user) throw new Exception('No user info to save');
379
380        $this->authdata['dokuwiki-instance'] = $this->instance;
381
382        $userdata = getCacheName($this->user,'.sfuser');
383        $authdata = getCacheName($this->user,'.sfauth');
384        $ok1 = io_saveFile($userdata, serialize($this->userdata));
385        $ok2 = io_saveFile($authdata, serialize($this->authdata));
386
387        return $ok1 && $ok2;
388    }
389
390    /**
391     * Get a config setting for the specified instance
392     *
393     * @param $config
394     * @param $instance
395     * @return mixed
396     */
397    protected function getIConf($config, $instance) {
398        if($instance === 2 || $instance === 3) {
399            $postfix = ' '.$instance;
400        } else {
401            $postfix = '';
402        }
403
404        return $this->getConf($config.$postfix);
405    }
406
407}