Bug 463293 - Babel should use OpenID

Provide a new login method using Eclipse OpenID. To keep the number of
changes low the users table is still used. When a user logged in through
OpenID we request some user details from account API to insert/update
the user in database. (but only parts of the existing user table)

Change-Id: Ifad61fe7fc3b3df3c1a89258b83da6735607fb7e
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
Signed-off-by: droy <denis.roy@eclipse-foundation.org>
diff --git a/addons/babel.eclipse.org/backend_functions.php b/addons/babel.eclipse.org/backend_functions.php
index 07defba..a6f21cf 100644
--- a/addons/babel.eclipse.org/backend_functions.php
+++ b/addons/babel.eclipse.org/backend_functions.php
@@ -123,6 +123,21 @@
 					 'db_read_user' => $ini['db_read_user'],
 					 'db_read_pass' => $ini['db_read_pass'], 
 					 'db_read_name' => $ini['db_read_name']);
+	}

+
+	/**

+	 * Returns a hash of the oauth parameters.

+	 */

+	function oauth_params() {

+		$ini = @parse_ini_file(dirname(__FILE__) . '/base.conf');

+		if (! $ini) {

+			die("Could not read the configuration file " . dirname(__FILE__) . "/base.conf");

+		}

+		return array(

+			'client_id' => $ini['oauth_client_id'],

+			'client_secret' => $ini['oauth_client_secret'],

+			'client_callback' => $ini['oauth_client_callback']

+		);

 	}
 
 	/**
@@ -146,7 +161,8 @@
     $addon->register('syncup_user', array('BabelEclipseOrg_backend', 'syncupUser'));
     $addon->register('genie_user', array('BabelEclipseOrg_backend', 'genieUser'));
     $addon->register('context', array('BabelEclipseOrg_backend', 'context'));
-	$addon->register('db_params', array('BabelEclipseOrg_backend', 'db_params'));
+    $addon->register('db_params', array('BabelEclipseOrg_backend', 'db_params'));
+    $addon->register('oauth_params', array('BabelEclipseOrg_backend', 'oauth_params'));
 	$addon->register('error_log', array('BabelEclipseOrg_backend', 'error_log'));
 	$addon->register('babel_working', array('BabelEclipseOrg_backend', 'babel_working'));
 }
diff --git a/addons/babel.eclipse.org/html/head.php b/addons/babel.eclipse.org/html/head.php
old mode 100755
new mode 100644
index db072b6..a2bfffe
--- a/addons/babel.eclipse.org/html/head.php
+++ b/addons/babel.eclipse.org/html/head.php
@@ -93,5 +93,5 @@
 <script src='js/translationHint.js' type='text/javascript'></script>
 
 <script language="javascript">
-	document.getElementById("header-utils").innerHTML = "<ul><li><a href='login.php<?= $LoginAction ?>'><?= $LoginString ?></a></li></ul>";
+	document.getElementById("header-utils").innerHTML = "<ul><li><a href='login_oauth.php<?= $LoginAction ?>'><?= $LoginString ?></a></li></ul>";
 </script>
\ No newline at end of file
diff --git a/addons/reference/backend_functions.php b/addons/reference/backend_functions.php
index 42b0353..9619e83 100644
--- a/addons/reference/backend_functions.php
+++ b/addons/reference/backend_functions.php
@@ -52,6 +52,17 @@
 					 'db_read_user' => '',
 					 'db_read_pass' => '', 
 					 'db_read_name' => '');
+	}

+

+	/**

+	 * Returns a hash of the oauth parameters.

+	 */

