1: <?php
2: /*****************************************************************************************
3: * X2Engine Open Source Edition is a customer relationship management program developed by
4: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
5: *
6: * This program is free software; you can redistribute it and/or modify it under
7: * the terms of the GNU Affero General Public License version 3 as published by the
8: * Free Software Foundation with the addition of the following permission added
9: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
10: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
11: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
12: *
13: * This program is distributed in the hope that it will be useful, but WITHOUT
14: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
16: * details.
17: *
18: * You should have received a copy of the GNU Affero General Public License along with
19: * this program; if not, see http://www.gnu.org/licenses or write to the Free
20: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21: * 02110-1301 USA.
22: *
23: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
24: * California 95067, USA. or at email address [email protected].
25: *
26: * The interactive user interfaces in modified source and object code versions
27: * of this program must display Appropriate Legal Notices, as required under
28: * Section 5 of the GNU Affero General Public License version 3.
29: *
30: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
31: * these Appropriate Legal Notices must retain the display of the "Powered by
32: * X2Engine" logo. If the display of the logo is not reasonably feasible for
33: * technical reasons, the Appropriate Legal Notices must display the words
34: * "Powered by X2Engine".
35: *****************************************************************************************/
36:
37: require_once 'protected/integration/Google/google-api-php-client/src/Google/autoload.php';
38:
39: /**
40: * Wrapper class for interaction with Google's API and authentication methods.
41: * This is designed to handle all user authentication and returning of Google API
42: * Client classes in an easy to use manner. Much of the code is from Google's stock
43: * PHP API examples, but it has been modified to be usable with our software and
44: * as such some of the comments/classes are Google developers' not mine.
45: */
46: class GoogleAuthenticator {
47:
48: /**
49: * Client ID of the Google API Project
50: * @var string
51: */
52: public $clientId = '';
53:
54: /**
55: * Client secret of the Google API Project
56: * @var string
57: */
58: public $clientSecret = '';
59:
60: /**
61: * Redirect URI for the authentication request
62: * @var string
63: */
64: public $redirectUri = '';
65:
66: /**
67: * A list of scopes required by the Google API to use for Google Integration
68: * within the software. This list defines the permissions that Google will ask
69: * for when a user is authenticating with them and X2.
70: * @var array
71: */
72: public $scopes = array(
73: 'https://www.googleapis.com/auth/plus.login', // Google+ login required to login with Google
74: 'https://www.googleapis.com/auth/drive', // Drive required for drive integration
75: 'https://www.googleapis.com/auth/userinfo.email', // Email required for Google login
76: 'https://www.googleapis.com/auth/userinfo.profile', // Basic profile info required for Google login
77: 'https://www.googleapis.com/auth/calendar', // Calendar required for Calendar sync
78: 'https://www.googleapis.com/auth/calendar.readonly', // Read only Calendar required for Calendar list
79: );
80:
81: /**
82: * An array of errors to be returned or displayed in case something goes wrong.
83: * @var array
84: */
85: private $_errors;
86:
87: /**
88: * Master control variable that prevents most methods being called unless
89: * Google Integration is enabled in the admin settings.
90: * @var boolean
91: */
92: private $_enabled;
93:
94: /**
95: * Constructor that sets up the Authenticator with all the required data to
96: * connect to Google properly.
97: */
98: public function __construct() {
99: $this->_enabled = Yii::app()->settings->googleIntegration; // Check if integration is enabled in the first place
100: $credentials = Yii::app()->settings->getGoogleIntegrationCredentials ();
101: if($this->_enabled){
102: $this->clientId = $credentials['clientId'];
103: $this->clientSecret = $credentials['clientSecret'];
104: if(empty($this->redirectUri)){
105: $this->redirectUri = (@$_SERVER['HTTPS'] == 'on' ? 'https://' : 'http://').
106: $_SERVER['HTTP_HOST'].Yii::app()->controller->createUrl('');
107: }
108: }
109: }
110:
111: /**
112: * Retrieved stored credentials for the provided user ID.
113: *
114: * @param String $userId User's ID.
115: * @return String Json representation of the OAuth 2.0 credentials.
116: */
117: public function getStoredCredentials($userId){
118: $profile = X2Model::model('Profile')->findByPk($userId);
119: if(isset($profile)){
120: return $profile->googleRefreshToken;
121: }
122: return null;
123: }
124:
125: /**
126: * Store OAuth 2.0 credentials in the application's database.
127: *
128: * @param Integer $userId User's ID.
129: * @param String $credentials Json representation of the OAuth 2.0 credentials to
130: store.
131: */
132: public function storeCredentials($userId, $credentials){
133: $profile = X2Model::model('Profile')->findByPk($userId);
134: $credentialsArray = json_decode($credentials, true);
135: if(isset($profile) && isset($credentialsArray['refresh_token'])){
136: $profile->googleRefreshToken = $credentialsArray['refresh_token'];
137: $profile->update(array('googleRefreshToken'));
138: }
139: }
140:
141: /**
142: * Exchange an authorization code for OAuth 2.0 credentials.
143: *
144: * @param String $authorizationCode Authorization code to exchange for OAuth 2.0
145: * credentials.
146: * @return String Json representation of the OAuth 2.0 credentials.
147: * @throws CodeExchangeException An error occurred.
148: */
149: public function exchangeCode($authorizationCode){
150: if($this->_enabled){
151: try{
152: $client = new Google_Client();
153: $client->setClientId($this->clientId);
154: $client->setClientSecret($this->clientSecret);
155: $client->setRedirectUri($this->redirectUri);
156: $_GET['code'] = $authorizationCode;
157: return $client->authenticate($authorizationCode);
158: }catch(Google_Auth_Exception $e){
159: $this->setErrors($e->getMessage());
160: throw new CodeExchangeException(null);
161: }
162: }else{
163: return false;
164: }
165: }
166:
167: /**
168: * Send a request to the UserInfo API to retrieve the user's information.
169: *
170: * @param String credentials OAuth 2.0 credentials to authorize the request.
171: * @return Userinfo User's information.
172: * @throws NoUserIdException An error occurred.
173: */
174: public function getUserInfo($credentials){
175: if($this->_enabled){
176: $apiClient = new Google_Client();
177: $apiClient->setAccessToken($credentials);
178: $userInfoService = new Google_Service_Oauth2 ($apiClient);
179: $userInfo = null;
180: try{
181: $userInfo = $userInfoService->userinfo->get();
182: }catch(Google_Exception $e){
183: $this->setErrors($e->getMessage());
184: }
185: if($userInfo != null && $userInfo->getId() != null){
186: return $userInfo;
187: }else{
188: throw new NoUserIdException();
189: }
190: }else{
191: return false;
192: }
193: }
194:
195: /**
196: * Retrieve the authorization URL.
197: *
198: * @param String $emailAddress User's e-mail address.
199: * @param String $state State for the authorization URL.
200: * @return String Authorization URL to redirect the user to.
201: */
202: public function getAuthorizationUrl($state){
203: if($this->_enabled){
204: $client = new Google_Client();
205:
206: $client->setClientId($this->clientId);
207: switch($state){
208: case 'calendar':
209: $_SESSION['calendarForceRefresh']=1;
210: $client->setRedirectUri(
211: (@$_SERVER['HTTPS'] == 'on' ? 'https://' : 'http://').$_SERVER['HTTP_HOST'].
212: Yii::app()->controller->createUrl(
213: '/calendar/calendar/syncActionsToGoogleCalendar'));
214: break;
215: default:
216: $client->setRedirectUri($this->redirectUri);
217: }
218: $client->setAccessType('offline');
219: $client->setApprovalPrompt('force');
220: $client->setState($state);
221: $client->setScopes($this->scopes);
222:
223: return $client->createAuthUrl();
224: }else{
225: return false;
226: }
227: // $tmpUrl = parse_url($client->createAuthUrl());
228: // $query = explode('&', $tmpUrl['query']);
229: // $query[] = 'user_id='.urlencode($emailAddress);
230: // return
231: // $tmpUrl['scheme'].'://'.$tmpUrl['host'].$tmpUrl['port'].
232: // $tmpUrl['path'].'?'.implode('&', $query);
233: }
234:
235: /**
236: * Retrieve credentials using the provided authorization code.
237: *
238: * This function exchanges the authorization code for an access token and
239: * queries the UserInfo API to retrieve the user's e-mail address. If a
240: * refresh token has been retrieved along with an access token, it is stored
241: * in the application database using the user's e-mail address as key. If no
242: * refresh token has been retrieved, the function checks in the application
243: * database for one and returns it if found or throws a NoRefreshTokenException
244: * with the authorization URL to redirect the user to.
245: *
246: * @param String authorizationCode Authorization code to use to retrieve an access
247: * token.
248: * @param String state State to set to the authorization URL in case of error.
249: * @return String Json representation of the OAuth 2.0 credentials.
250: * @throws NoRefreshTokenException No refresh token could be retrieved from
251: * the available sources.
252: */
253: public function getCredentials($authorizationCode, $state){
254: if($this->_enabled){
255: try{
256: $credentials = $this->exchangeCode($authorizationCode);
257: $userId = Yii::app()->user->getId();
258: $credentialsArray = json_decode($credentials, true);
259: if(isset($credentialsArray['refresh_token'])){
260: if(!empty($userId)){
261: $this->storeCredentials($userId, $credentials);
262: }
263: return $credentials;
264: }else{
265: $credentials = $this->getStoredCredentials($userId);
266: $credentialsArray = json_decode($credentials, true);
267: if($credentials != null &&
268: isset($credentialsArray['refresh_token'])){
269: return $credentials;
270: }
271: }
272: }catch(CodeExchangeException $e){
273: $this->setErrors($e->getMessage());
274: // Drive apps should try to retrieve the user and credentials for the current
275: // session.
276: // If none is available, redirect the user to the authorization URL.
277: $e->setAuthorizationUrl($this->getAuthorizationUrl($state));
278: throw $e;
279: }catch(NoUserIdException $e){
280: $this->setErrors('No e-mail address could be retrieved.');
281: }
282: // No refresh token has been retrieved.
283: $authorizationUrl = $this->getAuthorizationUrl($state);
284: throw new NoRefreshTokenException($authorizationUrl);
285: }else{
286: return false;
287: }
288: }
289:
290: /**
291: * Sometimes, terrible things happen. When an auth error occurs or a problem
292: * with the credentials arises, flush every place they're stored immediately
293: * to stop any errors badly provided credentials may be causing.
294: *
295: * @param boolean full Whether or not to flush all credentials or just temporary
296: * ones. This is useful because the token in the session will not contain a refresh
297: * token in most cases, but the refresh token may still be valid. In that case,
298: * just clearing the session tokens will allow for another attempt using the
299: * refresh token.
300: */
301: public function flushCredentials($full = true){
302: if($this->_enabled){
303: unset($_SESSION['access_token']);
304: unset($_SESSION['token']);
305: unset($_GET['code']);
306: $profile = Yii::app()->params->profile;
307: if($full && isset($profile)){
308: $profile->googleRefreshToken = null;
309: $profile->update(array('googleRefreshToken'));
310: }
311: }
312: }
313:
314: /**
315: * This function is used to get the current access token. This function is
316: * vital to the class as it allows any code using the Authenticator to discover
317: * if a connection to Google has been made and if any actions connecting to
318: * Google can procede. This function is called in a large number of places
319: * as a gate to continuing integration tasks.
320: * @param Integer $userId The ID of the current User
321: * @return String|boolean Returns either the JSON encoded access token, or false on failure
322: */
323: public function getAccessToken($userId = null){
324: if($this->_enabled){
325: $client = new Google_Client();
326: $client->setClientId($this->clientId);
327: $client->setClientSecret($this->clientSecret);
328: if(empty($userId)){
329: $userId = Yii::app()->user->getId();
330: }
331: if(isset($_SESSION['access_token'])){ // The access token is already stored in the session, return it.
332: // $token=json_decode($_SESSION['access_token']);
333: // $reqUrl = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token='.
334: // $token->access_token;
335: // $req = new Google_Http_Request($reqUrl);
336: //
337: // $tokenInfo = json_decode(
338: // $client::getIo()->authenticatedRequest($req)->getResponseBody());
339: // if(!isset($tokenInfo->error)){
340: return $_SESSION['access_token'];
341: // }
342: }
343: if(!empty($userId) && !is_null($this->getStoredCredentials($userId))){ // We found a stored refresh token
344: $refreshToken = $this->getStoredCredentials($userId);
345: try{
346: $client->refreshToken($refreshToken); // Try to get an access token based on the stored refresh token
347: $credentials = $client->getAccessToken(); // No recursion, this is a different function
348: $_SESSION['token'] = $credentials; // Set credentials as a session variable for quicker lookup.
349: $_SESSION['access_token'] = $credentials;
350: return $credentials;
351: }catch(Google_Auth_Exception $e){
352: $profile = Yii::app()->params->profile;
353: if(isset($profile)){ // If there was an error using the refresh token, remove it from the database so it can't cause issues.
354: $profile->googleRefreshToken = null;
355: $profile->update(array('googleRefreshToken'));
356: }
357: return false;
358: }
359: }
360: if(isset($_GET['code'])){ // There is a Google auth code in the GET request header.
361: try{
362: $credentials = $this->getCredentials($_GET['code'], null); // Attempt to exchange the auth code for an access token.
363: $_SESSION['token'] = $credentials;
364: $_SESSION['access_token'] = $credentials;
365: return $credentials;
366: }catch(CodeExchangeException $e){
367: return false;
368: }
369: }
370: }
371: return false; // No token was ever returned due to data not being set or exceptions. Return false to indicate a failure.
372: }
373:
374: public function getDriveService(){
375: if($this->getAccessToken()){
376: $client = new Google_Client();
377: $client->setClientId($this->clientId);
378: $client->setClientSecret($this->clientSecret);
379: $client->setRedirectUri($this->redirectUri);
380: $client->setScopes(array('https://www.googleapis.com/auth/drive'));
381: $client->setAccessToken($this->getAccessToken());
382: return new Google_Service_Drive ($client);
383: }else{
384: return false;
385: }
386: }
387:
388: public function getCalendarService(){
389: if($this->getAccessToken()){
390: $client = new Google_Client();
391: $client->setClientId($this->clientId);
392: $client->setClientSecret($this->clientSecret);
393: $client->setRedirectUri($this->redirectUri);
394: $client->setScopes(array(
395: 'https://www.googleapis.com/auth/calendar',
396: 'https://www.googleapis.com/auth/calendar.readonly'));
397: $client->setAccessToken($this->getAccessToken());
398: return new Google_Service_Calendar($client);
399: }else{
400: return false;
401: }
402: }
403:
404: public function setErrors($message){
405: if(!is_array($this->_errors)){
406: $this->_errors = array(
407: $message
408: );
409: }else{
410: $this->_errors[] = $message;
411: }
412: }
413:
414: public function getErrors(){
415: if(!is_array($this->_errors) || empty($this->_errors)){
416: return false;
417: }else{
418: return $this->_errors;
419: }
420: }
421:
422: }
423:
424: // All code below this point is stock Google code which I have not modified.
425:
426: /**
427: * Exception thrown when an error occurred while retrieving credentials.
428: */
429: class GetCredentialsException extends Exception {
430:
431: protected $authorizationUrl;
432:
433: /**
434: * Construct a GetCredentialsException.
435: *
436: * @param authorizationUrl The authorization URL to redirect the user to.
437: */
438: public function __construct($authorizationUrl){
439: $this->authorizationUrl = $authorizationUrl;
440: }
441:
442: /**
443: * @return the authorizationUrl.
444: */
445: public function getAuthorizationUrl(){
446: return $this->authorizationUrl;
447: }
448:
449: /**
450: * Set the authorization URL.
451: */
452: public function setAuthorizationurl($authorizationUrl){
453: $this->authorizationUrl = $authorizationUrl;
454: }
455:
456: }
457:
458: /**
459: * Exception thrown when no refresh token has been found.
460: */
461: class NoRefreshTokenException extends GetCredentialsException {
462:
463: }
464:
465: /**
466: * Exception thrown when a code exchange has failed.
467: */
468: class CodeExchangeException extends GetCredentialsException {
469:
470: }
471:
472: /**
473: * Exception thrown when no user ID could be retrieved.
474: */
475: class NoUserIdException extends Exception {
476:
477: }
478:
479: ?>
480: