Completed
Pull Request — master (#39)
by
unknown
02:33
created

OpauthController::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
/**
4
 * OpauthController
5
 * Wraps around Opauth for handling callbacks.
6
 * The SS equivalent of "index.php" and "callback.php" in the Opauth package.
7
 * @author Will Morgan <@willmorgan>
8
 * @author Dan Hensby <@dhensby>
9
 * @copyright Copyright (c) 2013, Better Brief LLP
10
 */
11
class OpauthController extends ContentController {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
12
13
	private static
14
		$allowed_actions = array(
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $allowed_actions.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
15
			'index',
16
			'finished',
17
			'profilecompletion',
18
			'RegisterForm',
19
		),
20
		$url_handlers = array(
0 ignored issues
show
Unused Code introduced by
The property $url_handlers is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
21
			'finished' => 'finished',
22
		);
23
24
	/**
25
	 * Bitwise indicators to extensions what sort of action is happening
26
	 */
27
	const
28
		/**
29
		 * LOGIN = already a user with an OAuth ID
30
		 */
31
		AUTH_FLAG_LOGIN = 2,
32
		/**
33
		 * LINK = already a user, linking a new OAuth ID
34
		 */
35
		AUTH_FLAG_LINK = 4,
36
		/**
37
		 * REGISTER = new user, linking OAuth ID
38
		 */
39
		AUTH_FLAG_REGISTER = 8;
40
41
	protected
42
		$registerForm;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $registerForm.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
43
44
	/**
45
	 * Fake a Page_Controller by using that class as a failover
46
	 */
47
	public function __construct($dataRecord = null) {
48
		if(class_exists('Page_Controller')) {
49
			$dataRecord = new Page_Controller($dataRecord);
50
		}
51
		parent::__construct($dataRecord);
52
	}
53
54
	/**
55
	 * Prepare the controller for handling the response to this request
56
	 *
57
	 * @param string $title Title to use
58
	 * @return Controller
59
	 */
60
	protected function getResponseController($title) {
61
		if(!class_exists('SiteTree')) return $this;
62
63
		// Use sitetree pages to render the opauth pages
64
		$tmpPage = new Page();
65
		
66
		$controller = ModelAsController::controller_for($tmpPage);
67
		$controller->init();
68
		return $controller;
69
	}
70
71
	/**
72
	 * This function only catches the request to pass it straight on.
73
	 * Opauth uses the last segment of the URL to identify the auth method.
74
	 * In _routes.yml we enforce a $Strategy request parameter to enforce this.
75
	 * Equivalent to "index.php" in the Opauth package.
76
	 * @todo: Validate the strategy works before delegating to Opauth.
77
	 */
78
	public function index(SS_HTTPRequest $request) {
79
80
		$strategy = $request->param('Strategy');
81
		$method = $request->param('StrategyMethod');
82
83
		if(!isset($strategy)) {
84
			return Security::permissionFailure($this);
85
		}
86
87
		// If there is no method then we redirect (not a callback)
88
		if(!isset($method)) {
89
			// Redirects:
90
			OpauthAuthenticator::opauth(true);
91
		}
92
		else {
93
			return $this->oauthCallback($request);
94
		}
95
	}
96
97
	/**
98
	 * This is executed when the Oauth provider redirects back to us
99
	 * Opauth handles everything sent back in this request.
100
	 */
101
	protected function oauthCallback(SS_HTTPRequest $request) {
102
103
		// Set up and run opauth with the correct params from the strategy:
104
		OpauthAuthenticator::opauth(true, array(
105
			'strategy'	=> $request->param('Strategy'),
106
			'action'	=> $request->param('StrategyMethod'),
107
		));
108
109
	}
110
111
	/**
112
	 * Equivalent to "callback.php" in the Opauth package.
113
	 * If there is a problem with the response, we throw an HTTP error.
114
	 * When done validating, we return back to the Authenticator continue auth.
115
	 * @throws SS_HTTPResponse_Exception if any validation errors
116
	 */
117
	public function finished(SS_HTTPRequest $request) {
118
119
		$opauth = OpauthAuthenticator::opauth(false);
120
121
		$response = $this->getOpauthResponse();
122
123
		if (!$response) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $response of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
124
			$response = array();
125
		}
126
		// Clear the response as it is only to be read once (if Session)
127
		Session::clear('opauth');
128
129
		// Handle all Opauth validation in this handy function
130
		try {
131
			$this->validateOpauthResponse($opauth, $response);
132
		}
133
		catch(OpauthValidationException $e) {
134
			return $this->handleOpauthException($e);
135
		}
136
137
		$identity = OpauthIdentity::factory($response);
138
139
		$member = $identity->findOrCreateMember();
140
141
		// If the member exists, associate it with the identity and log in
142
		if($member->isInDB() && $member->validate()->valid()) {
143
			if(!$identity->exists()) {
144
				$identity->write();
145
				$flag = self::AUTH_FLAG_LINK;
146
			}
147
			else {
148
				$flag = self::AUTH_FLAG_LOGIN;
149
			}
150
151
			Session::set('OpauthIdentityID', $identity->ID);
152
		}
153
		else {
154
155
			$flag = self::AUTH_FLAG_REGISTER;
156
157
			// Write the identity
158
			$identity->write();
159
160
			// Keep a note of the identity ID
161
			Session::set('OpauthIdentityID', $identity->ID);
162
163
			// Even if written, check validation - we might not have full fields
164
			$validationResult = $member->validate();
165
			if(!$validationResult->valid()) {
166
				// Set up the register form before it's output
167
				$regForm = $this->RegisterForm();
168
				$regForm->loadDataFrom($member);
169
				$regForm->setSessionData($member);
170
				$regForm->validate();
171
				return $this->redirect($this->Link('profilecompletion'));
172
			}
173
			else {
174
				$member->extend('onBeforeOpauthRegister');
175
				$member->write();
176
				$identity->MemberID = $member->ID;
0 ignored issues
show
Documentation introduced by
The property MemberID does not exist on object<OpauthIdentity>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
177
				$identity->write();
178
			}
179
		}
180
		return $this->loginAndRedirect($member, $identity, $flag);
181
	}
182
183
	/**
184
	 * @param Member
185
	 * @param OpauthIdentity
186
	 * @param int $mode One or more AUTH_FLAGs.
187
	 */
188
	protected function loginAndRedirect(Member $member, OpauthIdentity $identity, $mode) {
189
		// Back up the BackURL as Member::logIn regenerates the session
190
		$backURL = Session::get('BackURL');
191
192
		// Check if we can log in:
193
		$canLogIn = $member->canLogIn();
194
195
		if(!$canLogIn->valid()) {
196
			$extendedURLs = $this->extend('getCantLoginBackURL', $member, $identity, $canLogIn, $mode);
197
			if(count($extendedURLs)) {
198
				$redirectURL = array_pop($extendedURLs);
199
				$this->redirect($redirectURL, 302);
200
				return;
201
			}
202
			Security::permissionFailure($this, $canLogIn->message());
203
			return;
204
		}
205
206
		// Decide where to go afterwards...
207
		if(!empty($backURL)) {
208
			$redirectURL = $backURL;
209
		}
210
		else {
211
			$redirectURL = Security::config()->default_login_dest;
212
		}
213
214
		$extendedURLs = $this->extend('getSuccessBackURL', $member, $identity, $redirectURL, $mode);
215
216
		if(count($extendedURLs)) {
217
			$redirectURL = array_pop($extendedURLs);
218
		}
219
220
		$member->logIn();
221
222
		// Clear any identity ID
223
		Session::clear('OpauthIdentityID');
224
		
225
		// Clear the BackURL
226
		Session::clear('BackURL');
227
228
		return $this->redirect($redirectURL);
229
	}
230
231
	public function profilecompletion(SS_HTTPRequest $request = null) {
232
		// Get response handler
233
		$controller = $this->getResponseController(_t('Opauth.PROFILECOMPLETIONTITLE', 'Complete your profile'));
0 ignored issues
show
Unused Code introduced by
$controller is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
234
		
235
		if(!Session::get('OpauthIdentityID')) {
236
			Security::permissionFailure($this);
237
		}
238
		// Redirect to complete register step by adding in extra info
239
		return $this->renderWith(array(
240
				'OpauthController_profilecompletion',
241
				'Security_profilecompletion',
242
				'Page',
243
			)
244
		);
245
	}
246
247
	public function RegisterForm(SS_HTTPRequest $request = null, Member $member = null, $result = null) {
248
		if(!isset($this->registerForm)) {
249
			$form = Injector::inst()->create('OpauthRegisterForm', $this, 'RegisterForm', $result);
250
			$form->populateFromSources($request, $member, $result);
251
			// Set manually the form action due to how routing works
252
			$form->setFormAction(Controller::join_links(
253
				self::config()->opauth_path,
254
				'RegisterForm'
255
			));
256
			$this->registerForm = $form;
257
		}
258
		else {
259
			$this->registerForm->populateFromSources($request, $member, $result);
260
		}
261
		return $this->registerForm;
262
	}
263
264
	public function doCompleteRegister($data, $form, $request) {
265
		$member = new Member();
266
		$form->saveInto($member);
267
		$identityID = Session::get('OpauthIdentityID');
268
		$identity = DataObject::get_by_id('OpauthIdentity', $identityID);
269
		$validationResult = $member->validate();
270
		$existing = Member::get()->filter('Email', $member->Email)->first();
271
		$emailCollision = $existing && $existing->exists();
272
		// If not valid then we have to manually transpose errors to the form
273
		if(!$validationResult->valid() || $emailCollision) {
274
			$errors = $validationResult->messageList();
275
			$form->setRequiredFields($errors);
276
			// Mandatory check on the email address
277
			if($emailCollision) {
278
				$form->addErrorMessage('Email', _t(
279
					'OpauthRegisterForm.ERROREMAILTAKEN',
280
					'It looks like this email has already been used'
281
				), 'required');
282
			}
283
			return $this->redirect('profilecompletion');
284
		}
285
		// If valid then write and redirect
286
		else {
287
			$member->extend('onBeforeOpauthRegister');
288
			$member->write();
289
			$identity->MemberID = $member->ID;
290
			$identity->write();
291
			return $this->loginAndRedirect($member, $identity, self::AUTH_FLAG_REGISTER);
292
		}
293
	}
294
295
	/**
296
	 * Returns the response from the Oauth callback.
297
	 * @throws InvalidArugmentException
298
	 * @return array The response
299
	 */
300
	protected function getOpauthResponse() {
301
		$config = OpauthAuthenticator::get_opauth_config();
302
		$transportMethod = $config['callback_transport'];
303
		switch($transportMethod) {
304
			case 'session':
305
				return $this->getResponseFromSession();
306
			case 'get':
307
			case 'post':
308
				return $this->getResponseFromRequest($transportMethod);
309
			default:
310
				throw new InvalidArgumentException('Invalid transport method: ' . $transportMethod);
311
		}
312
	}
313
314
	/**
315
	 * Validates the Oauth response for Opauth.
316
	 * @throws InvalidArgumentException
317
	 */
318
	protected function validateOpauthResponse($opauth, $response) {
319
		if(!empty($response['error'])) {
320
			throw new OpauthValidationException('Oauth provider error', 1, $response['error']);
321
		}
322
323
		// Required components within the response
324
		$this->requireResponseComponents(
325
			array('auth', 'timestamp', 'signature'),
326
			$response
327
		);
328
329
		// More required components within the auth section...
330
		$this->requireResponseComponents(
331
			array('provider', 'uid'),
332
			$response['auth']
333
		);
334
335
		$invalidReason = '';
336
337
		/**
338
		 * @todo: improve this signature check. it's a bit weak.
339
		 */
340
		if(!$opauth->validate(
341
			sha1(print_r($response['auth'], true)),
342
			$response['timestamp'],
343
			$response['signature'],
344
			$invalidReason
345
		)) {
346
			throw new OpauthValidationException('Invalid auth response', 3, $invalidReason);
347
		}
348
	}
349
350
	/**
351
	 * Shorthand for quickly finding missing components and complaining about it
352
	 * @throws InvalidArgumentException
353
	 */
354
	protected function requireResponseComponents(array $components, $response) {
355
		foreach($components as $component) {
356
			if(empty($response[$component])) {
357
				throw new OpauthValidationException('Required component missing', 2, $component);
358
			}
359
		}
360
	}
361
362
	/**
363
	 * @return array Opauth response from session
364
	 */
365
	protected function getResponseFromSession() {
366
		return Session::get('opauth');
367
	}
368
369
	/**
370
	 * @param OpauthValidationException $e
371
	 */
372
	protected function handleOpauthException(OpauthValidationException $e) {
373
		$data = $e->getData();
374
		$loginFormName = 'OpauthLoginForm_LoginForm';
375
		$message = '';
376
		switch($e->getCode()) {
377
			case 1: // provider error
378
				$message = _t(
379
					'OpauthLoginForm.OAUTHFAILURE',
380
					'There was a problem logging in with {provider}.',
381
					array(
0 ignored issues
show
Documentation introduced by
array('provider' => $data['provider']) is of type array<string,?,{"provider":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
382
						'provider' => $data['provider'],
383
					)
384
				);
385
				break;
386
			case 2: // validation error
387
			case 3: // invalid auth response
388
				$message = _t(
389
					'OpauthLoginForm.RESPONSEVALIDATIONFAILURE',
390
					'There was a problem logging in - {message}',
391
					array(
0 ignored issues
show
Documentation introduced by
array('message' => $e->getMessage()) is of type array<string,string,{"message":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
392
						'message' => $e->getMessage(),
393
					)
394
				);
395
				break;
396
		}
397
		// Set form message, redirect to login with permission failure
398
		Form::messageForForm($loginFormName, $message, 'bad');
399
		// always redirect to login
400
		Security::permissionFailure($this, $message);
401
	}
402
403
	/**
404
	 * Looks at $method (GET, POST, PUT etc) for the response.
405
	 * @return array Opauth response
406
	 */
407
	protected function getResponseFromRequest($method) {
408
		return unserialize(base64_decode($this->request->{$method.'Var'}('opauth')));
409
	}
410
411
	public function Link($action = null) {
412
		return Controller::join_links(
413
			self::config()->opauth_path,
414
			$action
415
		);
416
	}
417
418
	/**
419
	 * 'path' param for use in Opauth's config
420
	 * MUST have trailling slash for Opauth needs
421
	 * @return string
422
	 */
423
	public static function get_path() {
424
		return Controller::join_links(
425
			self::config()->opauth_path,
426
			'strategy/'
427
		);
428
	}
429
430
	/**
431
	 * 'callback_url' param for use in Opauth's config
432
	 * MUST have trailling slash for Opauth needs
433
	 * @return string
434
	 */
435
	public static function get_callback_path() {
436
		return Controller::join_links(
437
			self::config()->opauth_path,
438
			'finished/'
439
		);
440
	}
441
442
////**** Template variables ****////
443
	function Title() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
444
		if($this->action == 'profilecompletion') {
445
			return _t('OpauthController.PROFILECOMPLETIONTITLE', 'Complete your profile');
446
		}
447
		return _t('OpauthController.TITLE', 'Social Login');
448
	}
449
450
	public function Form() {
451
		return $this->RegisterForm();
452
	}
453
////**** END Template variables ****////
454
455
}
456