+	function oauth_params() {

+		return array(

+			'client_id' => '',

+			'client_secret' => '',

+			'client_callback' => ''

+		);

 	}
 	
 	/**
@@ -76,6 +87,7 @@
     $addon->register('genie_user', array('Reference_backend', 'genieUser'));
 	$addon->register('context', array('Reference_backend', 'context'));
 	$addon->register('db_params', array('Reference_backend', 'db_parameters'));
+	$addon->register('oauth_params', array('Reference_backend', 'oauth_params'));
 	$addon->register('error_log', array('Reference_backend', 'error_log'));
 	$addon->register('babel_working', array('Reference_backend', 'babel_working'));
 }
diff --git a/babel-setup.sql b/babel-setup.sql
index f402393..22c533c 100644
--- a/babel-setup.sql
+++ b/babel-setup.sql
@@ -405,7 +405,7 @@
 

 DROP TABLE IF EXISTS `users`;

 CREATE TABLE `users` (

-  `userid` int(10) unsigned NOT NULL,

+  `userid` int(10) unsigned NOT NULL auto_increment,

   `username` varchar(256) NOT NULL default '',

   `first_name` varchar(256) NOT NULL default '',

   `last_name` varchar(256) NOT NULL default '',

@@ -418,8 +418,10 @@
   `updated_at` time NOT NULL,

   `created_on` date NOT NULL,

   `created_at` time NOT NULL,

+  `sub` varchar(128),

   PRIMARY KEY  (`userid`),

-  KEY `primary_language_id` (`primary_language_id`)

+  KEY `primary_language_id` (`primary_language_id`),

+  UNIQUE KEY `sub_idx` (`sub`)

 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

 DROP TABLE IF EXISTS `scoreboard`;

diff --git a/classes/system/user.class.php b/classes/system/user.class.php
index 5031bb0..16903f0 100644
--- a/classes/system/user.class.php
+++ b/classes/system/user.class.php
@@ -1,6 +1,6 @@
 <?php
 /*******************************************************************************
- * Copyright (c) 2007-2008 Eclipse Foundation and others.
+ * Copyright (c) 2007-2019 Eclipse Foundation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -10,6 +10,7 @@
  *    Paul Colton (Aptana)- initial API and implementation
  *    Eclipse Foundation 
  *    Matthew Mazaika <mmazaik  us.ibm.com> - bug 242011
+ *    Paul Pazderski - bug 463293: load user info from Eclipse account api
 *******************************************************************************/
 
 require_once(dirname(__FILE__) . "/backend_functions.php");
@@ -47,6 +48,104 @@
 			$Event->add();
 		}
 		return $this->userid;
+	}

+	

+	// Update user information in database by requesting account api with authorized oauth token. Return user id.

