<?php
/**************************************************************************************************
| Ionicware
| http://www.ionicware.com
| support@ionicware.com
|
|**************************************************************************************************
|
| Copyright (c) 2011 ionicware.com. All rights reserved.
|**************************************************************************************************/

include("include/config.php");
include("include/functions/import.php");
$thebaseurl = $config['baseurl'];

/**
 * "Sign In with LinkedIn using OpenID Connect", served at /linkedin-login.
 *
 * Both legs of the authorization code flow live here. The first request
 * has no code on it, so we mint a state value and bounce the browser to
 * LinkedIn. LinkedIn sends the browser back to this same URL with a code,
 * we swap that code for an access token server to server, and read the
 * identity from LinkedIn's userinfo endpoint with it.
 *
 * Nothing the browser sends is trusted as an identity. The uid and the
 * email come out of a TLS response to a request we made with our own
 * client secret, which is why there is no JWT signature check anywhere
 * below - we never parse the id_token at all.
 *
 * 2026-09-17 JD
 */

$r = cleanit((is_array($_REQUEST['r']) ? null : stripslashes($_REQUEST['r']))); // ?x[]=: as on 7.4, not a TypeError (#1925)

/*
 * The user is already logged in, or the owner has not filled the
 * credentials in yet - either way there is nothing to do here.
 */
if ($_SESSION['USERID'] > 0) redirectAndDie('/');
if (empty($config['linkedin_client_id']) || empty($config['linkedin_login_enabled'])) redirectAndDie('/');

/**
 * Tell the user the sign in did not happen and send them to the login
 * page, where views/alert/view_header-alert.php renders (and clears)
 * $_SESSION['error'].
 *
 * @param userMessage {string} - the $lang string to show
 * @param logDetail {string} - Optional, what actually went wrong, for us
 *
 * 2026-09-17 JD
 */
function linkedinLoginFailed($userMessage, $logDetail = '')
{
	if (!empty($logDetail))
	{
		error_log('LinkedIn login failed. ' . $logDetail . ' IP: ' . $_SERVER['REMOTE_ADDR']);
	}

	$_SESSION['error'] = $userMessage;

	redirectAndDie('/login');
}

/**
 * CurlFetch() in includes/curl.php cannot send request headers and
 * pretends to be a browser for 20 seconds, neither of which suits a
 * token exchange, so this endpoint makes its own requests.
 *
 * @param url {string} - the endpoint to call
 * @param postString {string} - Optional, form encoded body. Null sends a GET
 * @param headers {array} - Optional, additional request headers
 *
 * 2026-09-17 JD
 */
function linkedinCurlFetch($url, $postString = null, $headers = array())
{
	$ch = curl_init($url);

	if ($postString !== null)
	{
		$headers[] = 'Content-Type: application/x-www-form-urlencoded';

		curl_setopt($ch, CURLOPT_POST, 1);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $postString);
	}

	if (!empty($headers)) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
	curl_setopt($ch, CURLOPT_TIMEOUT, 10);
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);

	$response 	= curl_exec($ch);
	$httpCode 	= curl_getinfo($ch, CURLINFO_HTTP_CODE);

	curl_close($ch);

	return array('body' => $response, 'code' => $httpCode);
}

/*
 * The redirect_uri has to be byte for byte the same in both legs and in
 * the LinkedIn app, so it is built once here.
 */
$redirectUri = $config['baseurl'] . '/linkedin-login';

$oauthCode 		= !empty($_GET['code']) ? $_GET['code'] : '';
$oauthError 	= !empty($_GET['error']) ? $_GET['error'] : '';
$oauthState 	= !empty($_GET['state']) ? $_GET['state'] : '';

/*
 * First leg. Nothing came back from LinkedIn yet, so send them there.
 */
if (empty($oauthCode) && empty($oauthError))
{
	$_SESSION['linkedin_oauth_state'] 	= bin2hex(random_bytes(16));
	$_SESSION['linkedin_oauth_r'] 		= $r;

	$authorizeQuery = http_build_query(
							array(
								'response_type'	=> 'code',
								'client_id'		=> $config['linkedin_client_id'],
								'redirect_uri'	=> $redirectUri,
								'state'			=> $_SESSION['linkedin_oauth_state'],
								'scope'			=> 'openid profile email'
							),
							'',
							'&',
							PHP_QUERY_RFC3986
						);

	header('Location: https://www.linkedin.com/oauth/v2/authorization?' . $authorizeQuery);
	die();
}

