OpauthController::requireResponseComponents()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 3
eloc 4
nc 3
nop 2
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
	 * This function only catches the request to pass it straight on.
56
	 * Opauth uses the last segment of the URL to identify the auth method.
57
	 * In _routes.yml we enforce a $Strategy request parameter to enforce this.
58
	 * Equivalent to "index.php" in the Opauth package.
59
	 * @todo: Validate the strategy works before delegating to Opauth.
60
	 */
61
	public function index(SS_HTTPRequest $request) {
62
63
		$strategy = $request->param('Strategy');
64
		$method = $request->param('StrategyMethod');
65
66
		if(!isset($strategy)) {
67
			return Security::permissionFailure($this);
68
		}
69
70
		// If there is no method then we redirect (not a callback)
71
		if(!isset($method)) {
72
			// Redirects:
73
			OpauthAuthenticator::opauth(true);
74
		}
75
		else {
76
			return $this->oauthCallback($request);
77
		}
78
	}
79
80
	/**
81
	 * This is executed when the Oauth provider redirects back to us
82
	 * Opauth handles everything sent back in this request.
83
	 */
84
	protected function oauthCallback(SS_HTTPRequest $request) {
85
86
		// Set up and run opauth with the correct params from the strategy:
87
		OpauthAuthenticator::opauth(true, array(
88
			'strategy'	=> $request->param('Strategy'),
89
			'action'	=> $request->param('StrategyMethod'),
90
		));
91
92
	}
93
94
	/**
95
	 * Equivalent to "callback.php" in the Opauth package.
96
	 * If there is a problem with the response, we throw an HTTP error.
97
	 * When done validating, we return back to the Authenticator continue auth.
98
	 * @throws SS_HTTPResponse_Exception if any validation errors
99
	 */
100
	public function finished(SS_HTTPRequest $request) {
101
102
		$opauth = OpauthAuthenticator::opauth(false);
103
104
		$response = $this->getOpauthResponse();
105
106
		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...
107
			$response = array();
108
		}
109
		// Clear the response as it is only to be read once (if Session)
110
		Session::clear('opauth');
111
112
		// Handle all Opauth validation in this handy function
113
		try {
114
			$this->validateOpauthResponse($opauth, $response);
115
		}
116
		catch(OpauthValidationException $e) {
117
			return $this->handleOpauthException($e);
118
		}
119
120
		$identity = OpauthIdentity::factory($response);
121
122
		$member = $identity->findOrCreateMember();
123
124
		// If the member exists, associate it with the identity and log in
125
		if($member->isInDB() && $member->validate()->valid()) {
126
			if(!$identity->exists()) {
127
				$identity->write();
128
				$flag = self::AUTH_FLAG_LINK;
129
			}
130
			else {
131
				$flag = self::AUTH_FLAG_LOGIN;
132
			}
133
134
			Session::set('OpauthIdentityID', $identity->ID);
135
		}
136
		else {
137
138
			$flag = self::AUTH_FLAG_REGISTER;
139
140
			// Write the identity
141
			$identity->write();
142
143
			// Keep a note of the identity ID
144
			Session::set('OpauthIdentityID', $identity->ID);
145
146
			// Even if written, check validation - we might not have full fields
147
			$validationResult = $member->validate();
148
			if(!$validationResult->valid()) {
149
				// Set up the register form before it's output
150
				$regForm = $this->RegisterForm();
151
				$regForm->loadDataFrom($member);
152
				$regForm->setSessionData($member);
153
				$regForm->validate();
154
				return $this->redirect($this->Link('profilecompletion'));
155
			}
156
			else {
157
				$member->extend('onBeforeOpauthRegister');
158
				$member->write();
159
				$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...
160
				$identity->write();
161
			}
162
		}
163
		return $this->loginAndRedirect($member, $identity, $flag);
164
	}
165
166
	/**
167
	 * @param Member
168
	 * @param OpauthIdentity
169
	 * @param int $mode One or more AUTH_FLAGs.
170
	 */