+	function updateUser($access_token) {
+		$this->userid = $this->doUpdateUser($access_token);
+		if ($this->userid > 0) {
+			$Event = new EventLog("users", "userid", $this->userid, "__auth_success");
+			$Event->add();
+		} else {
+			$Event = new EventLog("users", "userid", $_SERVER['REMOTE_ADDR'], "__auth_failure");
+			$Event->add();
+		}
+		return $this->userid;
+	}
+	
+	function doUpdateUser($access_token) {

+		$eclipse_profile_url = "https://accounts.eclipse.org/oauth2/UserInfo";

+

+		$options = array(

+			'http' => array(

+				'header' => array(

+					"Authorization: Bearer $access_token"

+				)

+			)

+		);

+		$context = stream_context_create($options);

+		$result = file_get_contents($eclipse_profile_url, false, $context);

+		if ($result === false) {

+			$GLOBALS['g_ERRSTRS'][1] = error_get_last()["message"];

+			return 0;

+		}

+

+		$profile = json_decode($result, true, 10);

+		if ($profile === null) {

+			$GLOBALS['g_ERRSTRS'][1] = error_get_last()["message"];

+			return 0;

+		}

+

+		$_sub = $profile["sub"];

+		$_username = $profile["name"];

+		$_first_name = $profile["given_name"];

+		$_last_name = $profile["family_name"];

+		$_is_committer = $profile["is_committer"] ? 1 : 0;

+

+		// check if user already exist or logged in for the first time

+		global $dbh;

+		$sql = "SELECT userid FROM users WHERE sub = '" . sqlSanitize($_sub, $dbh) . "'";

+		$result = mysqli_query($dbh, $sql);

+		if ($result === false) {

+			$GLOBALS['g_ERRSTRS'][1] = mysqli_error($dbh);

+			return 0;

+		}
+		$row = mysqli_fetch_array($result);

+		$_userid = $row !== null ? $row[0] : 0;

+		$first_login = ! $_userid;

+

+		if ($first_login) {

+			// try to match existing username to OpenID subject

+			$sql = "UPDATE users SET sub = '" . sqlSanitize($_sub, $dbh) . "' WHERE username = '" . sqlSanitize($_username, $dbh) . "' AND userid > 3 LIMIT 1"; 

+			$result = mysqli_query($dbh, $sql);

+			if ($result === false) {

+				$GLOBALS['g_ERRSTRS'][1] = mysqli_error($dbh);

+				return 0;
+			}
+			if (mysqli_affected_rows($dbh)) {
+				$sql = "SELECT userid FROM users WHERE sub = '" . sqlSanitize($_sub, $dbh) . "'";
+				$result = mysqli_query($dbh, $sql);
+				if ($result === false) {
+					$GLOBALS['g_ERRSTRS'][1] = mysqli_error($dbh);
+					return 0;
+				}
+				$row = mysqli_fetch_array($result);
+				$_userid = $row !== null ? $row[0] : 0;
+				$first_login = ! $_userid;
+			}

+		}

+

+		$sql = ($first_login ? "INSERT INTO " : "UPDATE ");

+		$sql .= "users SET ";

+		$sql .= "username = '" . sqlSanitize($_username, $dbh) . "', ";

+		$sql .= "first_name = '" . sqlSanitize($_first_name, $dbh) . "', ";

+		$sql .= "last_name = '" . sqlSanitize($_last_name, $dbh) . "', ";

+		$sql .= "is_committer = $_is_committer, ";

+		$sql .= "updated_on = NOW(), ";

+		$sql .= "updated_at = NOW()";

+		if ($first_login) {

+			$sql .= ", created_on = NOW(), ";

+			$sql .= "created_at = NOW(), ";

+			$sql .= "sub = '" . sqlSanitize($_sub, $dbh) . "'";

+		} else {

+			$sql .= " WHERE sub = '" . sqlSanitize($_sub, $dbh) . "'";

+		}

+		$result = mysqli_query($dbh, $sql);

+		if ($result === false) {

+			$GLOBALS['g_ERRSTRS'][1] = mysqli_error($dbh);
+			return 0;
+		}

+		return $first_login ? mysqli_insert_id($dbh) : $_userid;

 	}
 	
 	function loadFromID($_userid) {
diff --git a/html/content/en_login_oauth.php b/html/content/en_login_oauth.php
new file mode 100644
index 0000000..5a5f651
--- /dev/null
+++ b/html/content/en_login_oauth.php
@@ -0,0 +1,42 @@
+<div id="maincontent">
+<div id="midcolumn">
+<h1><?= $pageTitle ?></h1>
+
+<div id="index-page">
+
+	<a href="https://accounts.eclipse.org/"><img src="<?php echo imageRoot() ?>/large_icons/categories/preferences-desktop-peripherals.png">	<h2>An Eclipse account is all you need</h2></a>
+    <br style='clear: both;'>
+	<p>If you don't already have an Eclipse account then <a href="https://accounts.eclipse.org/">create one today</a>.
+	If logging in doesn't work after a few minutes, please contact <a href="mailto:webmaster@eclipse.org">webmaster@eclipse.org</a>.</p>
+
+    <br style='clear: both;'>
+	<p>If you already have an Eclipse account, then authenticate and start helping Eclipse speak your language.</p>
+
+<form style="margin-left: 35px;" name="frmLogin" method="POST">
+<div>
+
+	<?php 
+	if($GLOBALS['g_ERRSTRS'][0] || $GLOBALS['g_ERRSTRS'][1]){ 
+			?>
+			  <img style='margin-left: 70px;' src='<?php echo imageRoot() ?>/small_icons/actions/process-stop.png'>
+		      <div style='color: red; font-weight: bold; '><?=$GLOBALS['g_ERRSTRS'][0]?></div>
+		      <div style='color: red; font-weight: bold; '><?=$GLOBALS['g_ERRSTRS'][1]?></div>
+		      <br style='clear: both;'>
+		    <?php
+	    }else{
+			?>
+		    	<br style='clear: both;'>
+		   <?php
+	    }
+	 ?>
+</div>
+
+<div style='margin-left: 65px;'>
+<input type="submit" name="oauth" value="Authenticate with Eclipse account" style="font-size:14px;" />
+</div>
+
+</form>
+</div>
+</div>
+<br class='clearing'>
+</div>
diff --git a/html/global.php b/html/global.php
index 225407c..9c718c3 100644
--- a/html/global.php
+++ b/html/global.php
@@ -39,6 +39,7 @@
 	
   if (isset($_SERVER['REQUEST_URI']) &&
 	 (strpos($_SERVER['REQUEST_URI'], "login.php") == FALSE) &&
+	 (strpos($_SERVER['REQUEST_URI'], "login_oauth.php") == FALSE) &&
 	 (strpos($_SERVER['REQUEST_URI'], "callback") == FALSE)) {
 	  	SetSessionVar('s_pageLast', $_SERVER['REQUEST_URI']);
   }
@@ -56,7 +57,7 @@
   		$Session = new Session();
 
   		if(!$Session->validate()) {
-    		exitTo("login.php");
+    		exitTo("login_oauth.php");
   		}
   		else {
   			$User = new User();
@@ -65,7 +66,7 @@
   		}
   	}
   	else {
-  		exitTo("login.php");
+  		exitTo("login_oauth.php");
   	}
   }
   