/*
 * Read the state and the stored redirect back out.
 */
$sessionState 	= !empty($_SESSION['linkedin_oauth_state']) ? $_SESSION['linkedin_oauth_state'] : '';
$storedRedirect = !empty($_SESSION['linkedin_oauth_r']) ? $_SESSION['linkedin_oauth_r'] : '';

$stateMatches 	= (!empty($sessionState) && !empty($oauthState) && hash_equals($sessionState, $oauthState));

/*
 * A request that is not the answer to our own round trip leaves the
 * pending state alone. Otherwise anyone could send a visitor to
 * /linkedin-login?error=x mid flow, wipe the state, and make the real
 * callback fail. Only a request carrying our state uses it up.
 *
 * 2026-09-18 JD
 */
if (!$stateMatches)
{
	if (!empty($oauthError)) redirectAndDie('/');

	linkedinLoginFailed($lang['LINKEDIN_LOGIN_FAILED'], 'State did not match the one we issued.');
}

unset($_SESSION['linkedin_oauth_state']);
unset($_SESSION['linkedin_oauth_r']);

/*
 * The user pressed cancel on LinkedIn's consent screen. That is not a
 * failure worth shouting about, so put them back on the homepage.
 */
if (!empty($oauthError)) redirectAndDie('/');

/*
 * Swap the code for an access token. This is the leg the client secret
 * is for, and the reason the code alone is worth nothing to anyone who
 * intercepts it.
 */
$tokenPost = http_build_query(
					array(
						'grant_type'	=> 'authorization_code',
						'code'			=> $oauthCode,
						'redirect_uri'	=> $redirectUri,
						'client_id'		=> $config['linkedin_client_id'],
						'client_secret'	=> $config['linkedin_client_secret']
					),
					'',
					'&',
					PHP_QUERY_RFC3986
				);

$tokenResponse 	= linkedinCurlFetch('https://www.linkedin.com/oauth/v2/accessToken', $tokenPost);
$tokenData 		= json_decode($tokenResponse['body'], true);

if (empty($tokenData['access_token']))
{
	linkedinLoginFailed($lang['LINKEDIN_LOGIN_FAILED'], 'Token exchange failed. HTTP: ' . $tokenResponse['code'] . ' Body: ' . $tokenResponse['body']);
}

/*
 * Now ask LinkedIn who the token belongs to.
 */
$profileResponse 	= linkedinCurlFetch('https://api.linkedin.com/v2/userinfo', null, array('Authorization: Bearer ' . $tokenData['access_token']));
$profile 			= json_decode($profileResponse['body'], true);

if (empty($profile['sub']) || empty($profile['email']))
{
	linkedinLoginFailed($lang['LINKEDIN_LOGIN_FAILED'], 'Userinfo had no sub or no email. HTTP: ' . $profileResponse['code']);
}

/*
 * An email match logs the caller into an account that already exists, so
 * an unverified address would be a way in to somebody else's. LinkedIn
 * answers with a JSON boolean, but take the string forms too rather than
 * trust the encoder - "false" is a truthy PHP string.
 */
$emailVerified = isset($profile['email_verified']) ? $profile['email_verified'] : false;

if (is_string($emailVerified)) $emailVerified = (strtolower($emailVerified) == 'true' || $emailVerified == '1');

if (empty($emailVerified))
{
	linkedinLoginFailed($lang['LINKEDIN_EMAIL_UNVERIFIED'], 'Email ' . $profile['email'] . ' is not verified at LinkedIn.');
}

$oauthUid 	= $profile['sub'];
$email 		= $profile['email'];
$firstName 	= !empty($profile['given_name']) ? $profile['given_name'] : '';
$lastName 	= !empty($profile['family_name']) ? $profile['family_name'] : '';

$loginResult = oauthLoginOrCreateMember('linkedin', $oauthUid, $email, $firstName, $lastName);

if (empty($loginResult['success']))
{
	linkedinLoginFailed($loginResult['error'], 'oauthLoginOrCreateMember() refused ' . $email . '.');
}

/*
 * Logged in. Any 2FA or new device check runs on the next page load the
 * same as it does for a password login, so there is nothing to do here
 * but send them on their way.
 */
if (!empty($storedRedirect)) redirectAndDie('/' . decodeRedirectPath($storedRedirect));
else redirectAndDie('/');