171
	protected function loginAndRedirect(Member $member, OpauthIdentity $identity, $mode) {
172
		// Back up the BackURL as Member::logIn regenerates the session
173
		$backURL = Session::get('BackURL');
174
175
		// Check if we can log in:
176
		$canLogIn = $member->canLogIn();
177
178
		if(!$canLogIn->valid()) {
179
			$extendedURLs = $this->extend('getCantLoginBackURL', $member, $identity, $canLogIn, $mode);
180
			if(count($extendedURLs)) {
181
				$redirectURL = array_pop($extendedURLs);
182
				$this->redirect($redirectURL, 302);
183
				return;
184
			}
185
			Security::permissionFailure($this, $canLogIn->message());
186
			return;
187
		}
188
189
		// Decide where to go afterwards...
190
		if(!empty($backURL)) {
191
			$redirectURL = $backURL;
192
		}
193
		else {
194
			$redirectURL = Security::config()->default_login_dest;
195
		}
196
197
		$extendedURLs = $this->extend('getSuccessBackURL', $member, $identity, $redirectURL, $mode);
198
199
		if(count($extendedURLs)) {
200
			$redirectURL = array_pop($extendedURLs);
201
		}
202
203
		$member->logIn();
204
205
		// Clear any identity ID
206
		Session::clear('OpauthIdentityID');
207
		
208
		// Clear the BackURL
209
		Session::clear('BackURL');
210
211
		return $this->redirect($redirectURL);
212
	}
213
214
	public function profilecompletion(SS_HTTPRequest $request = null) {
215
		if(!Session::get('OpauthIdentityID')) {
216
			Security::permissionFailure($this);
217
		}
218
		// Redirect to complete register step by adding in extra info
219
		return $this->renderWith(array(
220
				'OpauthController_profilecompletion',
221
				'Security_profilecompletion',
222
				'Page',
223
			)
224
		);
225
	}
226
227
	public function RegisterForm(SS_HTTPRequest $request = null, Member $member = null, $result = null) {
228
		if(!isset($this->registerForm)) {
229
			$form = Injector::inst()->create('OpauthRegisterForm', $this, 'RegisterForm', $result);
230
			$form->populateFromSources($request, $member, $result);
231
			// Set manually the form action due to how routing works
232
			$form->setFormAction(Controller::join_links(
233
				self::config()->opauth_path,
234
				'RegisterForm'
235
			));
236
			$this->registerForm = $form;
237
		}
238
		else {
239
			$this->registerForm->populateFromSources($request, $member, $result);
240
		}
241
		return $this->registerForm;
242
	}
243
244
	public function doCompleteRegister($data, $form, $request) {
245
		$member = new Member();
246
		$form->saveInto($member);
247
		$identityID = Session::get('OpauthIdentityID');
248
		$identity = DataObject::get_by_id('OpauthIdentity', $identityID);
249
		$validationResult = $member->validate();
250
		$existing = Member::get()->filter('Email', $member->Email)->first();
251
		$emailCollision = $existing && $existing->exists();
252
		// If not valid then we have to manually transpose errors to the form
253
		if(!$validationResult->valid() || $emailCollision) {
254
			$errors = $validationResult->messageList();
255
			$form->setRequiredFields($errors);
256
			// Mandatory check on the email address
257
			if($emailCollision) {
258
				$form->addErrorMessage('Email', _t(
259
					'OpauthRegisterForm.ERROREMAILTAKEN',
260
					'It looks like this email has already been used'
261
				), 'required');
262
			}
263
			return $this->redirect('profilecompletion');
264
		}
265
		// If valid then write and redirect
266
		else {
267
			$member->extend('onBeforeOpauthRegister');
268
			$member->write();
269
			$identity->MemberID = $member->ID;
270
			$identity->write();
271
			return $this->loginAndRedirect($member, $identity, self::AUTH_FLAG_REGISTER);
272
		}
273
	}
274
275
	/**
276
	 * Returns the response from the Oauth callback.
277
	 * @throws InvalidArugmentException
278
	 * @return array The response
279
	 */
280
	protected function getOpauthResponse() {
281
		$config = OpauthAuthenticator::get_opauth_config();
282
		$transportMethod = $config['callback_transport'];
283
		switch($transportMethod) {
284
			case 'session':
285
				return $this->getResponseFromSession();
286
			case 'get':
287
			case 'post':
288
				return $this->getResponseFromRequest($transportMethod);
289
			default:
290
				throw new InvalidArgumentException('Invalid transport method: ' . $transportMethod);
291
		}
292
	}
293
294
	/**
295
	 * Validates the Oauth response for Opauth.
296
	 * @throws InvalidArgumentException
297
	 */
298
	protected function validateOpauthResponse($opauth, $response) {
299
		if(!empty($response['error'])) {
300
			throw new OpauthValidationException('Oauth provider error', 1, $response['error']);
301
		}
302
303
		// Required components within the response
304
		$this->requireResponseComponents(
305
			array('auth', 'timestamp', 'signature'),
306
			$response
307
		);
308
309
		// More required components within the auth section...
310
		$this->requireResponseComponents(
311
			array('provider', 'uid'),
312
			$response['auth']
313
		);
314
315
		$invalidReason = '';
316
317
		/**
318
		 * @todo: improve this signature check. it's a bit weak.
319
		 */
320
		if(!$opauth->validate(
321
			sha1(print_r($response['auth'], true)),
322
			$response['timestamp'],
323
			$response['signature'],
324
			$invalidReason
325
		)) {
326
			throw new OpauthValidationException('Invalid auth response', 3, $invalidReason);
327
		}
328
	}
329
330
	/**
331
	 * Shorthand for quickly finding missing components and complaining about it
332
	 * @throws InvalidArgumentException
333
	 */
334
	protected function requireResponseComponents(array $components, $response) {
335
		foreach($components as $component) {
336
			if(empty($response[$component])) {
337
				throw new OpauthValidationException('Required component missing', 2, $component);
338
			}
339
		}
340
	}
341
342
	/**
343
	 * @return array Opauth response from session
344
	 */
345
	protected function getResponseFromSession() {
346
		return Session::get('opauth');
347
	}
348
349
	/**
350
	 * @param OpauthValidationException $e
351
	 */
352
	protected function handleOpauthException(OpauthValidationException $e) {
353
		$data = $e->getData();
354
		$loginFormName = 'OpauthLoginForm_LoginForm';
355
		$message = '';
356
		switch($e->getCode()) {
357
			case 1: // provider error
358
				$message = _t(
359
					'OpauthLoginForm.OAUTHFAILURE',
360
					'There was a problem logging in with {provider}.',
361
					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...
362
						'provider' => $data['provider'],
363
					)
364
				);
365
				break;
366
			case 2: // validation error
367
			case 3: // invalid auth response
368
				$message = _t(
369
					'OpauthLoginForm.RESPONSEVALIDATIONFAILURE',
370
					'There was a problem logging in - {message}',
371
					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...
372
						'message' => $e->getMessage(),
373
					)
374
				);
375
				break;
376
		}
377
		// Set form message, redirect to login with permission failure
378
		Form::messageForForm($loginFormName, $message, 'bad');
379
		// always redirect to login
380
		Security::permissionFailure($this, $message);
381
	}
382
383
	/**
384
	 * Looks at $method (GET, POST, PUT etc) for the response.
385
	 * @return array Opauth response
386
	 */
387
	protected function getResponseFromRequest($method) {
388
		return unserialize(base64_decode($this->request->{$method.'Var'}('opauth')));
389
	}
390
391
	public function Link($action = null) {
392
		return Controller::join_links(
393
			self::config()->opauth_path,
394
			$action
395
		);
396
	}
397
398
	/**
399
	 * 'path' param for use in Opauth's config
400
	 * MUST have trailling slash for Opauth needs
401
	 * @return string
402
	 */
403
	public static function get_path() {
404
		return Controller::join_links(
405
			self::config()->opauth_path,
406
			'strategy/'
407
		);
408
	}
409
410
	/**
411
	 * 'callback_url' param for use in Opauth's config
412
	 * MUST have trailling slash for Opauth needs
413
	 * @return string
414
	 */
415
	public static function get_callback_path() {
416
		return Controller::join_links(
417
			self::config()->opauth_path,
418
			'finished/'
419
		);
420
	}
421
422
////**** Template variables ****////
423
	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...
424
		if($this->action == 'profilecompletion') {
425
			return _t('OpauthController.PROFILECOMPLETIONTITLE', 'Complete your profile');
426
		}
427
		return _t('OpauthController.TITLE', 'Social Login');
428
	}
429
430
	public function Form() {
431
		return $this->RegisterForm();
432
	}
433
////**** END Template variables ****////
434
435
}
436