diff --git a/html/importing.php b/html/importing.php
index 87d630c..66375e6 100644
--- a/html/importing.php
+++ b/html/importing.php
@@ -45,13 +45,13 @@
 		<li>Submit the bug. 
 	</ol> 
 
-	<a href="login.php"><img src="<?php echo imageRoot() ?>/large_icons/apps/preferences-desktop-theme.png"><h2>Project Leads</h2></a>
+	<a href="login_oauth.php"><img src="<?php echo imageRoot() ?>/large_icons/apps/preferences-desktop-theme.png"><h2>Project Leads</h2></a>
 	<br style='clear: both;'>
 	<p>
 	If you are a project lead and your project is not included in Babel then follow the steps below.
 	</p>
 	<ol>
-		<li><a href="login.php">Log into Babel</a>.
+		<li><a href="login_oauth.php">Log into Babel</a>.
 		<li>Click on the 'FOR COMMITTERS' link at the top left hand corner of the web page.
 		<li>Follow the instructions on that page, good luck!
 	</ol>	
diff --git a/html/login_oauth.php b/html/login_oauth.php
new file mode 100644
index 0000000..8dfd4ee
--- /dev/null
+++ b/html/login_oauth.php
@@ -0,0 +1,118 @@
+<?php

+/*******************************************************************************

+ * Copyright (c) 2019 Paul Pazderski and others.

+ * All rights reserved. This program and the accompanying materials

+ * are made available under the terms of the Eclipse Public License v1.0

+ * which accompanies this distribution, and is available at

+ * http://www.eclipse.org/legal/epl-v10.html

+ *

+ * Contributors:

+ *    Paul Pazderski - initial API and implementation

+ *******************************************************************************/

+include ("global.php");

+InitPage("");

+

+require_once (dirname(__FILE__) . "/../classes/system/user.class.php");

+require_once (dirname(__FILE__) . "/../classes/system/session.class.php");

+

+$pageTitle = "Contribute Translations to Babel";

+$pageKeywords = "translation,language,nlpack,pack,eclipse,babel";

+

+$eclipse_oauth_api_url = "https://accounts.eclipse.org/oauth2/authorize";

+$eclipse_oauth_token_url = "https://accounts.eclipse.org/oauth2/token";

+

+$OAUTH = getHTTPParameter("oauth", "POST");

+$CODE = getHTTPParameter("code", "GET");

+$STATE = getHTTPParameter("state", "GET");

+$SUBMIT = getHTTPParameter("submit", "GET");

+$ERROR = getHTTPParameter("error", "GET");

+if (! empty($OAUTH)) {

+	global $addon;

+	$oauth_params = $addon->callHook("oauth_params");

+	$state = createNonce();

+	SetSessionVar("oauth_state", $state);

+	$params = array(

+		'response_type' => 'code',

+		'client_id' => $oauth_params["client_id"],

+		'redirect_uri' => $oauth_params["client_callback"],

+		'scope' => 'openid profile',

+		'state' => $state

+	);

+	exitTo($eclipse_oauth_api_url . "?" . http_build_query($params));

+} else if ($ERROR == "consent_required") {

+	// do nothing; user aborted login

+} else if (! empty($CODE)) {

+	// check state

+	$saved_state = GetSessionVar("oauth_state");

+	if ($STATE !== $saved_state) {

+		$GLOBALS['g_ERRSTRS'][0] = "Authentication failed.";

+		$GLOBALS['g_ERRSTRS'][1] = "Request was not started from login page.";

+	} else {

+		global $addon;

+		$oauth_params = $addon->callHook("oauth_params");

+		$params = array(

+			'grant_type' => 'authorization_code',

+			'client_id' => $oauth_params["client_id"],

+			'client_secret' => $oauth_params["client_secret"],

+			'code' => $CODE,

+			'redirect_uri' => $oauth_params["client_callback"],

+			'state' => $STATE

+		);

+		$options = array(

+			'http' => array(

+				'header' => array(

+					"Content-type: application/json"

+				),

+				'method' => 'POST',

+				'content' => json_encode($params)

+			)

+		);

+		$context = stream_context_create($options);

+		$result = file_get_contents($eclipse_oauth_token_url, false, $context);

+		if ($result === false) {

+			$GLOBALS['g_ERRSTRS'][0] = "Login failed.";

+			$GLOBALS['g_ERRSTRS'][1] = error_get_last()["message"];

+		} else {

+			$result = json_decode($result, true, 10);

+			if ($result === null) {

+				$GLOBALS['g_ERRSTRS'][0] = "Login failed.";

+			} else {

+				$User = new User();

+				$uid = $User->updateUser($result["access_token"]);

+				if ($uid <= 0) {

+					$GLOBALS['g_ERRSTRS'][0] = "Login failed.";

+				} else {

+					$User->loadFromID($uid);

+

+					$Session = new Session();

+					$Session->create($User->userid, true);

+					SetSessionVar('User', $User);

+					if (isset($_SESSION['s_pageLast']) && ! empty($_SESSION['s_pageLast'])) {

+						exitTo($_SESSION['s_pageLast']);

+					}

+					exitTo("translate.php");

+				}

+			}

+		}

+	}

+} else if ($SUBMIT == "Logout") {

+	$Session = new Session();

+	$Session->destroy();

+	// we're logging out, therefore we don't have a user anymore

+	$User = null;

+	$GLOBALS['g_ERRSTRS'][0] = "You have successfully logged out. You can login again using the button below.";

+}

+

+global $addon;

+$addon->callHook("head");

+

+include ("content/en_login_oauth.php");

+

+global $addon;

+$addon->callHook("footer");

+

+// Function to create a simple unguessable random string.

+function createNonce() {

+	return md5(openssl_random_pseudo_bytes(20));

+}

+?>
\ No newline at end of file
diff --git a/html/map_files.php b/html/map_files.php
index 6c68c2d..b9d6e0e 100644
--- a/html/map_files.php
+++ b/html/map_files.php
@@ -23,7 +23,7 @@
 }
 
 if($User->is_committer != 1) {
-	exitTo("login.php?errNo=3214","error: 3214 - you must be an Eclipse committer to access this page.");
+	exitTo("login_oauth.php?errNo=3214","error: 3214 - you must be an Eclipse committer to access this page.");
 }
 
 require(dirname(__FILE__) . "/../classes/file/file.class.php");
diff --git a/html/project_source_locations.php b/html/project_source_locations.php
index 666187b..a870ee4 100644
--- a/html/project_source_locations.php
+++ b/html/project_source_locations.php
@@ -22,7 +22,7 @@
 }
 
 if($User->is_committer != 1) {
-	exitTo("login.php?errNo=3214","error: 3214 - you must be an Eclipse committer to access this page.");
+	exitTo("login_oauth.php?errNo=3214","error: 3214 - you must be an Eclipse committer to access this page.");
 }
 
 require(dirname(__FILE__) . "/../classes/file/file.class.php");