Skip to content
Snippets Groups Projects
Commit ce73310a authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Added join confirmation feature and x509 parser

parent c10ecd68
No related branches found
No related tags found
No related merge requests found
Showing with 630 additions and 62 deletions
......@@ -48,13 +48,13 @@ class CallbackHandler {
public static function manageLoginRedirect($user) {
global $BASE_PATH, $session;
global $BASE_PATH, $session, $log;
if (isset($session->callback) && $session->callback !== null) {
if ($session->getCallbackURL() !== null) {
// External login using token
$token = Util::createNewToken();
DAO::get()->insertTokenData($token, $user->id);
header('Location: ' . $session->callback . '?token=' . $token);
DAO::get()->createLoginToken($token, $user->id);
header('Location: ' . $session->getCallbackURL() . '?token=' . $token);
} else {
// Login in session
$session->user = $user;
......
......@@ -60,11 +60,13 @@ abstract class DAO {
public abstract function createJoinRequest($token, $applicantUserId, $targetUserId);
public $config;
public abstract function findJoinRequest($token);
public function __construct($config) {
$this->config = $config;
}
public abstract function deleteUser($userId);
public abstract function joinUsers($userId1, $userId2);
public abstract function deleteJoinRequest($token);
public static function get() {
global $DATABASE;
......
<?php
/* ----------------------------------------------------------------------------
* INAF - National Institute for Astrophysics
* IRA - Radioastronomical Institute - Bologna
* OATS - Astronomical Observatory - Trieste
* ----------------------------------------------------------------------------
*
* Copyright (C) 2016 Istituto Nazionale di Astrofisica
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License Version 3 as published by the
* Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
namespace RAP;
class GrouperClient {
private $client;
function __construct($config) {
$this->client = new \SoapClient($config['wsdlURL'], array(
'login' => $config['user'],
'password' => $config['password'],
'trace' => 1,
// See: https://bugs.php.net/bug.php?id=36226
'features' => SOAP_SINGLE_ELEMENT_ARRAYS
)
);
}
private function getBaseRequestParams() {
return array(
'clientVersion' => 'v2_3_000'
);
}
private function startsWith($haystack, $needle) {
return strpos($haystack, "$needle", 0) === 0;
}
private function isSuccess($response) {
$success = isset($response->return->resultMetadata) && $response->return->resultMetadata->resultCode === 'SUCCESS';
if (!$success) {
throw new \Exception("Web Service Failure. Response=" . json_encode($response));
}
return $success;
}
public function getSubjectGroups($subjectId) {
$params = $this->getBaseRequestParams();
$params['subjectLookups'] = array(
'subjectId' => $subjectId,
'subjectSourceId' => 'RAP'
);
$response = $this->client->getGroups($params);
if ($this->isSuccess($response)) {
if (count($response->return->results) === 1) {
$groups = [];
foreach ($response->return->results[0]->wsGroups as $group) {
if (!$this->startsWith($group->name, 'etc:')) {
array_push($groups, $group->name);
}
}
return $groups;
} else {
throw new \Exception("Wrong results number. Response=" . json_encode($response));
}
}
}
public function addMemberships($subjectId, $groups) {
foreach ($groups as $group) {
$params = $this->getBaseRequestParams();
$params['subjectId'] = $subjectId;
$params['subjectSourceId'] = 'RAP';
$params['groupName'] = $group;
$this->client->addMemberLite($params);
}
}
public function removeMemberships($subjectId, $groups) {
foreach ($groups as $group) {
$params = $this->getBaseRequestParams();
$params['subjectId'] = $subjectId;
$params['subjectSourceId'] = 'RAP';
$params['groupName'] = $group;
$this->client->deleteMemberLite($params);
}
}
}
......@@ -24,20 +24,10 @@
namespace RAP;
class TokenHandler {
class MailSender {
public static function createNewToken($data) {
$token = bin2hex(openssl_random_pseudo_bytes(16)); // http://stackoverflow.com/a/18890309/771431
DAO::get()->insertTokenData($token, $data);
return $token;
}
public static function deleteToken($token) {
DAO::get()->deleteToken($token);
}
public static function sendJoinEmail(User $recipientUser, User $applicantUser) {
public static function getUserData($token) {
return DAO::get()->findTokenData($token);
}
}
......@@ -29,8 +29,12 @@ use PDO;
class MySQLDAO extends DAO {
public function getDBHandler() {
$connectionString = "mysql:host=" . $this->config['hostname'] . ";dbname=" . $this->config['dbname'];
return new PDO($connectionString, $this->config['username'], $this->config['password']);
global $DATABASE;
$connectionString = "mysql:host=" . $DATABASE['hostname'] . ";dbname=" . $DATABASE['dbname'];
$dbh = new PDO($connectionString, $DATABASE['username'], $DATABASE['password']);
// For transaction errors (see https://stackoverflow.com/a/9659366/771431)
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $dbh;
}
public function createLoginToken($token, $userId) {
......@@ -130,6 +134,10 @@ class MySQLDAO extends DAO {
public function findUserById($userId) {
if (!filter_var($userId, FILTER_VALIDATE_INT)) {
return null;
}
$dbh = $this->getDBHandler();
$stmt = $dbh->prepare("SELECT `id`, `type`, `typed_id`, `email`, `local_db_id`, `name`, `surname`, `institution`, `username`, `eppn`"
......@@ -269,4 +277,75 @@ class MySQLDAO extends DAO {
$stmt->execute();
}
public function findJoinRequest($token) {
$dbh = $this->getDBHandler();
$stmt = $dbh->prepare("SELECT `applicant_user_id`, `target_user_id` FROM `join_request` WHERE `token` = :token");
$stmt->bindParam(':token', $token);
$stmt->execute();
$result = $stmt->fetchAll();
switch (count($result)) {
case 0:
return null;
case 1:
$row = $result[0];
return [$row['applicant_user_id'], $row['target_user_id']];
default:
throw new Exception("Found multiple join request with the same token");
}
}
public function deleteUser($userId) {
$dbh = $this->getDBHandler();
$stmt3 = $dbh->prepare("DELETE FROM TABLE `user` WHERE `id` = :id2");
$stmt3->bindParam(':id2', $userId);
$stmt3->execute();
}
public function joinUsers($userId1, $userId2) {
$dbh = $this->getDBHandler();
try {
$dbh->beginTransaction();
// Moving identities from user2 to user1
$stmt1 = $dbh->prepare("UPDATE `identity` SET `user_id` = :id1 WHERE `user_id` = :id2");
$stmt1->bindParam(':id1', $userId1);
$stmt1->bindParam(':id2', $userId2);
$stmt1->execute();
// Moving additional email addresses from user2 to user1
$stmt2 = $dbh->prepare("UPDATE `additional_email` SET `user_id` = :id1 WHERE `user_id` = :id2");
$stmt2->bindParam(':id1', $userId1);
$stmt2->bindParam(':id2', $userId2);
$stmt2->execute();
// Deleting user2 join requests
$stmt3 = $dbh->prepare("DELETE FROM `join_request` WHERE `target_user_id` = :id2");
$stmt3->bindParam(':id2', $userId2);
$stmt3->execute();
// Deleting user2
$stmt4 = $dbh->prepare("DELETE FROM `user` WHERE `id` = :id2");
$stmt4->bindParam(':id2', $userId2);
$stmt4->execute();
$dbh->commit();
} catch (Exception $ex) {
$dbh->rollBack();
throw $ex;
}
}
public function deleteJoinRequest($token) {
$dbh = $this->getDBHandler();
$stmt = $dbh->prepare("DELETE FROM `join_request` WHERE `token` = :token");
$stmt->bindParam(':token', $token);
$stmt->execute();
}
}
......@@ -43,4 +43,7 @@ class User {
array_push($this->additionalEmailAddresses, $email);
}
public function getPrimaryEmail() {
return $this->identities[0]->email;
}
}
......@@ -52,4 +52,19 @@ class UserHandler {
return DAO::get()->findUserByIdentity($type, $identifier, $dbIdentifier);
}
public static function joinUsers($userId1, $userId2) {
global $GROUPER;
if (isset($GROUPER)) {
$gc = new GrouperClient($GROUPER);
$groupsToMove = $gc->getSubjectGroups('RAP:' . $userId2);
$gc->addMemberships('RAP:' . $userId1, $groupsToMove);
$gc->removeMemberships('RAP:' . $userId2, $groupsToMove);
}
DAO::get()->joinUsers($userId1, $userId2);
}
}
......@@ -26,12 +26,12 @@ namespace RAP;
class UserSearchResult {
private $userId;
private $user;
public $userDisplayText;
public static function buildFromUser(User $user) {
$usr = new UserSearchResult();
$usr->userId = $user->id;
$usr->user = $user;
$nameAndSurname = null;
$email = null;
......@@ -71,8 +71,8 @@ class UserSearchResult {
return $usr;
}
public function getUserId() {
return $this->userId;
public function getUser() {
return $this->user;
}
}
<?php
/* ----------------------------------------------------------------------------
* INAF - National Institute for Astrophysics
* IRA - Radioastronomical Institute - Bologna
* OATS - Astronomical Observatory - Trieste
* ----------------------------------------------------------------------------
*
* Copyright (C) 2016 Istituto Nazionale di Astrofisica
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License Version 3 as published by the
* Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
namespace RAP;
class X509Data {
public $email;
public $fullName;
public $institution;
public $serialNumber;
/**
* Retrieve full name from CN, removing the e-mail if necessary
*/
private function parseCN($cn) {
$cnSplit = explode(" ", $cn);
$count = count($cnSplit);
$lastSegment = $cnSplit[$count - 1];
if (strpos($lastSegment, "@") === false) {
if ($count < 2) {
// We need name + surname
throw new \Exception("Unparsable CN");
}
$this->fullName = $cn;
} else {
// Last segment is an email
if ($count < 3) {
// We need name + surname + email
throw new \Exception("Unparsable CN");
}
// Rebuilding full name removing the email part
$cnLength = strlen($cn);
$emailLength = strlen($lastSegment);
// -1 is for removing also the space between surname and email
$this->fullName = substr($cn, 0, $cnLength - $emailLength - 1);
}
}
private function parseUsingOpenSSL($sslClientCert) {
$parsedX509 = openssl_x509_parse($sslClientCert);
// try extracting email
if (isset($parsedX509["extensions"]["subjectAltName"])) {
$AyAlt = explode(":", $parsedX509["extensions"]["subjectAltName"]);
if ($AyAlt[0] === "email") {
$this->email = $AyAlt[1];
}
}
$this->serialNumber = $parsedX509["serialNumber"];
$cn = $parsedX509["subject"]["CN"];
$this->parseCN($cn);
$this->institution = $parsedX509["subject"]["O"];
}
private function extractDNFields($dn) {
$dnSplit = explode(",", $dn);
foreach ($dnSplit as $dnField) {
$pos = strpos($dnField, "=");
$key = substr($dnField, 0, $pos - 1);
$value = substr($dnField, $pos + 1, strlen($dnField) - 1);
switch ($key) {
case 'CN':
$this->parseCN($value);
break;
case 'O':
$this->institution = $value;
}
}
}
public static function parse($server) {
$parsedData = new X509Data();
if (isset($server['SSL_CLIENT_CERT'])) {
$parsedData->parseUsingOpenSSL(['SSL_CLIENT_CERT']);
// If all mandatory information has been parsed return the object
if ($parsedData->email !== null && $parsedData->fullName !== null && $parsedData->serialNumber !== null) {
return $parsedData;
}
}
if ($parsedData->fullName === null) {
if (isset($server['SSL_CLIENT_S_DN_CN'])) {
$parsedData->parseCN($server['SSL_CLIENT_S_DN_CN']);
if ($parsedData->fullName === null) {
throw new \Exception("Unable to extract CN from DN");
}
} else {
throw new \Exception("Unable to obtain DN from certificate");
}
}
if ($parsedData->email === null) {
if (isset($_SERVER['SSL_CLIENT_SAN_Email_0'])) {
$parsedData->email = $_SERVER['SSL_CLIENT_SAN_Email_0'];
} else {
throw new \Exception("Unable to retrieve e-mail address from certificate");
}
}
if ($parsedData->serialNumber === null) {
if (isset($_SERVER['SSL_CLIENT_M_SERIAL'])) {
// In this server variable the serial number is stored into an HEX format,
// while openssl_x509_parse function provides it in DEC format.
// Here a hex->dec conversion is performed, in order to store the
// serial number in a consistent format:
$hexSerial = $_SERVER['SSL_CLIENT_M_SERIAL'];
// TODO
} else {
throw new Exception("Unable to retrieve serial number from certificate");
}
}
return $parsedData;
}
}
......@@ -72,3 +72,10 @@ $AUTHENTICATION_METHODS = array(
)
)
);
$GROUPER = array(
//'serviceBaseURL' => 'https://sso.ia2.inaf.it/grouper-ws/servicesRest',
'wsdlURL' => 'http://localhost:8087/grouper-ws/services/GrouperService_v2_3?wsdl',
'user' => 'GrouperSystem',
'password' => '***REMOVED***'
);
......@@ -103,7 +103,7 @@ body {
margin-bottom: 8px
}
.callback-title {
.page-title {
margin-top: 0;
font-weight: bold;
color: #24388e;
......@@ -121,3 +121,24 @@ body {
.panel-default > .panel-heading {
background-image: linear-gradient(to bottom,#f5f5f5 0,#ccc 100%);
}
.table-like {
display: table;
width: 100%;
}
.table-like .table-row {
display: table-row;
}
.table-row .table-cell {
display: table-cell;
vertical-align: middle;
}
.join-icon-cell {
min-width: 100px;
text-align: center;
font-size: 30px;
}
......@@ -37,3 +37,67 @@ Flight::route('/facebook', function() {
startSession();
Flight::redirect('/oauth2/facebook_login.php');
});
Flight::route('/eduGAIN', function() {
startSession();
Flight::redirect('/saml2/aai.php');
});
Flight::route('/x509', function() {
startSession();
Flight::redirect('/x509/certlogin.php');
});
Flight::route('GET /confirm-join', function() {
$token = Flight::request()->query['token'];
if ($token === null) {
http_response_code(422);
die("Token not found");
}
$dao = RAP\DAO::get();
$userIds = $dao->findJoinRequest($token);
if ($userIds === null) {
http_response_code(422);
die("Invalid token");
}
$applicantUser = $dao->findUserById($userIds[0]);
$targetUser = $dao->findUserById($userIds[1]);
Flight::render('confirm-join.php', array('title' => 'RAP',
'token' => $token,
'applicantUser' => $applicantUser,
'targetUser' => $targetUser));
});
Flight::route('POST /confirm-join', function() {
$token = Flight::request()->data['token'];
if ($token === null) {
http_response_code(422);
die("Token not found");
}
$dao = RAP\DAO::get();
$userIds = $dao->findJoinRequest($token);
if ($userIds === null) {
http_response_code(422);
die("Invalid token");
}
RAP\UserHandler::joinUsers($userIds[0], $userIds[1]);
$dao->deleteJoinRequest($token);
// Force user to relogin to see changes to him/her identities
session_start();
session_destroy();
global $BASE_PATH;
Flight::render('join-success.php', array('title' => 'Success - RAP Join Request',
'basePath' => $BASE_PATH));
});
......@@ -38,10 +38,12 @@ Flight::route('POST /join', function() {
global $session;
$selectedUserIndex = Flight::request()->data['selectedUserIndex'];
$targetUserId = $session->userSearchResults[$selectedUserIndex]->getUserId();
$selectedSearchResult = $session->userSearchResults[$selectedUserIndex];
$targetUserId = $selectedSearchResult->getUser()->id;
$token = RAP\Util::createNewToken();
RAP\DAO::get()->createJoinRequest($token, $session->user->id, $targetUserId);
RAP\MailSender::sendJoinEmail($selectedSearchResult->getUser(), $session->user);
echo "";
echo $selectedSearchResult->userDisplayText;
});
......@@ -34,9 +34,12 @@ spl_autoload_register(function ($class_name) {
}
});
// Loading dependecy classes
include ROOT . '/vendor/autoload.php';
// Loading configuration
include ROOT . '/config.php';
// Setup logging
$log = new Monolog\Logger('mainLogger');
$log->pushHandler(new Monolog\Handler\StreamHandler($LOG_PATH, $LOG_LEVEL));
......
......@@ -9,14 +9,14 @@ $WS_PREFIX = '/ws';
Flight::route('GET ' . $WS_PREFIX . '/user-info', function() {
$token = Flight::request()->query['token'];
$userData = RAP\DAO::get()->findTokenData($token);
$userData = RAP\DAO::get()->findLoginToken($token);
if (is_null($userData)) {
http_response_code(404);
die("Token not found");
}
RAP\DAO::get()->deleteToken($token);
RAP\DAO::get()->deleteLoginToken($token);
header('Content-Type: text/plain');
echo $userData;
......@@ -40,3 +40,6 @@ Flight::route('GET ' . $WS_PREFIX . '/user', function() {
$users = RAP\DAO::get()->searchUser($searchText);
echo json_encode($users);
});
Flight::route('GET ' . $WS_PREFIX . '/test', function() {
});
<?php foreach ($user->identities as $identity) { ?>
<dl class="dl-horizontal">
<dt>Type</dt>
<dd><?php echo $identity->type; ?></dd>
<dt>E-mail</dt>
<dd><?php echo $identity->email; ?></dd>
<?php if ($identity->eppn !== null) { ?>
<dt>EduPersonPrincipalName</dt>
<dd><?php echo $identity->eppn; ?></dd>
<?php } ?>
<?php if ($identity->username !== null) { ?>
<dt>Username</dt>
<dd><?php echo $identity->username; ?></dd>
<?php } ?>
<?php if ($identity->name !== null) { ?>
<dt>Name</dt>
<dd><?php echo $identity->name; ?></dd>
<?php } ?>
<?php if ($identity->surname !== null) { ?>
<dt>Surname</dt>
<dd><?php echo $identity->surname; ?></dd>
<?php } ?>
<?php if ($identity->institution !== null) { ?>
<dt>Institution</dt>
<dd><?php echo $identity->institution; ?></dd>
<?php } ?>
</dl>
<?php } ?>
......@@ -20,11 +20,16 @@
}
function sendJoinRequest() {
$userSelector = $('#user-selector-group select');
var selectedUserIndex = $userSelector.val();
if (selectedUserIndex !== null) {
$.post('join', {selectedUserIndex: selectedUserIndex}, function (response) {
console.log(response);
$('#search-user-modal').modal('hide');
$infoAlert = $('#info-message-alert');
$infoAlert.find('.info-message').text("An e-mail has been sent to " + response + " primary e-mail address.");
$infoAlert.removeClass('hide');
});
}
}
......@@ -43,6 +48,10 @@
// Add click event handler to join request button
$(document).on('click', '#send-join-request-btn', sendJoinRequest);
$(document).on('click', '#info-message-alert .close', function () {
$('#info-message-alert').addClass('hide');
});
});
})(jQuery);
\ No newline at end of file
<?php
/* ----------------------------------------------------------------------------
* INAF - National Institute for Astrophysics
* IRA - Radioastronomical Institute - Bologna
* OATS - Astronomical Observatory - Trieste
* ----------------------------------------------------------------------------
*
* Copyright (C) 2016 Istituto Nazionale di Astrofisica
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License Version 3 as published by the
* Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
include '../include/init.php';
startSession();
if (isset($_SERVER['Shib-Session-ID'])) {
$eppn = $_SERVER['eppn'];
$user = RAP\UserHandler::findUserByIdentity(RAP\Identity::EDU_GAIN, $eppn, null);
if ($user === null) {
$user = new RAP\User();
$identity = new RAP\Identity(RAP\Identity::EDU_GAIN);
$identity->email = $_SERVER['mail'];
$identity->name = $_SERVER['givenName'];
$identity->surname = $_SERVER['sn'];
$identity->typedId = $eppn;
//$_SERVER['Shib-Identity-Provider']
$user->addIdentity($identity);
RAP\UserHandler::saveUser($user);
}
RAP\CallbackHandler::manageLoginRedirect($user);
} else {
throw new Exception("Shib-Session-ID not found!");
}
<?php
include 'include/header.php';
?>
<h1 class="text-center page-title">Confirm join request</h1>
<br/>
<div class="table-like">
<div class="table-row">
<div class="table-cell">
<div class="panel panel-default">
<div class="panel-body">
<?php
$user = $applicantUser;
include 'include/user-data.php';
?>
</div>
</div>
</div>
<div class="table-cell join-icon-cell text-success">
<span class="glyphicon glyphicon-transfer"></span>
</div>
<div class="table-cell">
<div class="panel panel-default">
<div class="panel-body">
<?php
$user = $targetUser;
include 'include/user-data.php';
?>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 text-center">
<p>Pressing the following button the identities listed below will be joined.</p>
<form action="confirm-join" method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="submit" class="btn btn-success btn-lg" value="Join users" />
</form>
</div>
</div>
<?php
include 'include/footer.php';
......@@ -5,7 +5,7 @@ include 'include/header.php';
<?php if ($session->user === null) { ?>
<div class="row">
<div class="col-xs-12">
<h1 class="text-center callback-title"><?php echo $session->getCallbackTitle(); ?></h1>
<h1 class="text-center page-title"><?php echo $session->getCallbackTitle(); ?></h1>
</div>
</div>
<div class="row">
......@@ -71,6 +71,15 @@ include 'include/header.php';
</div>
</div>
<?php } else { ?>
<div class="row">
<div class="col-xs-12">
<div class="alert alert-success hide" id="info-message-alert">
<button type="button" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span class="glyphicon glyphicon-info-sign"></span>
<span class="info-message"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-5 col-xs-12">
<div class="panel panel-default">
......@@ -78,34 +87,10 @@ include 'include/header.php';
<h3 class="panel-title">Your identities</h3>
</div>
<div class="panel-body">
<?php foreach ($session->user->identities as $identity) { ?>
<dl class="dl-horizontal">
<dt>Type</dt>
<dd><?php echo $identity->type; ?></dd>
<dt>E-mail</dt>
<dd><?php echo $identity->email; ?></dd>
<?php if ($identity->eppn !== null) { ?>
<dt>EduPersonPrincipalName</dt>
<dd><?php echo $identity->eppn; ?></dd>
<?php } ?>
<?php if ($identity->username !== null) { ?>
<dt>Username</dt>
<dd><?php echo $identity->username; ?></dd>
<?php } ?>
<?php if ($identity->name !== null) { ?>
<dt>Name</dt>
<dd><?php echo $identity->name; ?></dd>
<?php } ?>
<?php if ($identity->surname !== null) { ?>
<dt>Surname</dt>
<dd><?php echo $identity->surname; ?></dd>
<?php } ?>
<?php if ($identity->institution !== null) { ?>
<dt>Institution</dt>
<dd><?php echo $identity->institution; ?></dd>
<?php } ?>
</dl>
<?php } ?>
<?php
$user = $session->user;
include 'include/user-data.php';
?>
</div>
</div>
</div>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment