commit/mtrack: 29 new changesets

13 views
Skip to first unread message

Bitbucket

unread,
May 4, 2012, 9:17:20 PM5/4/12
to mtr...@googlegroups.com
29 new commits in mtrack:


https://bitbucket.org/wez/mtrack/changeset/94923e55b028/
changeset: 94923e55b028
user: wez
date: 2012-04-28 15:25:58
summary: move openid around, add simple browserid support (but no admin ui for it)
affected #: 6 files

diff -r c335f24df2c7ad3a6cf66b37ab9ccf6ce5a7bb74 -r 94923e55b0289a3f58cbde5395acf07989f112eb inc/auth.php
--- a/inc/auth.php
+++ b/inc/auth.php
@@ -4,6 +4,7 @@
include_once MTRACK_INC_DIR . '/auth/anon.php';
include_once MTRACK_INC_DIR . '/auth/http.php';
include_once MTRACK_INC_DIR . '/auth/openid.php';
+include_once MTRACK_INC_DIR . '/auth/browserid.php';

interface IMTrackAuth {
/** Returns the authenticated user, or null if authentication is


diff -r c335f24df2c7ad3a6cf66b37ab9ccf6ce5a7bb74 -r 94923e55b0289a3f58cbde5395acf07989f112eb inc/auth/browserid.php
--- /dev/null
+++ b/inc/auth/browserid.php
@@ -0,0 +1,111 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+
+/* You can enable this module using this in your config.ini:
+ * [plugins]
+ * MTrackAuth_BrowserID = ""
+ */
+
+class MTrackAuth_BrowserID implements IMTrackAuth, IMTrackNavigationHelper {
+ function __construct() {
+ MTrackAuth::registerMech($this);
+ MTrackNavigation::registerHelper($this);
+ }
+
+ function augmentUserInfo(&$content) {
+ if (!isset($_SESSION['auth.browserid'])) {
+ global $ABSWEB;
+ $content = <<<HTML
+<script src='https://browserid.org/include.js'></script>
+<script>
+function browserid_got_assertion(assertion) {
+ if (assertion) {
+ $.ajax({
+ url: ABSWEB + 'auth/browserid.php',
+ contentType: 'application/octet-stream',
+ type: 'POST',
+ data: assertion,
+ success: function(data) {
+ if (data.status == 'okay') {
+ // Success; reload page (ajax call set the cookie we need)
+ window.location.reload();
+ } else {
+ window.alert("BrowserID authentication failed: " +
+ data.reason);
+ }
+ },
+ error: function (xhr, status, err) {
+ // the alert is a bit nasty, but browserid tends to
+ // deal with most errors by not even invoking this
+ // assertion callback
+ window.alert("BrowserID authentication failed: " +
+ status + " " + err);
+ }
+ });
+ }
+}
+
+function browserid_login() {
+ navigator.id.get(browserid_got_assertion, {allowPersistent: true});
+ return false;
+}
+$(function () {
+ navigator.id.get(browserid_got_assertion, {silent:true});
+});
+</script>
+<a href='javascript:browserid_login()'>Log In</a>
+HTML;
+ }
+ }
+ function augmentNavigation($id, &$items) {
+ }
+
+ function authenticate() {
+ if (!strlen(session_id()) && php_sapi_name() != 'cli') {
+ session_start();
+ }
+ if (isset($_SESSION['auth.browserid'])) {
+ return $_SESSION['auth.browserid'];
+ }
+ return null;
+ }
+
+ function doAuthenticate($force = false) {
+ if ($force) {
+ global $ABSWEB;
+ header("Location: {$ABSWEB}auth/browserid.php");
+ exit;
+ }
+ return null;
+ }
+
+ function enumGroups() {
+ return null;
+ }
+
+ function getGroups($username) {
+ return null;
+ }
+
+ function addToGroup($username, $groupname) {
+ return null;
+ }
+
+ function removeFromGroup($username, $groupname) {
+ return null;
+ }
+
+ function getUserData($username) {
+ return null;
+ }
+
+ function canLogOut() {
+ return true;
+ }
+
+ function LogOut() {
+ session_destroy();
+ header('Location: ' . $GLOBALS['ABSWEB']);
+ exit;
+ }
+}


diff -r c335f24df2c7ad3a6cf66b37ab9ccf6ce5a7bb74 -r 94923e55b0289a3f58cbde5395acf07989f112eb inc/auth/openid.php
--- a/inc/auth/openid.php
+++ b/inc/auth/openid.php
@@ -11,9 +11,9 @@
function augmentUserInfo(&$content) {
global $ABSWEB;
if (isset($_SESSION['openid.id'])) {
- //$content .= " | <a href='{$ABSWEB}openid.php/signout'>Log off</a>";
+ //$content .= " | <a href='{$ABSWEB}auth/openid.php/signout'>Log off</a>";
} else {
- $content = "<a href='{$ABSWEB}openid.php'>Log In</a>";
+ $content = "<a href='{$ABSWEB}auth/openid.php'>Log In</a>";
}
}

@@ -36,7 +36,7 @@
function doAuthenticate($force = false) {
if ($force) {
global $ABSWEB;
- header("Location: {$ABSWEB}openid.php");
+ header("Location: {$ABSWEB}auth/openid.php");
exit;
}
return null;


diff -r c335f24df2c7ad3a6cf66b37ab9ccf6ce5a7bb74 -r 94923e55b0289a3f58cbde5395acf07989f112eb web/auth/browserid.php
--- /dev/null
+++ b/web/auth/browserid.php
@@ -0,0 +1,41 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+include '../../inc/common.php';
+
+function verify_assertion($assertion)
+{
+ if (!strlen($assertion)) {
+ $res = new stdclass;
+ $res->status = 'error';
+ $res->reason = 'missing assertion';
+ return $res;
+ }
+ $audience = ($_SERVER['HTTPS'] ? 'https://' : 'http://') .
+ $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT'];
+ $postdata = 'assertion=' . urlencode($assertion) .
+ '&audience=' . urlencode($audience);
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, "https://browserid.org/verify");
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
+ $json = curl_exec($ch);
+ curl_close($ch);
+
+ return json_decode($json);
+}
+
+$assertion = file_get_contents('php://input');
+$res = verify_assertion($assertion);
+if ($res->status == 'okay') {
+ $user = mtrack_canon_username($res->email);
+ if ($user) {
+ $_SESSION['auth.browserid'] = $user;
+ $res->user = $user;
+ }
+}
+
+header('Content-Type: application/json');
+echo json_encode($res);
+


diff -r c335f24df2c7ad3a6cf66b37ab9ccf6ce5a7bb74 -r 94923e55b0289a3f58cbde5395acf07989f112eb web/auth/openid.php
--- /dev/null
+++ b/web/auth/openid.php
@@ -0,0 +1,212 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+include '../../inc/common.php';
+require_once 'Auth/OpenID/Consumer.php';
+require_once 'Auth/OpenID/FileStore.php';
+require_once 'Auth/OpenID/SReg.php';
+require_once 'Auth/OpenID/PAPE.php';
+
+$store_location = MTrackConfig::get('openid', 'store_dir');
+if (!$store_location) {
+ $store_location = MTrackConfig::get('core', 'vardir') . '/openid';
+}
+if (!is_dir($store_location)) {
+ mkdir($store_location);
+}
+$store = new Auth_OpenID_FileStore($store_location);
+$consumer = new Auth_OpenID_Consumer($store);
+
+$message = null;
+
+$pi = mtrack_get_pathinfo();
+if ($_SERVER['REQUEST_METHOD'] == 'POST' && $pi != 'register') {
+
+ $req = null;
+
+ if (!isset($_POST['openid_identifier']) ||
+ !strlen($_POST['openid_identifier'])) {
+ $message = "you must fill in your OpenID";
+ } else {
+ $id = $_POST['openid_identifier'];
+ if (!preg_match('/^https?:\/\//', $id)) {
+ $id = "http://$id";
+ }
+ $req = $consumer->begin($id);
+ if (!$req) {
+ $message = "not a valid OpenID";
+ }
+ }
+ if ($req) {
+ $sreg = Auth_OpenID_SRegRequest::build(
+ array('nickname', 'fullname', 'email')
+ );
+ $req->addExtension($sreg);
+
+ if ($req->shouldSendRedirect()) {
+ $rurl = $req->redirectURL(
+ $ABSWEB, $ABSWEB . 'auth/openid.php/callback');
+ if (Auth_OpenID::isFailure($rurl)) {
+ $message = "Unable to redirect to server: " . $rurl->message;
+ } else {
+ header("Location: $rurl");
+ exit;
+ }
+ } else {
+ $html = $req->htmlMarkup($ABSWEB, $ABSWEB . 'auth/openid.php/callback',
+ false, array('id' => 'openid_message'));
+ if (Auth_OpenID::isFailure($html)) {
+ $message = "Unable to redirect to server: " . $html->message;
+ } else {
+ echo $html;
+ }
+ }
+ }
+} else if ($pi == 'callback') {
+ $res = $consumer->complete($ABSWEB . 'auth/openid.php/callback');
+
+ if ($res->status == Auth_OpenID_CANCEL) {
+ $message = 'Verification cancelled';
+ } else if ($res->status == Auth_OpenID_FAILURE) {
+ $message = 'OpenID authentication failed: ' . $res->message;
+ } else if ($res->status == Auth_OpenID_SUCCESS) {
+ $id = $res->getDisplayIdentifier();
+ $sreg = Auth_OpenID_SRegResponse::fromSuccessResponse($res)->contents();
+
+ if (!empty($sreg['nickname'])) {
+ $name = $sreg['nickname'];
+ } else if (!empty($sreg['fullname'])) {
+ $name = $sreg['fullname'];
+ } else {
+ $name = $id;
+ }
+ $message = 'Authenticated as ' . $name;
+
+ $_SESSION['openid.id'] = $id;
+ unset($_SESSION['openid.userid']);
+ $_SESSION['openid.name'] = $name;
+ if (!empty($sreg['email'])) {
+ $_SESSION['openid.email'] = $sreg['email'];
+ }
+ /* See if we can find a canonical identity for the user */
+ foreach (MTrackDB::q('select userid from useraliases where alias = ?',
+ $id)->fetchAll() as $row) {
+ $_SESSION['openid.userid'] = $row[0];
+ break;
+ }
+
+ if (!isset($_SESSION['openid.userid'])) {
+ /* no alias; is there a direct userinfo entry? */
+ foreach (MTrackDB::q('select userid from userinfo where userid = ?',
+ $id)->fetchAll() as $row) {
+ $_SERVER['openid.userid'] = $row[0];
+ break;
+ }
+ }
+
+ if (!isset($_SESSION['openid.userid'])) {
+ /* prompt the user to fill out some basic details so that we can create
+ * a local identity and associate their OpenID with it */
+ header("Location: {$ABSWEB}auth/openid.php/register?" .
+ http_build_query($sreg));
+ } else {
+ header("Location: " . $ABSWEB);
+ }
+ exit;
+ } else {
+ $message = 'An error occurred while talking to your OpenID provider';
+ }
+} else if ($pi == 'signout') {
+ session_destroy();
+ header('Location: ' . $ABSWEB);
+ exit;
+} else if ($pi == 'register') {
+
+ if (!isset($_SESSION['openid.id'])) {
+ header("Location: " . $ABSWEB);
+ exit;
+ }
+
+ $userid = isset($_REQUEST['nickname']) ? $_REQUEST['nickname'] : '';
+ $email = isset($_REQUEST['email']) ? $_REQUEST['email'] : '';
+ $message = null;
+
+ /* See if we can find a canonical identity for the user */
+ foreach (MTrackDB::q('select userid from useraliases where alias = ?',
+ $_SESSION['openid.id'])->fetchAll() as $row) {
+ header("Location: " . $ABSWEB);
+ exit;
+ }
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (!strlen($userid)) {
+ $message = 'You must enter a userid';
+ } else {
+ /* is the requested id available? */
+ $avail = true;
+ foreach (MTrackDB::q('select userid from userinfo where userid = ?',
+ $userid)->fetchAll() as $row) {
+ $avail = false;
+ $message = "Your selected user ID is not available";
+ }
+ if ($avail) {
+ MTrackDB::q('insert into userinfo (userid, email, active) values (?, ?, 1)', $userid, $email);
+ /* we know the alias doesn't already exist, because we double-checked
+ * for it above */
+ MTrackDB::q('insert into useraliases (userid, alias) values (?,?)',
+ $userid, $_SESSION['openid.id']);
+ header("Location: {$ABSWEB}user.php?user=$userid&edit=1");
+ exit;
+ }
+ }
+ }
+
+ mtrack_head('Register');
+
+ $userid = htmlentities($userid, ENT_QUOTES, 'utf-8');
+ $email = htmlentities($email, ENT_QUOTES, 'utf-8');
+
+ if ($message) {
+ $message = htmlentities($message, ENT_QUOTES, 'utf-8');
+ echo <<<HTML
+<div class='ui-state-error ui-corner-all'>
+ <span class='ui-icon ui-icon-alert'></span>
+ $message
+</div>
+HTML;
+ }
+
+ echo <<<HTML
+<h1>Set up your local account</h1>
+<form method='post'>
+ User ID: <input type='text' name='nickname' value='$userid'><br>
+ Email: <input type='text' name='email' value='$email'><br>
+ <button type='submit'>Save</button>
+</form>
+
+
+HTML;
+ mtrack_foot();
+ exit;
+}
+
+mtrack_head('Authentication Required');
+echo "<h1>Please sign in with your <a id='openidlink' href='http://openid.net'><img src='{$ABSWEB}images/logo_openid.png' alt='OpenID' border='0'></a></h1>\n";
+echo "<form method='post' action='{$ABSWEB}auth/openid.php'>";
+echo "<input type='text' name='openid_identifier' id='openid_identifier'>";
+echo " <button type='submit' id='openid-sign-in'>Sign In</button>";
+
+if ($message) {
+ $message = htmlentities($message, ENT_QUOTES, 'utf-8');
+ echo <<<HTML
+<div class='ui-state-highlight ui-corner-all'>
+ <span class='ui-icon ui-icon-info'></span>
+ $message
+</div>
+HTML;
+}
+
+echo "</form>";
+
+
+mtrack_foot();
+


diff -r c335f24df2c7ad3a6cf66b37ab9ccf6ce5a7bb74 -r 94923e55b0289a3f58cbde5395acf07989f112eb web/openid.php
--- a/web/openid.php
+++ /dev/null
@@ -1,212 +0,0 @@
-<?php # vim:ts=2:sw=2:et:
-/* For licensing and copyright terms, see the file named LICENSE */
-include '../inc/common.php';
-require_once 'Auth/OpenID/Consumer.php';
-require_once 'Auth/OpenID/FileStore.php';
-require_once 'Auth/OpenID/SReg.php';
-require_once 'Auth/OpenID/PAPE.php';
-
-$store_location = MTrackConfig::get('openid', 'store_dir');
-if (!$store_location) {
- $store_location = MTrackConfig::get('core', 'vardir') . '/openid';
-}
-if (!is_dir($store_location)) {
- mkdir($store_location);
-}
-$store = new Auth_OpenID_FileStore($store_location);
-$consumer = new Auth_OpenID_Consumer($store);
-
-$message = null;
-
-$pi = mtrack_get_pathinfo();
-if ($_SERVER['REQUEST_METHOD'] == 'POST' && $pi != 'register') {
-
- $req = null;
-
- if (!isset($_POST['openid_identifier']) ||
- !strlen($_POST['openid_identifier'])) {
- $message = "you must fill in your OpenID";
- } else {
- $id = $_POST['openid_identifier'];
- if (!preg_match('/^https?:\/\//', $id)) {
- $id = "http://$id";
- }
- $req = $consumer->begin($id);
- if (!$req) {
- $message = "not a valid OpenID";
- }
- }
- if ($req) {
- $sreg = Auth_OpenID_SRegRequest::build(
- array('nickname', 'fullname', 'email')
- );
- $req->addExtension($sreg);
-
- if ($req->shouldSendRedirect()) {
- $rurl = $req->redirectURL(
- $ABSWEB, $ABSWEB . 'openid.php/callback');
- if (Auth_OpenID::isFailure($rurl)) {
- $message = "Unable to redirect to server: " . $rurl->message;
- } else {
- header("Location: $rurl");
- exit;
- }
- } else {
- $html = $req->htmlMarkup($ABSWEB, $ABSWEB . 'openid.php/callback',
- false, array('id' => 'openid_message'));
- if (Auth_OpenID::isFailure($html)) {
- $message = "Unable to redirect to server: " . $html->message;
- } else {
- echo $html;
- }
- }
- }
-} else if ($pi == 'callback') {
- $res = $consumer->complete($ABSWEB . 'openid.php/callback');
-
- if ($res->status == Auth_OpenID_CANCEL) {
- $message = 'Verification cancelled';
- } else if ($res->status == Auth_OpenID_FAILURE) {
- $message = 'OpenID authentication failed: ' . $res->message;
- } else if ($res->status == Auth_OpenID_SUCCESS) {
- $id = $res->getDisplayIdentifier();
- $sreg = Auth_OpenID_SRegResponse::fromSuccessResponse($res)->contents();
-
- if (!empty($sreg['nickname'])) {
- $name = $sreg['nickname'];
- } else if (!empty($sreg['fullname'])) {
- $name = $sreg['fullname'];
- } else {
- $name = $id;
- }
- $message = 'Authenticated as ' . $name;
-
- $_SESSION['openid.id'] = $id;
- unset($_SESSION['openid.userid']);
- $_SESSION['openid.name'] = $name;
- if (!empty($sreg['email'])) {
- $_SESSION['openid.email'] = $sreg['email'];
- }
- /* See if we can find a canonical identity for the user */
- foreach (MTrackDB::q('select userid from useraliases where alias = ?',
- $id)->fetchAll() as $row) {
- $_SESSION['openid.userid'] = $row[0];
- break;
- }
-
- if (!isset($_SESSION['openid.userid'])) {
- /* no alias; is there a direct userinfo entry? */
- foreach (MTrackDB::q('select userid from userinfo where userid = ?',
- $id)->fetchAll() as $row) {
- $_SERVER['openid.userid'] = $row[0];
- break;
- }
- }
-
- if (!isset($_SESSION['openid.userid'])) {
- /* prompt the user to fill out some basic details so that we can create
- * a local identity and associate their OpenID with it */
- header("Location: {$ABSWEB}openid.php/register?" .
- http_build_query($sreg));
- } else {
- header("Location: " . $ABSWEB);
- }
- exit;
- } else {
- $message = 'An error occurred while talking to your OpenID provider';
- }
-} else if ($pi == 'signout') {
- session_destroy();
- header('Location: ' . $ABSWEB);
- exit;
-} else if ($pi == 'register') {
-
- if (!isset($_SESSION['openid.id'])) {
- header("Location: " . $ABSWEB);
- exit;
- }
-
- $userid = isset($_REQUEST['nickname']) ? $_REQUEST['nickname'] : '';
- $email = isset($_REQUEST['email']) ? $_REQUEST['email'] : '';
- $message = null;
-
- /* See if we can find a canonical identity for the user */
- foreach (MTrackDB::q('select userid from useraliases where alias = ?',
- $_SESSION['openid.id'])->fetchAll() as $row) {
- header("Location: " . $ABSWEB);
- exit;
- }
-
- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- if (!strlen($userid)) {
- $message = 'You must enter a userid';
- } else {
- /* is the requested id available? */
- $avail = true;
- foreach (MTrackDB::q('select userid from userinfo where userid = ?',
- $userid)->fetchAll() as $row) {
- $avail = false;
- $message = "Your selected user ID is not available";
- }
- if ($avail) {
- MTrackDB::q('insert into userinfo (userid, email, active) values (?, ?, 1)', $userid, $email);
- /* we know the alias doesn't already exist, because we double-checked
- * for it above */
- MTrackDB::q('insert into useraliases (userid, alias) values (?,?)',
- $userid, $_SESSION['openid.id']);
- header("Location: {$ABSWEB}user.php?user=$userid&edit=1");
- exit;
- }
- }
- }
-
- mtrack_head('Register');
-
- $userid = htmlentities($userid, ENT_QUOTES, 'utf-8');
- $email = htmlentities($email, ENT_QUOTES, 'utf-8');
-
- if ($message) {
- $message = htmlentities($message, ENT_QUOTES, 'utf-8');
- echo <<<HTML
-<div class='ui-state-error ui-corner-all'>
- <span class='ui-icon ui-icon-alert'></span>
- $message
-</div>
-HTML;
- }
-
- echo <<<HTML
-<h1>Set up your local account</h1>
-<form method='post'>
- User ID: <input type='text' name='nickname' value='$userid'><br>
- Email: <input type='text' name='email' value='$email'><br>
- <button type='submit'>Save</button>
-</form>
-
-
-HTML;
- mtrack_foot();
- exit;
-}
-
-mtrack_head('Authentication Required');
-echo "<h1>Please sign in with your <a id='openidlink' href='http://openid.net'><img src='{$ABSWEB}images/logo_openid.png' alt='OpenID' border='0'></a></h1>\n";
-echo "<form method='post' action='{$ABSWEB}openid.php'>";
-echo "<input type='text' name='openid_identifier' id='openid_identifier'>";
-echo " <button type='submit' id='openid-sign-in'>Sign In</button>";
-
-if ($message) {
- $message = htmlentities($message, ENT_QUOTES, 'utf-8');
- echo <<<HTML
-<div class='ui-state-highlight ui-corner-all'>
- <span class='ui-icon ui-icon-info'></span>
- $message
-</div>
-HTML;
-}
-
-echo "</form>";
-
-
-mtrack_foot();
-



https://bitbucket.org/wez/mtrack/changeset/a097368a5f0c/
changeset: a097368a5f0c
user: wez
date: 2012-04-28 17:56:01
summary: add facebook authentication
affected #: 6 files

diff -r 94923e55b0289a3f58cbde5395acf07989f112eb -r a097368a5f0cf9eb4170c3e711706c4f50552044 inc/auth.php
--- a/inc/auth.php
+++ b/inc/auth.php
@@ -5,6 +5,7 @@
include_once MTRACK_INC_DIR . '/auth/http.php';
include_once MTRACK_INC_DIR . '/auth/openid.php';
include_once MTRACK_INC_DIR . '/auth/browserid.php';
+include_once MTRACK_INC_DIR . '/auth/facebook.php';

interface IMTrackAuth {
/** Returns the authenticated user, or null if authentication is


diff -r 94923e55b0289a3f58cbde5395acf07989f112eb -r a097368a5f0cf9eb4170c3e711706c4f50552044 inc/auth/facebook.php
--- /dev/null
+++ b/inc/auth/facebook.php
@@ -0,0 +1,137 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+
+/* You can enable this module using this in your config.ini:
+ * [plugins]
+ * MTrackAuth_Facebook = "APPID,APPSECRET"
+ */
+
+class MTrackAuth_Facebook implements IMTrackAuth, IMTrackNavigationHelper {
+ var $appid;
+ var $secret;
+ var $fb;
+ var $me = null;
+
+ function __construct($appid, $secret) {
+ $this->appid = $appid;
+ $this->secret = $secret;
+ MTrackAuth::registerMech($this);
+ MTrackNavigation::registerHelper($this);
+ }
+
+ function getFB() {
+ if (!$this->fb) {
+ set_include_path(MTRACK_INC_DIR . '/lib/facebook:' .
+ get_include_path());
+ include 'facebook.php';
+
+ $this->fb = new Facebook(array(
+ 'appId' => $this->appid,
+ 'secret' => $this->secret
+ ));
+ }
+ return $this->fb;
+ }
+
+ function getMe() {
+ if ($this->me === null) {
+ $this->me = false;
+
+ session_start();
+
+ if (isset($_SESSION['auth.facebook'])) {
+ $profile = $_SESSION['auth.facebook'];
+ $this->me = $profile;
+ return $this->me;
+ }
+
+ $fb = $this->getFB();
+ $user = $fb->getUser();
+ if ($user) {
+ try {
+ $profile = $fb->api('/me');
+ $this->me = $profile;
+ $_SESSION['auth.facebook'] = $this->me;
+ } catch (Exception $e) {
+ }
+ }
+ }
+ return $this->me;
+ }
+
+ function augmentUserInfo(&$content) {
+ global $ABSWEB;
+
+ if (isset($_SESSION['auth.facebook'])) {
+ return;
+ }
+
+ $me = $this->getMe();
+ if (!$me) {
+ $fb = $this->getFB();
+ $url = $fb->getLoginUrl(array(
+ 'redirect_uri' => $GLOBALS['ABSWEB'] . 'auth/facebook.php'
+ ));
+
+ $content = <<<HTML
+<a href='$url'>Log In</a>
+HTML;
+ }
+ }
+
+ function augmentNavigation($id, &$items) {
+ }
+
+ function authenticate() {
+ $me = $this->getMe();
+ if ($me) {
+ return mtrack_canon_username($me['email']);
+ }
+
+ return null;
+ }
+
+ function doAuthenticate($force = false) {
+ if ($force) {
+ header("Location: " . $this->getFB()->getLoginUrl(array(
+ 'redirect_uri' => $GLOBALS['ABSWEB'] . 'auth/facebook.php'
+ )));
+ exit;
+ }
+ return null;
+ }
+
+ function enumGroups() {
+ return null;
+ }
+
+ function getGroups($username) {
+ return null;
+ }
+
+ function addToGroup($username, $groupname) {
+ return null;
+ }
+
+ function removeFromGroup($username, $groupname) {
+ return null;
+ }
+
+ function getUserData($username) {
+ return null;
+ }
+
+ function canLogOut() {
+ return true;
+ }
+
+ function LogOut() {
+ session_destroy();
+ $fb = $this->getFB();
+ header('Location: ' . $fb->getLogoutUrl(array(
+ 'next' => $GLOBALS['ABSWEB']
+ )));
+ exit;
+ }
+}
+


diff -r 94923e55b0289a3f58cbde5395acf07989f112eb -r a097368a5f0cf9eb4170c3e711706c4f50552044 inc/lib/facebook/base_facebook.php
--- /dev/null
+++ b/inc/lib/facebook/base_facebook.php
@@ -0,0 +1,1269 @@
+<?php
+/**
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+if (!function_exists('curl_init')) {
+ throw new Exception('Facebook needs the CURL PHP extension.');
+}
+if (!function_exists('json_decode')) {
+ throw new Exception('Facebook needs the JSON PHP extension.');
+}
+
+/**
+ * Thrown when an API call returns an exception.
+ *
+ * @author Naitik Shah <nai...@facebook.com>
+ */
+class FacebookApiException extends Exception
+{
+ /**
+ * The result from the API server that represents the exception information.
+ */
+ protected $result;
+
+ /**
+ * Make a new API Exception with the given result.
+ *
+ * @param array $result The result from the API server
+ */
+ public function __construct($result) {
+ $this->result = $result;
+
+ $code = isset($result['error_code']) ? $result['error_code'] : 0;
+
+ if (isset($result['error_description'])) {
+ // OAuth 2.0 Draft 10 style
+ $msg = $result['error_description'];
+ } else if (isset($result['error']) && is_array($result['error'])) {
+ // OAuth 2.0 Draft 00 style
+ $msg = $result['error']['message'];
+ } else if (isset($result['error_msg'])) {
+ // Rest server style
+ $msg = $result['error_msg'];
+ } else {
+ $msg = 'Unknown Error. Check getResult()';
+ }
+
+ parent::__construct($msg, $code);
+ }
+
+ /**
+ * Return the associated result object returned by the API server.
+ *
+ * @return array The result from the API server
+ */
+ public function getResult() {
+ return $this->result;
+ }
+
+ /**
+ * Returns the associated type for the error. This will default to
+ * 'Exception' when a type is not available.
+ *
+ * @return string
+ */
+ public function getType() {
+ if (isset($this->result['error'])) {
+ $error = $this->result['error'];
+ if (is_string($error)) {
+ // OAuth 2.0 Draft 10 style
+ return $error;
+ } else if (is_array($error)) {
+ // OAuth 2.0 Draft 00 style
+ if (isset($error['type'])) {
+ return $error['type'];
+ }
+ }
+ }
+
+ return 'Exception';
+ }
+
+ /**
+ * To make debugging easier.
+ *
+ * @return string The string representation of the error
+ */
+ public function __toString() {
+ $str = $this->getType() . ': ';
+ if ($this->code != 0) {
+ $str .= $this->code . ': ';
+ }
+ return $str . $this->message;
+ }
+}
+
+/**
+ * Provides access to the Facebook Platform. This class provides
+ * a majority of the functionality needed, but the class is abstract
+ * because it is designed to be sub-classed. The subclass must
+ * implement the four abstract methods listed at the bottom of
+ * the file.
+ *
+ * @author Naitik Shah <nai...@facebook.com>
+ */
+abstract class BaseFacebook
+{
+ /**
+ * Version.
+ */
+ const VERSION = '3.1.1';
+
+ /**
+ * Default options for curl.
+ */
+ public static $CURL_OPTS = array(
+ CURLOPT_CONNECTTIMEOUT => 10,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 60,
+ CURLOPT_USERAGENT => 'facebook-php-3.1',
+ );
+
+ /**
+ * List of query parameters that get automatically dropped when rebuilding
+ * the current URL.
+ */
+ protected static $DROP_QUERY_PARAMS = array(
+ 'code',
+ 'state',
+ 'signed_request',
+ );
+
+ /**
+ * Maps aliases to Facebook domains.
+ */
+ public static $DOMAIN_MAP = array(
+ 'api' => 'https://api.facebook.com/',
+ 'api_video' => 'https://api-video.facebook.com/',
+ 'api_read' => 'https://api-read.facebook.com/',
+ 'graph' => 'https://graph.facebook.com/',
+ 'graph_video' => 'https://graph-video.facebook.com/',
+ 'www' => 'https://www.facebook.com/',
+ );
+
+ /**
+ * The Application ID.
+ *
+ * @var string
+ */
+ protected $appId;
+
+ /**
+ * The Application App Secret.
+ *
+ * @var string
+ */
+ protected $appSecret;
+
+ /**
+ * The ID of the Facebook user, or 0 if the user is logged out.
+ *
+ * @var integer
+ */
+ protected $user;
+
+ /**
+ * The data from the signed_request token.
+ */
+ protected $signedRequest;
+
+ /**
+ * A CSRF state variable to assist in the defense against CSRF attacks.
+ */
+ protected $state;
+
+ /**
+ * The OAuth access token received in exchange for a valid authorization
+ * code. null means the access token has yet to be determined.
+ *
+ * @var string
+ */
+ protected $accessToken = null;
+
+ /**
+ * Indicates if the CURL based @ syntax for file uploads is enabled.
+ *
+ * @var boolean
+ */
+ protected $fileUploadSupport = false;
+
+ /**
+ * Initialize a Facebook Application.
+ *
+ * The configuration:
+ * - appId: the application ID
+ * - secret: the application secret
+ * - fileUpload: (optional) boolean indicating if file uploads are enabled
+ *
+ * @param array $config The application configuration
+ */
+ public function __construct($config) {
+ $this->setAppId($config['appId']);
+ $this->setAppSecret($config['secret']);
+ if (isset($config['fileUpload'])) {
+ $this->setFileUploadSupport($config['fileUpload']);
+ }
+
+ $state = $this->getPersistentData('state');
+ if (!empty($state)) {
+ $this->state = $this->getPersistentData('state');
+ }
+ }
+
+ /**
+ * Set the Application ID.
+ *
+ * @param string $appId The Application ID
+ * @return BaseFacebook
+ */
+ public function setAppId($appId) {
+ $this->appId = $appId;
+ return $this;
+ }
+
+ /**
+ * Get the Application ID.
+ *
+ * @return string the Application ID
+ */
+ public function getAppId() {
+ return $this->appId;
+ }
+
+ /**
+ * Set the App Secret.
+ *
+ * @param string $apiSecret The App Secret
+ * @return BaseFacebook
+ * @deprecated
+ */
+ public function setApiSecret($apiSecret) {
+ $this->setAppSecret($apiSecret);
+ return $this;
+ }
+
+ /**
+ * Set the App Secret.
+ *
+ * @param string $appSecret The App Secret
+ * @return BaseFacebook
+ */
+ public function setAppSecret($appSecret) {
+ $this->appSecret = $appSecret;
+ return $this;
+ }
+
+ /**
+ * Get the App Secret.
+ *
+ * @return string the App Secret
+ * @deprecated
+ */
+ public function getApiSecret() {
+ return $this->getAppSecret();
+ }
+
+ /**
+ * Get the App Secret.
+ *
+ * @return string the App Secret
+ */
+ public function getAppSecret() {
+ return $this->appSecret;
+ }
+
+ /**
+ * Set the file upload support status.
+ *
+ * @param boolean $fileUploadSupport The file upload support status.
+ * @return BaseFacebook
+ */
+ public function setFileUploadSupport($fileUploadSupport) {
+ $this->fileUploadSupport = $fileUploadSupport;
+ return $this;
+ }
+
+ /**
+ * Get the file upload support status.
+ *
+ * @return boolean true if and only if the server supports file upload.
+ */
+ public function getFileUploadSupport() {
+ return $this->fileUploadSupport;
+ }
+
+ /**
+ * DEPRECATED! Please use getFileUploadSupport instead.
+ *
+ * Get the file upload support status.
+ *
+ * @return boolean true if and only if the server supports file upload.
+ */
+ public function useFileUploadSupport() {
+ return $this->getFileUploadSupport();
+ }
+
+ /**
+ * Sets the access token for api calls. Use this if you get
+ * your access token by other means and just want the SDK
+ * to use it.
+ *
+ * @param string $access_token an access token.
+ * @return BaseFacebook
+ */
+ public function setAccessToken($access_token) {
+ $this->accessToken = $access_token;
+ return $this;
+ }
+
+ /**
+ * Determines the access token that should be used for API calls.
+ * The first time this is called, $this->accessToken is set equal
+ * to either a valid user access token, or it's set to the application
+ * access token if a valid user access token wasn't available. Subsequent
+ * calls return whatever the first call returned.
+ *
+ * @return string The access token
+ */
+ public function getAccessToken() {
+ if ($this->accessToken !== null) {
+ // we've done this already and cached it. Just return.
+ return $this->accessToken;
+ }
+
+ // first establish access token to be the application
+ // access token, in case we navigate to the /oauth/access_token
+ // endpoint, where SOME access token is required.
+ $this->setAccessToken($this->getApplicationAccessToken());
+ $user_access_token = $this->getUserAccessToken();
+ if ($user_access_token) {
+ $this->setAccessToken($user_access_token);
+ }
+
+ return $this->accessToken;
+ }
+
+ /**
+ * Determines and returns the user access token, first using
+ * the signed request if present, and then falling back on
+ * the authorization code if present. The intent is to
+ * return a valid user access token, or false if one is determined
+ * to not be available.
+ *
+ * @return string A valid user access token, or false if one
+ * could not be determined.
+ */
+ protected function getUserAccessToken() {
+ // first, consider a signed request if it's supplied.
+ // if there is a signed request, then it alone determines
+ // the access token.
+ $signed_request = $this->getSignedRequest();
+ if ($signed_request) {
+ // apps.facebook.com hands the access_token in the signed_request
+ if (array_key_exists('oauth_token', $signed_request)) {
+ $access_token = $signed_request['oauth_token'];
+ $this->setPersistentData('access_token', $access_token);
+ return $access_token;
+ }
+
+ // the JS SDK puts a code in with the redirect_uri of ''
+ if (array_key_exists('code', $signed_request)) {
+ $code = $signed_request['code'];
+ $access_token = $this->getAccessTokenFromCode($code, '');
+ if ($access_token) {
+ $this->setPersistentData('code', $code);
+ $this->setPersistentData('access_token', $access_token);
+ return $access_token;
+ }
+ }
+
+ // signed request states there's no access token, so anything
+ // stored should be cleared.
+ $this->clearAllPersistentData();
+ return false; // respect the signed request's data, even
+ // if there's an authorization code or something else
+ }
+
+ $code = $this->getCode();
+ if ($code && $code != $this->getPersistentData('code')) {
+ $access_token = $this->getAccessTokenFromCode($code);
+ if ($access_token) {
+ $this->setPersistentData('code', $code);
+ $this->setPersistentData('access_token', $access_token);
+ return $access_token;
+ }
+
+ // code was bogus, so everything based on it should be invalidated.
+ $this->clearAllPersistentData();
+ return false;
+ }
+
+ // as a fallback, just return whatever is in the persistent
+ // store, knowing nothing explicit (signed request, authorization
+ // code, etc.) was present to shadow it (or we saw a code in $_REQUEST,
+ // but it's the same as what's in the persistent store)
+ return $this->getPersistentData('access_token');
+ }
+
+ /**
+ * Retrieve the signed request, either from a request parameter or,
+ * if not present, from a cookie.
+ *
+ * @return string the signed request, if available, or null otherwise.
+ */
+ public function getSignedRequest() {
+ if (!$this->signedRequest) {
+ if (isset($_REQUEST['signed_request'])) {
+ $this->signedRequest = $this->parseSignedRequest(
+ $_REQUEST['signed_request']);
+ } else if (isset($_COOKIE[$this->getSignedRequestCookieName()])) {
+ $this->signedRequest = $this->parseSignedRequest(
+ $_COOKIE[$this->getSignedRequestCookieName()]);
+ }
+ }
+ return $this->signedRequest;
+ }
+
+ /**
+ * Get the UID of the connected user, or 0
+ * if the Facebook user is not connected.
+ *
+ * @return string the UID if available.
+ */
+ public function getUser() {
+ if ($this->user !== null) {
+ // we've already determined this and cached the value.
+ return $this->user;
+ }
+
+ return $this->user = $this->getUserFromAvailableData();
+ }
+
+ /**
+ * Determines the connected user by first examining any signed
+ * requests, then considering an authorization code, and then
+ * falling back to any persistent store storing the user.
+ *
+ * @return integer The id of the connected Facebook user,
+ * or 0 if no such user exists.
+ */
+ protected function getUserFromAvailableData() {
+ // if a signed request is supplied, then it solely determines
+ // who the user is.
+ $signed_request = $this->getSignedRequest();
+ if ($signed_request) {
+ if (array_key_exists('user_id', $signed_request)) {
+ $user = $signed_request['user_id'];
+ $this->setPersistentData('user_id', $signed_request['user_id']);
+ return $user;
+ }
+
+ // if the signed request didn't present a user id, then invalidate
+ // all entries in any persistent store.
+ $this->clearAllPersistentData();
+ return 0;
+ }
+
+ $user = $this->getPersistentData('user_id', $default = 0);
+ $persisted_access_token = $this->getPersistentData('access_token');
+
+ // use access_token to fetch user id if we have a user access_token, or if
+ // the cached access token has changed.
+ $access_token = $this->getAccessToken();
+ if ($access_token &&
+ $access_token != $this->getApplicationAccessToken() &&
+ !($user && $persisted_access_token == $access_token)) {
+ $user = $this->getUserFromAccessToken();
+ if ($user) {
+ $this->setPersistentData('user_id', $user);
+ } else {
+ $this->clearAllPersistentData();
+ }
+ }
+
+ return $user;
+ }
+
+ /**
+ * Get a Login URL for use with redirects. By default, full page redirect is
+ * assumed. If you are using the generated URL with a window.open() call in
+ * JavaScript, you can pass in display=popup as part of the $params.
+ *
+ * The parameters:
+ * - redirect_uri: the url to go to after a successful login
+ * - scope: comma separated list of requested extended perms
+ *
+ * @param array $params Provide custom parameters
+ * @return string The URL for the login flow
+ */
+ public function getLoginUrl($params=array()) {
+ $this->establishCSRFTokenState();
+ $currentUrl = $this->getCurrentUrl();
+
+ // if 'scope' is passed as an array, convert to comma separated list
+ $scopeParams = isset($params['scope']) ? $params['scope'] : null;
+ if ($scopeParams && is_array($scopeParams)) {
+ $params['scope'] = implode(',', $scopeParams);
+ }
+
+ return $this->getUrl(
+ 'www',
+ 'dialog/oauth',
+ array_merge(array(
+ 'client_id' => $this->getAppId(),
+ 'redirect_uri' => $currentUrl, // possibly overwritten
+ 'state' => $this->state),
+ $params));
+ }
+
+ /**
+ * Get a Logout URL suitable for use with redirects.
+ *
+ * The parameters:
+ * - next: the url to go to after a successful logout
+ *
+ * @param array $params Provide custom parameters
+ * @return string The URL for the logout flow
+ */
+ public function getLogoutUrl($params=array()) {
+ return $this->getUrl(
+ 'www',
+ 'logout.php',
+ array_merge(array(
+ 'next' => $this->getCurrentUrl(),
+ 'access_token' => $this->getAccessToken(),
+ ), $params)
+ );
+ }
+
+ /**
+ * Get a login status URL to fetch the status from Facebook.
+ *
+ * The parameters:
+ * - ok_session: the URL to go to if a session is found
+ * - no_session: the URL to go to if the user is not connected
+ * - no_user: the URL to go to if the user is not signed into facebook
+ *
+ * @param array $params Provide custom parameters
+ * @return string The URL for the logout flow
+ */
+ public function getLoginStatusUrl($params=array()) {
+ return $this->getUrl(
+ 'www',
+ 'extern/login_status.php',
+ array_merge(array(
+ 'api_key' => $this->getAppId(),
+ 'no_session' => $this->getCurrentUrl(),
+ 'no_user' => $this->getCurrentUrl(),
+ 'ok_session' => $this->getCurrentUrl(),
+ 'session_version' => 3,
+ ), $params)
+ );
+ }
+
+ /**
+ * Make an API call.
+ *
+ * @return mixed The decoded response
+ */
+ public function api(/* polymorphic */) {
+ $args = func_get_args();
+ if (is_array($args[0])) {
+ return $this->_restserver($args[0]);
+ } else {
+ return call_user_func_array(array($this, '_graph'), $args);
+ }
+ }
+
+ /**
+ * Constructs and returns the name of the cookie that
+ * potentially houses the signed request for the app user.
+ * The cookie is not set by the BaseFacebook class, but
+ * it may be set by the JavaScript SDK.
+ *
+ * @return string the name of the cookie that would house
+ * the signed request value.
+ */
+ protected function getSignedRequestCookieName() {
+ return 'fbsr_'.$this->getAppId();
+ }
+
+ /**
+ * Constructs and returns the name of the coookie that potentially contain
+ * metadata. The cookie is not set by the BaseFacebook class, but it may be
+ * set by the JavaScript SDK.
+ *
+ * @return string the name of the cookie that would house metadata.
+ */
+ protected function getMetadataCookieName() {
+ return 'fbm_'.$this->getAppId();
+ }
+
+ /**
+ * Get the authorization code from the query parameters, if it exists,
+ * and otherwise return false to signal no authorization code was
+ * discoverable.
+ *
+ * @return mixed The authorization code, or false if the authorization
+ * code could not be determined.
+ */
+ protected function getCode() {
+ if (isset($_REQUEST['code'])) {
+ if ($this->state !== null &&
+ isset($_REQUEST['state']) &&
+ $this->state === $_REQUEST['state']) {
+
+ // CSRF state has done its job, so clear it
+ $this->state = null;
+ $this->clearPersistentData('state');
+ return $_REQUEST['code'];
+ } else {
+ self::errorLog('CSRF state token does not match one provided.');
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Retrieves the UID with the understanding that
+ * $this->accessToken has already been set and is
+ * seemingly legitimate. It relies on Facebook's Graph API
+ * to retrieve user information and then extract
+ * the user ID.
+ *
+ * @return integer Returns the UID of the Facebook user, or 0
+ * if the Facebook user could not be determined.
+ */
+ protected function getUserFromAccessToken() {
+ try {
+ $user_info = $this->api('/me');
+ return $user_info['id'];
+ } catch (FacebookApiException $e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the access token that should be used for logged out
+ * users when no authorization code is available.
+ *
+ * @return string The application access token, useful for gathering
+ * public information about users and applications.
+ */
+ protected function getApplicationAccessToken() {
+ return $this->appId.'|'.$this->appSecret;
+ }
+
+ /**
+ * Lays down a CSRF state token for this process.
+ *
+ * @return void
+ */
+ protected function establishCSRFTokenState() {
+ if ($this->state === null) {
+ $this->state = md5(uniqid(mt_rand(), true));
+ $this->setPersistentData('state', $this->state);
+ }
+ }
+
+ /**
+ * Retrieves an access token for the given authorization code
+ * (previously generated from www.facebook.com on behalf of
+ * a specific user). The authorization code is sent to graph.facebook.com
+ * and a legitimate access token is generated provided the access token
+ * and the user for which it was generated all match, and the user is
+ * either logged in to Facebook or has granted an offline access permission.
+ *
+ * @param string $code An authorization code.
+ * @return mixed An access token exchanged for the authorization code, or
+ * false if an access token could not be generated.
+ */
+ protected function getAccessTokenFromCode($code, $redirect_uri = null) {
+ if (empty($code)) {
+ return false;
+ }
+
+ if ($redirect_uri === null) {
+ $redirect_uri = $this->getCurrentUrl();
+ }
+
+ try {
+ // need to circumvent json_decode by calling _oauthRequest
+ // directly, since response isn't JSON format.
+ $access_token_response =
+ $this->_oauthRequest(
+ $this->getUrl('graph', '/oauth/access_token'),
+ $params = array('client_id' => $this->getAppId(),
+ 'client_secret' => $this->getAppSecret(),
+ 'redirect_uri' => $redirect_uri,
+ 'code' => $code));
+ } catch (FacebookApiException $e) {
+ // most likely that user very recently revoked authorization.
+ // In any event, we don't have an access token, so say so.
+ return false;
+ }
+
+ if (empty($access_token_response)) {
+ return false;
+ }
+
+ $response_params = array();
+ parse_str($access_token_response, $response_params);
+ if (!isset($response_params['access_token'])) {
+ return false;
+ }
+
+ return $response_params['access_token'];
+ }
+
+ /**
+ * Invoke the old restserver.php endpoint.
+ *
+ * @param array $params Method call object
+ *
+ * @return mixed The decoded response object
+ * @throws FacebookApiException
+ */
+ protected function _restserver($params) {
+ // generic application level parameters
+ $params['api_key'] = $this->getAppId();
+ $params['format'] = 'json-strings';
+
+ $result = json_decode($this->_oauthRequest(
+ $this->getApiUrl($params['method']),
+ $params
+ ), true);
+
+ // results are returned, errors are thrown
+ if (is_array($result) && isset($result['error_code'])) {
+ $this->throwAPIException($result);
+ }
+
+ if ($params['method'] === 'auth.expireSession' ||
+ $params['method'] === 'auth.revokeAuthorization') {
+ $this->destroySession();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return true if this is video post.
+ *
+ * @param string $path The path
+ * @param string $method The http method (default 'GET')
+ *
+ * @return boolean true if this is video post
+ */
+ protected function isVideoPost($path, $method = 'GET') {
+ if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Invoke the Graph API.
+ *
+ * @param string $path The path (required)
+ * @param string $method The http method (default 'GET')
+ * @param array $params The query/post data
+ *
+ * @return mixed The decoded response object
+ * @throws FacebookApiException
+ */
+ protected function _graph($path, $method = 'GET', $params = array()) {
+ if (is_array($method) && empty($params)) {
+ $params = $method;
+ $method = 'GET';
+ }
+ $params['method'] = $method; // method override as we always do a POST
+
+ if ($this->isVideoPost($path, $method)) {
+ $domainKey = 'graph_video';
+ } else {
+ $domainKey = 'graph';
+ }
+
+ $result = json_decode($this->_oauthRequest(
+ $this->getUrl($domainKey, $path),
+ $params
+ ), true);
+
+ // results are returned, errors are thrown
+ if (is_array($result) && isset($result['error'])) {
+ $this->throwAPIException($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Make a OAuth Request.
+ *
+ * @param string $url The path (required)
+ * @param array $params The query/post data
+ *
+ * @return string The decoded response object
+ * @throws FacebookApiException
+ */
+ protected function _oauthRequest($url, $params) {
+ if (!isset($params['access_token'])) {
+ $params['access_token'] = $this->getAccessToken();
+ }
+
+ // json_encode all params values that are not strings
+ foreach ($params as $key => $value) {
+ if (!is_string($value)) {
+ $params[$key] = json_encode($value);
+ }
+ }
+
+ return $this->makeRequest($url, $params);
+ }
+
+ /**
+ * Makes an HTTP request. This method can be overridden by subclasses if
+ * developers want to do fancier things or use something other than curl to
+ * make the request.
+ *
+ * @param string $url The URL to make the request to
+ * @param array $params The parameters to use for the POST body
+ * @param CurlHandler $ch Initialized curl handle
+ *
+ * @return string The response text
+ */
+ protected function makeRequest($url, $params, $ch=null) {
+ if (!$ch) {
+ $ch = curl_init();
+ }
+
+ $opts = self::$CURL_OPTS;
+ if ($this->getFileUploadSupport()) {
+ $opts[CURLOPT_POSTFIELDS] = $params;
+ } else {
+ $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&');
+ }
+ $opts[CURLOPT_URL] = $url;
+
+ // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait
+ // for 2 seconds if the server does not support this header.
+ if (isset($opts[CURLOPT_HTTPHEADER])) {
+ $existing_headers = $opts[CURLOPT_HTTPHEADER];
+ $existing_headers[] = 'Expect:';
+ $opts[CURLOPT_HTTPHEADER] = $existing_headers;
+ } else {
+ $opts[CURLOPT_HTTPHEADER] = array('Expect:');
+ }
+
+ curl_setopt_array($ch, $opts);
+ $result = curl_exec($ch);
+
+ if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
+ self::errorLog('Invalid or no certificate authority found, '.
+ 'using bundled information');
+ curl_setopt($ch, CURLOPT_CAINFO,
+ dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
+ $result = curl_exec($ch);
+ }
+
+ if ($result === false) {
+ $e = new FacebookApiException(array(
+ 'error_code' => curl_errno($ch),
+ 'error' => array(
+ 'message' => curl_error($ch),
+ 'type' => 'CurlException',
+ ),
+ ));
+ curl_close($ch);
+ throw $e;
+ }
+ curl_close($ch);
+ return $result;
+ }
+
+ /**
+ * Parses a signed_request and validates the signature.
+ *
+ * @param string $signed_request A signed token
+ * @return array The payload inside it or null if the sig is wrong
+ */
+ protected function parseSignedRequest($signed_request) {
+ list($encoded_sig, $payload) = explode('.', $signed_request, 2);
+
+ // decode the data
+ $sig = self::base64UrlDecode($encoded_sig);
+ $data = json_decode(self::base64UrlDecode($payload), true);
+
+ if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
+ self::errorLog('Unknown algorithm. Expected HMAC-SHA256');
+ return null;
+ }
+
+ // check sig
+ $expected_sig = hash_hmac('sha256', $payload,
+ $this->getAppSecret(), $raw = true);
+ if ($sig !== $expected_sig) {
+ self::errorLog('Bad Signed JSON signature!');
+ return null;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Build the URL for api given parameters.
+ *
+ * @param $method String the method name.
+ * @return string The URL for the given parameters
+ */
+ protected function getApiUrl($method) {
+ static $READ_ONLY_CALLS =
+ array('admin.getallocation' => 1,
+ 'admin.getappproperties' => 1,
+ 'admin.getbannedusers' => 1,
+ 'admin.getlivestreamvialink' => 1,
+ 'admin.getmetrics' => 1,
+ 'admin.getrestrictioninfo' => 1,
+ 'application.getpublicinfo' => 1,
+ 'auth.getapppublickey' => 1,
+ 'auth.getsession' => 1,
+ 'auth.getsignedpublicsessiondata' => 1,
+ 'comments.get' => 1,
+ 'connect.getunconnectedfriendscount' => 1,
+ 'dashboard.getactivity' => 1,
+ 'dashboard.getcount' => 1,
+ 'dashboard.getglobalnews' => 1,
+ 'dashboard.getnews' => 1,
+ 'dashboard.multigetcount' => 1,
+ 'dashboard.multigetnews' => 1,
+ 'data.getcookies' => 1,
+ 'events.get' => 1,
+ 'events.getmembers' => 1,
+ 'fbml.getcustomtags' => 1,
+ 'feed.getappfriendstories' => 1,
+ 'feed.getregisteredtemplatebundlebyid' => 1,
+ 'feed.getregisteredtemplatebundles' => 1,
+ 'fql.multiquery' => 1,
+ 'fql.query' => 1,
+ 'friends.arefriends' => 1,
+ 'friends.get' => 1,
+ 'friends.getappusers' => 1,
+ 'friends.getlists' => 1,
+ 'friends.getmutualfriends' => 1,
+ 'gifts.get' => 1,
+ 'groups.get' => 1,
+ 'groups.getmembers' => 1,
+ 'intl.gettranslations' => 1,
+ 'links.get' => 1,
+ 'notes.get' => 1,
+ 'notifications.get' => 1,
+ 'pages.getinfo' => 1,
+ 'pages.isadmin' => 1,
+ 'pages.isappadded' => 1,
+ 'pages.isfan' => 1,
+ 'permissions.checkavailableapiaccess' => 1,
+ 'permissions.checkgrantedapiaccess' => 1,
+ 'photos.get' => 1,
+ 'photos.getalbums' => 1,
+ 'photos.gettags' => 1,
+ 'profile.getinfo' => 1,
+ 'profile.getinfooptions' => 1,
+ 'stream.get' => 1,
+ 'stream.getcomments' => 1,
+ 'stream.getfilters' => 1,
+ 'users.getinfo' => 1,
+ 'users.getloggedinuser' => 1,
+ 'users.getstandardinfo' => 1,
+ 'users.hasapppermission' => 1,
+ 'users.isappuser' => 1,
+ 'users.isverified' => 1,
+ 'video.getuploadlimits' => 1);
+ $name = 'api';
+ if (isset($READ_ONLY_CALLS[strtolower($method)])) {
+ $name = 'api_read';
+ } else if (strtolower($method) == 'video.upload') {
+ $name = 'api_video';
+ }
+ return self::getUrl($name, 'restserver.php');
+ }
+
+ /**
+ * Build the URL for given domain alias, path and parameters.
+ *
+ * @param $name string The name of the domain
+ * @param $path string Optional path (without a leading slash)
+ * @param $params array Optional query parameters
+ *
+ * @return string The URL for the given parameters
+ */
+ protected function getUrl($name, $path='', $params=array()) {
+ $url = self::$DOMAIN_MAP[$name];
+ if ($path) {
+ if ($path[0] === '/') {
+ $path = substr($path, 1);
+ }
+ $url .= $path;
+ }
+ if ($params) {
+ $url .= '?' . http_build_query($params, null, '&');
+ }
+
+ return $url;
+ }
+
+ /**
+ * Returns the Current URL, stripping it of known FB parameters that should
+ * not persist.
+ *
+ * @return string The current URL
+ */
+ protected function getCurrentUrl() {
+ if (isset($_SERVER['HTTPS']) &&
+ ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) ||
+ isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
+ $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
+ $protocol = 'https://';
+ }
+ else {
+ $protocol = 'http://';
+ }
+ $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ $parts = parse_url($currentUrl);
+
+ $query = '';
+ if (!empty($parts['query'])) {
+ // drop known fb params
+ $params = explode('&', $parts['query']);
+ $retained_params = array();
+ foreach ($params as $param) {
+ if ($this->shouldRetainParam($param)) {
+ $retained_params[] = $param;
+ }
+ }
+
+ if (!empty($retained_params)) {
+ $query = '?'.implode($retained_params, '&');
+ }
+ }
+
+ // use port if non default
+ $port =
+ isset($parts['port']) &&
+ (($protocol === 'http://' && $parts['port'] !== 80) ||
+ ($protocol === 'https://' && $parts['port'] !== 443))
+ ? ':' . $parts['port'] : '';
+
+ // rebuild
+ return $protocol . $parts['host'] . $port . $parts['path'] . $query;
+ }
+
+ /**
+ * Returns true if and only if the key or key/value pair should
+ * be retained as part of the query string. This amounts to
+ * a brute-force search of the very small list of Facebook-specific
+ * params that should be stripped out.
+ *
+ * @param string $param A key or key/value pair within a URL's query (e.g.
+ * 'foo=a', 'foo=', or 'foo'.
+ *
+ * @return boolean
+ */
+ protected function shouldRetainParam($param) {
+ foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) {
+ if (strpos($param, $drop_query_param.'=') === 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Analyzes the supplied result to see if it was thrown
+ * because the access token is no longer valid. If that is
+ * the case, then we destroy the session.
+ *
+ * @param $result array A record storing the error message returned
+ * by a failed API call.
+ */
+ protected function throwAPIException($result) {
+ $e = new FacebookApiException($result);
+ switch ($e->getType()) {
+ // OAuth 2.0 Draft 00 style
+ case 'OAuthException':
+ // OAuth 2.0 Draft 10 style
+ case 'invalid_token':
+ // REST server errors are just Exceptions
+ case 'Exception':
+ $message = $e->getMessage();
+ if ((strpos($message, 'Error validating access token') !== false) ||
+ (strpos($message, 'Invalid OAuth access token') !== false) ||
+ (strpos($message, 'An active access token must be used') !== false)
+ ) {
+ $this->destroySession();
+ }
+ break;
+ }
+
+ throw $e;
+ }
+
+
+ /**
+ * Prints to the error log if you aren't in command line mode.
+ *
+ * @param string $msg Log message
+ */
+ protected static function errorLog($msg) {
+ // disable error log if we are running in a CLI environment
+ // @codeCoverageIgnoreStart
+ if (php_sapi_name() != 'cli') {
+ error_log($msg);
+ }
+ // uncomment this if you want to see the errors on the page
+ // print 'error_log: '.$msg."\n";
+ // @codeCoverageIgnoreEnd
+ }
+
+ /**
+ * Base64 encoding that doesn't need to be urlencode()ed.
+ * Exactly the same as base64_encode except it uses
+ * - instead of +
+ * _ instead of /
+ *
+ * @param string $input base64UrlEncoded string
+ * @return string
+ */
+ protected static function base64UrlDecode($input) {
+ return base64_decode(strtr($input, '-_', '+/'));
+ }
+
+ /**
+ * Destroy the current session
+ */
+ public function destroySession() {
+ $this->accessToken = null;
+ $this->signedRequest = null;
+ $this->user = null;
+ $this->clearAllPersistentData();
+
+ // Javascript sets a cookie that will be used in getSignedRequest that we
+ // need to clear if we can
+ $cookie_name = $this->getSignedRequestCookieName();
+ if (array_key_exists($cookie_name, $_COOKIE)) {
+ unset($_COOKIE[$cookie_name]);
+ if (!headers_sent()) {
+ // The base domain is stored in the metadata cookie if not we fallback
+ // to the current hostname
+ $base_domain = '.'. $_SERVER['HTTP_HOST'];
+
+ $metadata = $this->getMetadataCookie();
+ if (array_key_exists('base_domain', $metadata) &&
+ !empty($metadata['base_domain'])) {
+ $base_domain = $metadata['base_domain'];
+ }
+
+ setcookie($cookie_name, '', 0, '/', $base_domain);
+ } else {
+ self::errorLog(
+ 'There exists a cookie that we wanted to clear that we couldn\'t '.
+ 'clear because headers was already sent. Make sure to do the first '.
+ 'API call before outputing anything'
+ );
+ }
+ }
+ }
+
+ /**
+ * Parses the metadata cookie that our Javascript API set
+ *
+ * @return an array mapping key to value
+ */
+ protected function getMetadataCookie() {
+ $cookie_name = $this->getMetadataCookieName();
+ if (!array_key_exists($cookie_name, $_COOKIE)) {
+ return array();
+ }
+
+ // The cookie value can be wrapped in "-characters so remove them
+ $cookie_value = trim($_COOKIE[$cookie_name], '"');
+
+ if (empty($cookie_value)) {
+ return array();
+ }
+
+ $parts = explode('&', $cookie_value);
+ $metadata = array();
+ foreach ($parts as $part) {
+ $pair = explode('=', $part, 2);
+ if (!empty($pair[0])) {
+ $metadata[urldecode($pair[0])] =
+ (count($pair) > 1) ? urldecode($pair[1]) : '';
+ }
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * Each of the following four methods should be overridden in
+ * a concrete subclass, as they are in the provided Facebook class.
+ * The Facebook class uses PHP sessions to provide a primitive
+ * persistent store, but another subclass--one that you implement--
+ * might use a database, memcache, or an in-memory cache.
+ *
+ * @see Facebook
+ */
+
+ /**
+ * Stores the given ($key, $value) pair, so that future calls to
+ * getPersistentData($key) return $value. This call may be in another request.
+ *
+ * @param string $key
+ * @param array $value
+ *
+ * @return void
+ */
+ abstract protected function setPersistentData($key, $value);
+
+ /**
+ * Get the data for $key, persisted by BaseFacebook::setPersistentData()
+ *
+ * @param string $key The key of the data to retrieve
+ * @param boolean $default The default value to return if $key is not found
+ *
+ * @return mixed
+ */
+ abstract protected function getPersistentData($key, $default = false);
+
+ /**
+ * Clear the data with $key from the persistent storage
+ *
+ * @param string $key
+ * @return void
+ */
+ abstract protected function clearPersistentData($key);
+
+ /**
+ * Clear all data from the persistent storage
+ *
+ * @return void
+ */
+ abstract protected function clearAllPersistentData();
+}


diff -r 94923e55b0289a3f58cbde5395acf07989f112eb -r a097368a5f0cf9eb4170c3e711706c4f50552044 inc/lib/facebook/facebook.php
--- /dev/null
+++ b/inc/lib/facebook/facebook.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+require_once "base_facebook.php";
+
+/**
+ * Extends the BaseFacebook class with the intent of using
+ * PHP sessions to store user ids and access tokens.
+ */
+class Facebook extends BaseFacebook
+{
+ /**
+ * Identical to the parent constructor, except that
+ * we start a PHP session to store the user ID and
+ * access token if during the course of execution
+ * we discover them.
+ *
+ * @param Array $config the application configuration.
+ * @see BaseFacebook::__construct in facebook.php
+ */
+ public function __construct($config) {
+ if (!session_id()) {
+ session_start();
+ }
+ parent::__construct($config);
+ }
+
+ protected static $kSupportedKeys =
+ array('state', 'code', 'access_token', 'user_id');
+
+ /**
+ * Provides the implementations of the inherited abstract
+ * methods. The implementation uses PHP sessions to maintain
+ * a store for authorization codes, user ids, CSRF states, and
+ * access tokens.
+ */
+ protected function setPersistentData($key, $value) {
+ if (!in_array($key, self::$kSupportedKeys)) {
+ self::errorLog('Unsupported key passed to setPersistentData.');
+ return;
+ }
+
+ $session_var_name = $this->constructSessionVariableName($key);
+ $_SESSION[$session_var_name] = $value;
+ }
+
+ protected function getPersistentData($key, $default = false) {
+ if (!in_array($key, self::$kSupportedKeys)) {
+ self::errorLog('Unsupported key passed to getPersistentData.');
+ return $default;
+ }
+
+ $session_var_name = $this->constructSessionVariableName($key);
+ return isset($_SESSION[$session_var_name]) ?
+ $_SESSION[$session_var_name] : $default;
+ }
+
+ protected function clearPersistentData($key) {
+ if (!in_array($key, self::$kSupportedKeys)) {
+ self::errorLog('Unsupported key passed to clearPersistentData.');
+ return;
+ }
+
+ $session_var_name = $this->constructSessionVariableName($key);
+ unset($_SESSION[$session_var_name]);
+ }
+
+ protected function clearAllPersistentData() {
+ foreach (self::$kSupportedKeys as $key) {
+ $this->clearPersistentData($key);
+ }
+ }
+
+ protected function constructSessionVariableName($key) {
+ return implode('_', array('fb',
+ $this->getAppId(),
+ $key));
+ }
+}


diff -r 94923e55b0289a3f58cbde5395acf07989f112eb -r a097368a5f0cf9eb4170c3e711706c4f50552044 inc/lib/facebook/fb_ca_chain_bundle.crt
--- /dev/null
+++ b/inc/lib/facebook/fb_ca_chain_bundle.crt
@@ -0,0 +1,121 @@
+-----BEGIN CERTIFICATE-----
+MIIFgjCCBGqgAwIBAgIQDKKbZcnESGaLDuEaVk6fQjANBgkqhkiG9w0BAQUFADBm
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSUwIwYDVQQDExxEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBDQS0zMB4XDTEwMDExMzAwMDAwMFoXDTEzMDQxMTIzNTk1OVowaDELMAkGA1UE
+BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEX
+MBUGA1UEChMORmFjZWJvb2ssIEluYy4xFzAVBgNVBAMUDiouZmFjZWJvb2suY29t
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9rzj7QIuLM3sdHu1HcI1VcR3g
+b5FExKNV646agxSle1aQ/sJev1mh/u91ynwqd2BQmM0brZ1Hc3QrfYyAaiGGgEkp
+xbhezyfeYhAyO0TKAYxPnm2cTjB5HICzk6xEIwFbA7SBJ2fSyW1CFhYZyo3tIBjj
+19VjKyBfpRaPkzLmRwIDAQABo4ICrDCCAqgwHwYDVR0jBBgwFoAUUOpzidsp+xCP
+nuUBINTeeZlIg/cwHQYDVR0OBBYEFPp+tsFBozkjrHlEnZ9J4cFj2eM0MA4GA1Ud
+DwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMF8GA1UdHwRYMFYwKaAnoCWGI2h0dHA6
+Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9jYTMtZmIuY3JsMCmgJ6AlhiNodHRwOi8vY3Js
+NC5kaWdpY2VydC5jb20vY2EzLWZiLmNybDCCAcYGA1UdIASCAb0wggG5MIIBtQYL
+YIZIAYb9bAEDAAEwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3LmRpZ2ljZXJ0
+LmNvbS9zc2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUHAgIwggFWHoIB
+UgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkA
+YwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEA
+bgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMA
+UABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkA
+IABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwA
+aQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8A
+cgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMA
+ZQAuMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQUF
+AAOCAQEACOkTIdxMy11+CKrbGNLBSg5xHaTvu/v1wbyn3dO/mf68pPfJnX6ShPYy
+4XM4Vk0x4uaFaU4wAGke+nCKGi5dyg0Esg7nemLNKEJaFAJZ9enxZm334lSCeARy
+wlDtxULGOFRyGIZZPmbV2eNq5xdU/g3IuBEhL722mTpAye9FU/J8Wsnw54/gANyO
+Gzkewigua8ip8Lbs9Cht399yAfbfhUP1DrAm/xEcnHrzPr3cdCtOyJaM6SRPpRqH
+ITK5Nc06tat9lXVosSinT3KqydzxBYua9gCFFiR3x3DgZfvXkC6KDdUlDrNcJUub
+a1BHnLLP4mxTHL6faAXYd05IxNn/IA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIGVTCCBT2gAwIBAgIQCFH5WYFBRcq94CTiEsnCDjANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA3MDQwMzAwMDAwMFoXDTIyMDQwMzAwMDAwMFowZjEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTElMCMGA1UEAxMcRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+Q0EtMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9hCikQH17+NDdR
+CPge+yLtYb4LDXBMUGMmdRW5QYiXtvCgFbsIYOBC6AUpEIc2iihlqO8xB3RtNpcv
+KEZmBMcqeSZ6mdWOw21PoF6tvD2Rwll7XjZswFPPAAgyPhBkWBATaccM7pxCUQD5
+BUTuJM56H+2MEb0SqPMV9Bx6MWkBG6fmXcCabH4JnudSREoQOiPkm7YDr6ictFuf
+1EutkozOtREqqjcYjbTCuNhcBoz4/yO9NV7UfD5+gw6RlgWYw7If48hl66l7XaAs
+zPw82W3tzPpLQ4zJ1LilYRyyQLYoEt+5+F/+07LJ7z20Hkt8HEyZNp496+ynaF4d
+32duXvsCAwEAAaOCAvcwggLzMA4GA1UdDwEB/wQEAwIBhjCCAcYGA1UdIASCAb0w
+ggG5MIIBtQYLYIZIAYb9bAEDAAIwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3
+LmRpZ2ljZXJ0LmNvbS9zc2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUH
+AgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQBy
+AHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBj
+AGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAg
+AEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQ
+AGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBt
+AGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBj
+AG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBl
+AHIAZQBuAGMAZQAuMA8GA1UdEwEB/wQFMAMBAf8wNAYIKwYBBQUHAQEEKDAmMCQG
+CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wgY8GA1UdHwSBhzCB
+hDBAoD6gPIY6aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0SGlnaEFz
+c3VyYW5jZUVWUm9vdENBLmNybDBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQu
+Y29tL0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDAfBgNVHSMEGDAW
+gBSxPsNpA/i/RwHUmCYaCALvY2QrwzAdBgNVHQ4EFgQUUOpzidsp+xCPnuUBINTe
+eZlIg/cwDQYJKoZIhvcNAQEFBQADggEBAF1PhPGoiNOjsrycbeUpSXfh59bcqdg1
+rslx3OXb3J0kIZCmz7cBHJvUV5eR13UWpRLXuT0uiT05aYrWNTf58SHEW0CtWakv
+XzoAKUMncQPkvTAyVab+hA4LmzgZLEN8rEO/dTHlIxxFVbdpCJG1z9fVsV7un5Tk
+1nq5GMO41lJjHBC6iy9tXcwFOPRWBW3vnuzoYTYMFEuFFFoMg08iXFnLjIpx2vrF
+EIRYzwfu45DC9fkpx1ojcflZtGQriLCnNseaIGHr+k61rmsb5OPs4tk8QUmoIKRU
+9ZKNu8BVIASm2LAXFszj0Mi0PeXZhMbT9m5teMl5Q+h6N/9cNUm/ocU=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEQjCCA6ugAwIBAgIEQoclDjANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC
+VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u
+ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc
+KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u
+ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEy
+MjIxNTI3MjdaFw0xNDA3MjIxNTU3MjdaMGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
+EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xKzApBgNV
+BAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0EwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGzOVz5vvUu+UtLTKm3+WBP8nNJUm2cSrD
+1ZQ0Z6IKHLBfaaZAscS3so/QmKSpQVk609yU1jzbdDikSsxNJYL3SqVTEjju80lt
+cZF+Y7arpl/DpIT4T2JRvvjF7Ns4kuMG5QiRDMQoQVX7y1qJFX5x6DW/TXIJPb46
+OFBbdzEbjbPHJEWap6xtABRaBLe6E+tRCphBQSJOZWGHgUFQpnlcid4ZSlfVLuZd
+HFMsfpjNGgYWpGhz0DQEE1yhcdNafFXbXmThN4cwVgTlEbQpgBLxeTmIogIRfCdm
+t4i3ePLKCqg4qwpkwr9mXZWEwaElHoddGlALIBLMQbtuC1E4uEvLAgMBAAGjggET
+MIIBDzASBgNVHRMBAf8ECDAGAQH/AgEBMCcGA1UdJQQgMB4GCCsGAQUFBwMBBggr
+BgEFBQcDAgYIKwYBBQUHAwQwMwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzABhhdo
+dHRwOi8vb2NzcC5lbnRydXN0Lm5ldDAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8v
+Y3JsLmVudHJ1c3QubmV0L3NlcnZlcjEuY3JsMB0GA1UdDgQWBBSxPsNpA/i/RwHU
+mCYaCALvY2QrwzALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7
+UISX8+1i0BowGQYJKoZIhvZ9B0EABAwwChsEVjcuMQMCAIEwDQYJKoZIhvcNAQEF
+BQADgYEAUuVY7HCc/9EvhaYzC1rAIo348LtGIiMduEl5Xa24G8tmJnDioD2GU06r
+1kjLX/ktCdpdBgXadbjtdrZXTP59uN0AXlsdaTiFufsqVLPvkp5yMnqnuI3E2o6p
+NpAkoQSbB6kUCNnXcW26valgOjDLZFOnr241QiwdBAJAAE/rRa8=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC
+VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u
+ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc
+KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u
+ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1
+MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE
+ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j
+b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF
+bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg
+U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA
+A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/
+I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3
+wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC
+AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb
+oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5
+BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p
+dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk
+MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp
+b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu
+dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0
+MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi
+E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa
+MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI
+hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN
+95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd
+2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI=
+-----END CERTIFICATE-----


diff -r 94923e55b0289a3f58cbde5395acf07989f112eb -r a097368a5f0cf9eb4170c3e711706c4f50552044 web/auth/facebook.php
--- /dev/null
+++ b/web/auth/facebook.php
@@ -0,0 +1,13 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+
+/* we exist solely to redirect back to the main app; we need this
+ * because the oauth redirect back to us passes GET parameters that
+ * we don't care about, but that the facebook auth stuff consumes */
+
+include '../../inc/common.php';
+
+MTrackAuth::whoami();
+
+header('Location: ' . $ABSWEB);
+



https://bitbucket.org/wez/mtrack/changeset/5616c8c6e4d3/
changeset: 5616c8c6e4d3
user: wez
date: 2012-04-29 00:34:58
summary: add mtrack-native authentication
affected #: 7 files

diff -r a097368a5f0cf9eb4170c3e711706c4f50552044 -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 inc/auth.php
--- a/inc/auth.php
+++ b/inc/auth.php
@@ -1,6 +1,7 @@
<?php # vim:ts=2:sw=2:et:
/* For licensing and copyright terms, see the file named LICENSE */

+include_once MTRACK_INC_DIR . '/auth/mtrack.php';
include_once MTRACK_INC_DIR . '/auth/anon.php';
include_once MTRACK_INC_DIR . '/auth/http.php';
include_once MTRACK_INC_DIR . '/auth/openid.php';


diff -r a097368a5f0cf9eb4170c3e711706c4f50552044 -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 inc/auth/mtrack.php
--- /dev/null
+++ b/inc/auth/mtrack.php
@@ -0,0 +1,73 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+
+/* mtrack locally maintained authentication */
+
+class MTrackAuth_MTrack implements IMTrackAuth, IMTrackNavigationHelper {
+ function __construct() {
+ MTrackAuth::registerMech($this);
+ MTrackNavigation::registerHelper($this);
+ }
+
+ function augmentUserInfo(&$content) {
+ if (!$this->authenticate()) {
+ $content = "<a href='$GLOBALS[ABSWEB]auth/mtrack.php'>Log In</a>";
+ }
+ }
+
+ function augmentNavigation($id, &$items) {
+ }
+
+ function authenticate() {
+ if (!strlen(session_id()) && php_sapi_name() != 'cli') {
+ session_start();
+ }
+
+ if (isset($_SESSION['auth.mtrack'])) {
+ return $_SESSION['auth.mtrack'];
+ }
+ return null;
+ }
+
+ function doAuthenticate($force = false) {
+ if ($force) {
+ header("Location: $GLOBALS[ABSWEB]auth/mtrack.php");
+ exit;
+ }
+ return null;
+ }
+
+ function enumGroups() {
+ return null;
+ }
+
+ function getGroups($username) {
+ return null;
+ }
+
+ function addToGroup($username, $groupname)
+ {
+ return null;
+ }
+
+ function removeFromGroup($username, $groupname)
+ {
+ return null;
+ }
+
+ function getUserData($username) {
+ return null;
+ }
+
+ function canLogOut() {
+ return true;
+ }
+
+ function LogOut() {
+ session_destroy();
+ header('Location: ' . $GLOBALS['ABSWEB']);
+ exit;
+ }
+
+}
+


diff -r a097368a5f0cf9eb4170c3e711706c4f50552044 -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 inc/user.php
--- a/inc/user.php
+++ b/inc/user.php
@@ -9,14 +9,15 @@
public $email;
public $timezone;
public $active;
- public $sshkeys;
+ public $sshkeys = null;
public $aliases = null;
+ public $prefs = null;
private $stored = false;

static function loadUser($username) {
$username = mtrack_canon_username($username);

- $data = MTrackDB::q('select * from userinfo where userid = ?', $username)
+ $data = MTrackDB::q('select userid, fullname, email, timezone, active, sshkeys, prefs from userinfo where userid = ?', $username)
->fetchAll(PDO::FETCH_ASSOC);
if (!isset($data[0])) {
$udata = MTrackAuth::getUserData($username);
@@ -45,6 +46,12 @@
$user->timezone = $data['timezone'];
$user->active = $data['active'];
$user->sshkeys = $data['sshkeys'];
+ $user->prefs = $data['prefs'];
+ if ($user->prefs) {
+ $user->prefs = json_decode($user->prefs);
+ } else {
+ $user->prefs = new stdclass;
+ }

$user->aliases = MTrackDB::q(<<<SQL
select alias from useraliases where userid = ? order by alias
@@ -56,22 +63,24 @@

function save(MTrackChangeset $CS) {
if ($this->stored) {
- MTrackDB::q('update userinfo set fullname = ?, email = ?, timezone = ?, active = ?, sshkeys = ? where userid = ?',
+ MTrackDB::q('update userinfo set fullname = ?, email = ?, timezone = ?, active = ?, sshkeys = ?, prefs = ? where userid = ?',
$this->fullname,
$this->email,
$this->timezone,
$this->active ? 1 : 0,
$this->sshkeys,
+ json_encode($this->prefs),
$this->userid
);
} else {
- MTrackDB::q('insert into userinfo (active, fullname, email, timezone, sshkeys, userid) values (?, ?, ?, ?, ?, ?)',
+ MTrackDB::q('insert into userinfo (active, fullname, email, timezone, sshkeys, userid, prefs) values (?, ?, ?, ?, ?, ?, ?)',
$this->active ? 1 : 0,
$this->fullname,
$this->email,
$this->timezone,
$this->sshkeys,
- $this->userid
+ $this->userid,
+ json_encode($this->prefs)
);
$this->stored = true;
}
@@ -147,6 +156,9 @@
$u->role = MTrackAuth::getUserClass($u->id);
$u->groups = array_values(MTrackAuth::getGroups($this->userid));

+ /* ensure that we don't ever return the hash */
+ unset($u->pwhash);
+
return $u;
}

@@ -164,8 +176,8 @@
}
if ($method == 'PUT') {
$in = MTrackAPI::getPayload();
- foreach (array('fullname', 'email', 'timezone', 'timezone', 'active')
- as $prop) {
+ foreach (array('fullname', 'email', 'timezone', 'timezone',
+ 'active', 'prefs') as $prop) {
if (!isset($in->$prop)) continue;
$user->$prop = $in->$prop;
}
@@ -187,13 +199,77 @@
return $user->rest_return_user();
}

+ /** updates the stored password associated with the user.
+ * We use SSHA512 for this */
+ function setPassword($password) {
+ /* make a salt */
+ $salt = '';
+ if (function_exists('openssl_random_psuedo_bytes')) {
+ $salt = openssl_random_psuedo_bytes(4);
+ } else {
+ for ($i = 0; $i < 4; $i++) {
+ $salt .= chr(mt_rand(0, 255));
+ }
+ }
+ $digest = hash('sha512', $password . $salt, true);
+ $pwhash = '{SSHA512}' . base64_encode($digest . $salt);
+
+ MTrackDB::q('update userinfo set pwhash = ? where userid = ?',
+ $pwhash, $this->userid);
+ }
+
+ /** verifies the provided password against the stored credential
+ * information, returning true if the password matches */
+ function verifyPassword($password)
+ {
+ foreach (MTrackDB::q('select pwhash from userinfo where userid = ?',
+ $this->userid)->fetchAll(PDO::FETCH_COLUMN, 0) as $pwhash)
+ {
+ if (preg_match('/^\{([A-Z0-9]+)\}(.*)$/', $pwhash, $M)) {
+ $mech = $M[1];
+ $hash = $M[2];
+
+ error_log("Loaded mech=$mech hash=$hash");
+
+ switch ($mech) {
+ case 'SSHA512':
+ $d = base64_decode($hash);
+ $salt = substr($d, 64);
+ $hash = substr($d, 0, 64);
+ return hash('sha512', $password . $salt, true) == $hash;
+ }
+ }
+ }
+ error_log("no entry for $this->userid ??");
+ return false;
+ }
+
+ /** computes a new MD5 hash for the user based on the provided password */
static function rest_password($method, $uri, $captures) {
MTrackAPI::checkAllowed($method, 'POST');
self::rest_perm_check($method, $captures['user']);

+ $in = MTrackAPI::getPayload();
+
+ $user = self::loadUser($captures['user']);
+ if ($user === null) {
+ MTrackACL::requireAllRights('User', 'create');
+ $user = new MTrackUser();
+ $user->userid = $captures['user'];
+ $CS = MTrackChangeset::begin("user:$user->userid",
+ "create and set password");
+ $user->save($CS);
+ $CS->commit();
+ }
+
+ $local_auth = MTrackAuth::getMech('MTrackAuth_MTrack');
+ if ($local_auth) {
+ $user->setPassword($in->password);
+ return;
+ }
+
$http_auth = MTrackAuth::getMech('MTrackAuth_HTTP');
if ($http_auth && !isset($_SERVER['REMOTE_USER'])) {
- $in = MTrackAPI::getPayload();
$http_auth->setUserPassword($captures['user'], $in->password);
return;
}


diff -r a097368a5f0cf9eb4170c3e711706c4f50552044 -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 schema/12.xml
--- /dev/null
+++ b/schema/12.xml
@@ -0,0 +1,570 @@
+<schema version='12'>
+ <table name='projects'>
+ <field name='projid' type='autoinc'/>
+ <field name='ordinal' type='integer' nullable='0' default='5'>
+ <comment>
+ used to order the project names
+ </comment>
+ </field>
+ <field name='name' type='text' nullable='0'>
+ <comment>
+ readable version of the name
+ </comment>
+ </field>
+ <field name='shortname' type='varchar(16)' nullable='0'>
+ <comment>
+ shorter name
+ </comment>
+ </field>
+ <field name='notifyemail' type='varchar(320)'>
+ <comment>
+ where email notifications are sent
+ </comment>
+ </field>
+ <key>
+ <field>projid</field>
+ </key>
+ <key type='unique'>
+ <field>name</field>
+ </key>
+ <key type='unique'>
+ <field>shortname</field>
+ </key>
+ </table>
+
+ <table name='groups'>
+ <field name='name' type='text' nullable='0'/>
+ <field name='project' type='integer' nullable='0'
+ reftable='projects' refcol='projid'/>
+ <key>
+ <field>name</field>
+ <field>project</field>
+ </key>
+ </table>
+
+ <table name='group_membership'>
+ <field name='groupname' type='text' nullable='0'
+ reftable='groups' refcol='name'/>
+ <field name='project' type='integer' nullable='0'
+ reftable='projects' refcol='projid'/>
+ <field name='username' type='text' nullable='0'
+ reftable='userinfo' refcol='userid'/>
+ <key>
+ <field>groupname</field>
+ <field>project</field>
+ <field>username</field>
+ </key>
+ </table>
+
+ <table name='repos'>
+ <field name='repoid' type='autoinc'/>
+ <field name='shortname' type='varchar(16)' nullable='0'/>
+ <field name='scmtype' type='varchar(32)' nullable='0'/>
+ <field name='repopath' type='text' nullable='0'/>
+ <field name='browserurl' type='text'>
+ <comment>
+ if defined, mtrack will use this as the base for links
+ to changesets and repo browsing, otherwise it will
+ handle it locally
+ </comment>
+ </field>
+ <field name='browsertype' type='text'/>
+ <field name='description' type='text'/>
+ <field name='serverurl' type='text'>
+ <comment>
+ The URL that SCM tools will use to checkout,
+ clone, push, pull or otherwise interact with
+ the repo.
+ </comment>
+ </field>
+ <field name='parent' type='text' nullable='0' default=''>
+ <comment>
+ If NULL, this is a global repo. Otherwise, parent is
+ a string like 'user:wez' to indicate that it is owned
+ by 'wez', or 'project:name' to indicate that it is owned
+ by the 'name' project.
+ </comment>
+ </field>
+ <field name='clonedfrom' type='integer'
+ reftable='repos' refcol='repoid'>
+ <comment>
+ If this was forked from another repo in the system,
+ then this field is set to its repoid
+ </comment>
+ </field>
+ <key>
+ <field>repoid</field>
+ </key>
+ <key type='unique'>
+ <field>shortname</field>
+ <field>parent</field>
+ </key>
+ </table>
+
+ <table name='project_repo_link'>
+ <comment>
+Links a location within a repo to its "parent" project.
+This allows multiple projects to exist within a repository
+and also allows pre/post commit rules to determine whether
+the location is a personal branch or scratch space, versus
+a formal project branch.
+ </comment>
+ <field name='linkid' type='autoinc'/>
+ <field name='projid' type='integer' reftable='projects' refcol='projid'
+ nullable='0'/>
+ <field name='repoid' type='integer' reftable='repos' refcol='repoid'
+ nullable='0'/>
+ <field name='repopathregex' type='text'/>
+ <field name='is_scratch_space' type='integer' nullable='0' default='0'>
+ <comment>
+ May replace this with a reference to a workflow or other kind
+ of ruleset to affect pre/post commit
+ </comment>
+ </field>
+ <key>
+ <field>linkid</field>
+ </key>
+ </table>
+
+ <table name='components'>
+ <field name='compid' type='autoinc'/>
+ <field name='deleted' type='integer' nullable='0' default='0'/>
+ <field name='name' type='text'/>
+ <key>
+ <field>compid</field>
+ </key>
+ <key type='unique'>
+ <field>name</field>
+ </key>
+ </table>
+
+ <table name='components_by_project'>
+ <field name='projid' type='integer'/>
+ <field name='compid' type='integer'
+ reftable='components' refcol='compid' nullable='0'/>
+ <key><field>projid</field><field>compid</field></key>
+ </table>
+
+ <table name='priorities'>
+ <field name='priorityname' type='varchar(32)' nullable='0'/>
+ <field name='deleted' type='integer' nullable='0' default='0'/>
+ <field name='value' type='integer' nullable='0' default='5'/>
+ <key><field>priorityname</field></key>
+ </table>
+
+ <table name='severities'>
+ <field name='sevname' type='varchar(32)' nullable='0'/>
+ <key><field>sevname</field></key>
+ <field name='deleted' type='integer' nullable='0' default='0'/>
+ <field name='ordinal' type='integer' nullable='0' default='5'/>
+ </table>
+
+ <table name='resolutions'>
+ <field name='resname' type='varchar(32)' nullable='0'/>
+ <key><field>resname</field></key>
+ <field name='deleted' type='integer' nullable='0' default='0'/>
+ <field name='ordinal' type='integer' nullable='0' default='5'/>
+ </table>
+ <table name='classifications'>
+ <field name='classname' type='varchar(32)' nullable='0'/>
+ <key><field>classname</field></key>
+ <field name='deleted' type='integer' nullable='0' default='0'/>
+ <field name='ordinal' type='integer' nullable='0' default='5'/>
+ </table>
+ <table name='ticketstates'>
+ <field name='statename' type='varchar(32)' nullable='0'/>
+ <key><field>statename</field></key>
+ <field name='deleted' type='integer' nullable='0' default='0'/>
+ <field name='ordinal' type='integer' nullable='0' default='5'/>
+ </table>
+ <table name='keywords'>
+ <field name='kid' type='autoinc'/>
+ <key><field>kid</field></key>
+ <key type='unique'><field>keyword</field></key>
+ <field name='keyword' type='text' nullable='0'/>
+ </table>
+ <table name='changes'>
+ <field name='cid' type='autoinc'/>
+ <field name='who' type='text'/>
+ <field name='object' type='text'>
+ <comment>
+ usually tablename:id
+ where id is a comma separated list of the primary key fields
+ of the object that was edited
+ </comment>
+ </field>
+ <field name='changedate' type='timestamp' nullable='0'
+ default='CURRENT_TIMESTAMP'/>
+ <field name='reason' type='text'>
+ <comment>
+ commit/changelog message
+ </comment>
+ </field>
+ <key><field>cid</field></key>
+ <key type='multiple' name='idx_changes_object'><field>object</field></key>
+ <key type='multiple' name='idx_changes_date'><field>changedate</field></key>
+ </table>
+
+ <table name='change_audit'>
+ <field name='cid' type='integer' nullable='0'
+ reftable='changes' refcol='cid'/>
+ <field name='fieldname' type='text'/>
+ <field name='action' type='varchar(16)'>
+ <comment>
+ set, changed, deleted, added, removed.
+ set: filled in from a blank value
+ changed: changed existing value. value field has old value.
+ deleted: set value to blank, value field has old value
+ added: used for associated values (like keywords); the value field
+ lists out the primary keys of the added items, comma separated.
+ removed: used for associated values (like keywords); the value field
+ lists out the primary keys of the removed items, comma separated
+ </comment>
+ </field>
+ <field name='oldvalue' type='text'/>
+ <field name='value' type='text'/>
+ </table>
+
+ <table name='milestones'>
+ <field name='mid' type='autoinc'/>
+ <key><field>mid</field></key>
+ <field name='name' type='text'/>
+ <key type='unique'>
+ <field>name</field>
+ </key>
+ <field name='description' type='text'/>
+ <field name='startdate' type='timestamp'/>
+ <field name='duedate' type='timestamp'/>
+ <field name='completed' type='timestamp'/>
+ <field name='deleted' type='integer' nullable='0' default='0'/>
+ <field name='created' type='integer' nullable='0'
+ reftable='changes' refcol='cid'/>
+ <field name='updated' type='integer' nullable='0'
+ reftable='changes' refcol='cid'/>
+ <field name='pmid' type='integer' reftable='milestones' refcol='mid'>
+ <comment>
+ parent milestone (for sprint support)
+ </comment>
+ </field>
+ </table>
+
+ <table name='tickets'>
+ <field name='tid' type='char(32)' nullable='0'>
+ <comment>unique identifier (short form UUID)</comment>
+ </field>
+ <field name='nsident' type='text' nullable='0'>
+ <comment>
+ identifier assigned within a particular namespace
+ eg: when a ticket is accepted as a bug, will be assigned
+ a bug number for that project
+ </comment>
+ </field>
+
+ <field name='summary' type='text' nullable='0'>
+ <comment>
+ -- one line summary
+ -- problem description in detail
+ </comment>
+ </field>
+ <field name='description' type='text'/>
+
+ <field name='changelog' type='text'>
+ <comment>
+ -- end-user (or customer) facing summary, suitable for use in
+ -- a release notes or ChangeLog format
+ </comment>
+ </field>
+
+ <field name='created' type='integer'
+ nullable='0' reftable='changes' refcol='cid'/>
+ <field name='updated' type='integer'
+ nullable='0' reftable='changes' refcol='cid'/>
+
+ <field name='owner' type='text'/>
+ <field name='priority' type='text'/>
+ <field name='severity' type='text'/>
+ <field name='classification' type='text'/>
+ <field name='resolution' type='text'/>
+ <field name='cc' type='text'/>
+
+ <field name='status' type='text' nullable='0'/>
+ <field name='estimated' type='real'/>
+ <field name='spent' type='real'/>
+
+ <field name='ptid' type='char(32)' reftable='tickets' refcol='tid'>
+ <comment>
+ parent ticket
+ </comment>
+ </field>
+
+ <key><field>tid</field></key>
+ <key type='unique'><field>nsident</field></key>
+ </table>
+
+ <table name='ticket_deps'>
+ <field name='tid' type='char(32)' nullable='0'
+ reftable='tickets' refcol='tid'/>
+ <field name='depends_on' type='char(32)' nullable='0'
+ reftable='tickets' refcol='tid'/>
+ <field name='rel' type='text' nullable='0'
+ default="'depends'"/>
+ <key>
+ <field>tid</field>
+ <field>depends_on</field>
+ </key>
+ </table>
+
+ <table name='ticket_components'>
+ <field name='tid' type='char(32)' nullable='0'
+ reftable='tickets' refcol='tid'/>
+ <field name='compid' type='integer' nullable='0'
+ reftable='components' refcol='cid'/>
+ </table>
+
+ <table name='ticket_milestones'>
+ <field name='tid' type='char(32)' nullable='0'
+ reftable='tickets' refcol='tid'/>
+ <field name='mid' type='integer' nullable='0'
+ reftable='milestones' refcol='mid'/>
+ <field name='pri_ord' type='integer' nullable='0'
+ default='0'>
+ <comment>
+ ordinal priority for fine tuning relative ordering
+ of tickets
+ </comment>
+ </field>
+
+ <key>
+ <field>tid</field>
+ <field>mid</field>
+ </key>
+ </table>
+
+ <table name='ticket_keywords'>
+ <field name='tid' type='char(32)' nullable='0'
+ reftable='tickets' refcol='tid'/>
+ <field name='kid' type='integer' nullable='0'
+ reftable='keywords' refcol='kid'/>
+ </table>
+
+ <table name='ticket_changeset_hashes'>
+ <field name='tid' type='char(32)' nullable='0'
+ reftable='tickets' refcol='tid'/>
+ <field name='hash' type='text'>
+ <comment>
+ For distributed version control, we may push the same
+ changes into multiple repos that we maintain in the same
+ mtrack instance. We don't want to count any spent time
+ more than once, so we allow storing a hash with each
+ ticket.
+ </comment>
+ </field>
+ <key>
+ <field>tid</field>
+ <field>hash</field>
+ </key>
+ </table>
+
+ <table name='reports'>
+ <field name='rid' type='autoinc'/>
+ <field name='summary' type='text' nullable='0'/>
+ <field name='description' type='text' nullable='0'/>
+ <field name='query' type='text' nullable='0'/>
+ <field name='changed' type='integer'
+ nullable='0' reftable='changes' refcol='cid'/>
+ <key><field>rid</field></key>
+ <key type='unique'><field>summary</field></key>
+ </table>
+
+ <table name='effort'>
+ <field name='eid' type='autoinc'/>
+ <key><field>eid</field></key>
+ <field name='tid' type='char(32)' nullable='0'/>
+ <field name='cid' type='integer'
+ nullable='0' reftable='changes' refcol='cid'/>
+ <field name='expended' type='real'/>
+ <field name='remaining' type='real'>
+ <comment>revised estimate</comment>
+ </field>
+ <key type='multiple' name='idx_effort_ticket'><field>tid</field></key>
+ </table>
+
+ <table name='acl'>
+ <comment>access control list</comment>
+ <field name='objectid' type='text'/>
+ <field name='cascade' type='integer' nullable='0'>
+ <comment>
+ indicates whether the entry applies to this item or its children
+ sequence number allows explicit ordering for fine grained
+ permissions (exclude all members of a group, except a particular user)
+ </comment>
+ </field>
+ <field name='seq' type='integer' nullable='0'/>
+ <field name='role' type='text' nullable='0'>
+ <comment>user or group name</comment>
+ </field>
+ <field name='action' type='text' nullable='0'>
+ <comment>
+ -- activity or action name ("read", "write")
+ -- whether access is allowed
+ </comment>
+ </field>
+ <field name='allow' type='integer' nullable='0'/>
+ <key>
+ <field>objectid</field>
+ <field>seq</field>
+ <field>cascade</field>
+ </key>
+ <key type='multiple' name='idx_acl_role'>
+ <field>role</field>
+ </key>
+ </table>
+
+ <table name='userinfo'>
+ <field name='userid' type='text' nullable='0'>
+ <comment>canonical user id</comment>
+ </field>
+ <key><field>userid</field></key>
+ <field name='fullname' type='text'/>
+ <field name='email' type='text'/>
+ <field name='timezone' type='text'/>
+ <field name='active' type='integer' nullable='0' default='1'/>
+ <field name='sshkeys' type='text'>
+ <comment>
+ Equivalent to the contents of an authorized_keys file
+ </comment>
+ </field>
+ <field name='prefs' type='text'>
+ <comment>
+ A JSON blob holding user specified preferences.
+ This is intended to only be accessed by the logged-in
+ user and not queryable outside of their session,
+ so a JSON blob is a fair choice for this
+ </comment>
+ </field>
+ <field name='pwhash' type='text'>
+ <comment>
+ The password hash for this user
+ </comment>
+ </field>
+ </table>
+
+ <table name='useraliases'>
+ <field name='alias' type='text' nullable='0'/>
+ <key><field>alias</field></key>
+ <field name='userid' type='text' reftable='userinfo' refcol='userid'/>
+ </table>
+
+ <table name='attachments'>
+ <field name='object' type='text' nullable='0'>
+ <comment>
+ the object to which this is attached
+ sha1 hash of the contents of the attachment
+ </comment>
+ </field>
+ <field name='hash' type='text' nullable='0'/>
+ <field name='filename' type='text' nullable='0'/>
+ <field name='size' type='integer' nullable='0'/>
+ <field name='cid' type='integer'
+ nullable='0' reftable='changes' refcol='cid'/>
+ <field name='payload' type='blob'/>
+ </table>
+
+ <table name='last_notification'>
+ <comment>last time that we procesed change notifications</comment>
+ <field name='last_run' type='timestamp' nullable='0'/>
+ <key><field>last_run</field></key>
+ </table>
+ <table name='search_engine_state'>
+ <field name='last_run' type='timestamp' nullable='0'/>
+ <key><field>last_run</field></key>
+ </table>
+
+ <table name='snippets'>
+ <field name='snid' type='text' nullable='0'>
+ <comment>snippet id</comment>
+ </field>
+ <field name='created' type='integer'
+ nullable='0' reftable='changes' refcol='cid'/>
+ <field name='updated' type='integer'
+ nullable='0' reftable='changes' refcol='cid'/>
+ <field name='description' type='text' nullable='0'>
+ <comment>summary/blurb in wiki markup</comment>
+ </field>
+ <field name='lang' type='text' nullable='0'>
+ <comment>what language?</comment>
+ </field>
+ <field name='snippet' type='text' nullable='0'>
+ <comment>and the snippet itself</comment>
+ </field>
+ <key><field>snid</field></key>
+ </table>
+
+ <table name='watches'>
+ <comment>Records things that are being watched by a given user</comment>
+ <field name='otype' type='text' nullable='0'>
+ <comment>
+ The type of object being watched: ticket, repo, user, project,
+ milestone, wiki
+ </comment>
+ </field>
+ <field name='oid' type='text' nullable='0'>
+ <comment>
+ The id of the object being watched.
+ If '*', treated as a wildcard for objects of the specified
+ type.
+ </comment>
+ </field>
+ <field name='userid' type='text'
+ reftable='userinfo' refcol='userid' nullable='0'>
+ <comment>
+ The person doing the watching
+ </comment>
+ </field>
+ <field name='event' type='text' nullable='0'>
+ <comment>
+ all - interested in all events
+ tickets - ticket changes
+ changeset - repo changes
+ </comment>
+ </field>
+ <key>
+ <field>otype</field>
+ <field>oid</field>
+ <field>userid</field>
+ <field>event</field>
+ <field>medium</field>
+ </key>
+ <field name='medium' type='text' nullable='0'>
+ <comment>
+ email - receive via email
+ feed - visible in RSS feed
+ timeline - show up in timeline by default
+ </comment>
+ </field>
+ <field name='active' type='integer' nullable='0' default='1'/>
+ </table>
+
+
+ <post driver="pgsql">
+CREATE OR REPLACE FUNCTION _mtrack_group_concat(text, text)
+ RETURNS text as $$
+SELECT CASE
+ WHEN $2 IS NULL THEN $1
+ WHEN $1 IS NULL THEN $2
+ELSE
+ $1 operator(pg_catalog.||) ',' operator(pg_catalog.||) $2
+END
+$$ IMMUTABLE LANGUAGE SQL;
+
+-- requires postgres 8.2 and higher
+DROP AGGREGATE IF EXISTS mtrack_group_concat(text);
+
+CREATE AGGREGATE mtrack_group_concat(
+ BASETYPE = text,
+ SFUNC = _mtrack_group_concat,
+ STYPE = text
+);
+ </post>
+
+</schema>


diff -r a097368a5f0cf9eb4170c3e711706c4f50552044 -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 t/rest/user.php
--- a/t/rest/user.php
+++ b/t/rest/user.php
@@ -28,6 +28,7 @@
'timezone' => null,
'active' => true,
'aliases' => array(),
+ 'prefs' => new stdclass,
'role' => 'admin',
'groups' => array(
"BrowserCreator",


diff -r a097368a5f0cf9eb4170c3e711706c4f50552044 -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 web/auth/mtrack.php
--- /dev/null
+++ b/web/auth/mtrack.php
@@ -0,0 +1,72 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+
+include '../../inc/common.php';
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (isset($_POST['userid']) && isset($_POST['password'])) {
+ $user = MTrackUser::loadUser($_POST['userid']);
+ if ($user) {
+ if ($user->verifyPassword($_POST['password'])) {
+ if (session_id()) {
+ session_regenerate_id(true);
+ } else {
+ session_start();
+ }
+ $_SESSION['auth.mtrack'] = $user->userid;
+ header("Location: $ABSWEB");
+ exit;
+ }
+ }
+ }
+ $fail = "Invalid username or password";
+}
+
+mtrack_head('Login');
+
+echo <<<HTML
+<h1>Log In</h1>
+
+<form method="POST">
+
+<table>
+ <tr>
+ <td>
+ <label for="userid">User Id</label>
+ </td>
+ <td>
+ <input type="text" name="userid" placeholder="Enter your user id">
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label for="password">Password</label>
+ </td>
+ <td>
+ <input type="password" name="password" placeholder="Enter your password">
+ </td>
+ </tr>
+</table>
+HTML;
+
+if ($fail) {
+ $fail = htmlentities($fail, ENT_QUOTES, 'utf-8');
+
+ echo <<<HTML
+<div class="alert alert-danger">
+ <a class="close" data-dismiss="alert">&times;</a>
+ $fail
+</div>
+HTML;
+}
+
+echo <<<HTML
+ <button type='submit' class='btn btn-primary'>Log In</button>
+
+</form>
+
+HTML;
+
+mtrack_foot();
+


diff -r a097368a5f0cf9eb4170c3e711706c4f50552044 -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 web/user.php
--- a/web/user.php
+++ b/web/user.php
@@ -46,9 +46,21 @@
}

$http_auth = MTrackAuth::getMech('MTrackAuth_HTTP');
-$pw_change = ($http_auth && !isset($_SERVER['REMOTE_USER'])) &&
- ($me === $user || MTrackACL::hasAnyRights('User', 'modify'));
+$local_auth = MTrackAuth::getMech('MTrackAuth_MTrack');
+
+/* show password change controls? */
+if ($me === $user || MTrackACL::hasAnyRights('User', 'modify')) {
+ if ($http_auth) {
+ $pw_change = ($http_auth && !isset($_SERVER['REMOTE_USER']));
+ } else if ($local_auth) {
+ $pw_change = true;
+ }
+} else {
+ $pw_change = false;
+}
+
$pw_change = json_encode($pw_change);
+
$privileged = json_encode(MTrackACL::hasAnyRights('User', 'modify'));

ob_start();



https://bitbucket.org/wez/mtrack/changeset/e9202c26f4e8/
changeset: e9202c26f4e8
user: wez
date: 2012-04-30 00:40:47
summary: HEADS UP: authentication changes in progress.

Updating to this revision has a couple of noteworthy items:

* admin_party will need to be explicitly turned off via the UI if
you have not already configured it to false in your main config.ini

* while admin_party is enabled, all accesses from the party addresses
are treated as admin, and all other accesses can see no data (they'll
see a note about admin party being enabled, but can't see wiki
content, tickets or anything else; the pages are halted in the
mtrack_head() function).

* this revision marks the start of moving towards a better
out-of-the-box authentication story for mtrack.

This commit introduces self-registration and the start of a page to
allow users to get themselves an account. At the moment this is only
plumbed for OpenID and BrowserID. The other mechanisms will follow.
affected #: 24 files

diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e bin/runtests
--- a/bin/runtests
+++ b/bin/runtests
@@ -257,6 +257,7 @@
[core]
weburl="http://$ENV{INCUB_HOSTNAME}:$http_port/"
search_engine = $search_engine
+admin_party = false

[repos]
serverurl="$ENV{INCUB_HOSTNAME}:$ssh_port"


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e config.ini.sample
--- a/config.ini.sample
+++ b/config.ini.sample
@@ -148,3 +148,4 @@
; MTrackCommitCheck_RequiresTimeReference =
; MTrackCaptcha_Recaptcha = public, private, userclasses
; MTrackAuth_OpenID =
+; MTrackAuth_MTrack =


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e inc/auth.php
--- a/inc/auth.php
+++ b/inc/auth.php
@@ -103,10 +103,15 @@
/** returns the authenticated user, or null if authentication
* is required */
public static function authenticate() {
- foreach (self::$mechs as $mech) {
- $name = $mech->authenticate();
- if ($name !== null) {
- return $name;
+
+ /* admin party trumps all; we need to bypass all auth mechs
+ * while we're configuring the system */
+ if (!MTrackConfig::get('core', 'admin_party')) {
+ foreach (self::$mechs as $mech) {
+ $name = $mech->authenticate();
+ if ($name !== null) {
+ return $name;
+ }
}
}

@@ -119,10 +124,11 @@
return $_ENV[$name];
}
}
- } elseif (count(self::$mechs) == 0 &&
- MTrackConfig::get('core', 'admin_party') == 1
- && in_array($_SERVER['REMOTE_ADDR'], explode(',', MTrackConfig::get('core', 'admin_party_remote_address')))) {
- return 'adminparty';
+ } elseif (MTrackConfig::get('core', 'admin_party') == 1) {
+ $party = MTrackConfig::get('core', 'admin_party_remote_address');
+ if (in_array($_SERVER['REMOTE_ADDR'], explode(',', $party))) {
+ return 'adminparty';
+ }
}

return null;
@@ -138,7 +144,7 @@
if (count(self::$stack) == 0 && $doauth) {
try {
$who = self::authenticate();
- if ($who === null) {
+ if ($who === null && !MTrackConfig::get('core', 'admin_party')) {
foreach (self::$mechs as $mech) {
$who = $mech->doAuthenticate();
if ($who !== null) {
@@ -312,7 +318,7 @@
static function forceAuthenticate() {
try {
$who = self::authenticate();
- if ($who === null) {
+ if ($who === null && !MTrackConfig::get('core', 'admin_party')) {
foreach (self::$mechs as $mech) {
$who = $mech->doAuthenticate(true);
if ($who !== null) {


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e inc/auth/browserid.php
--- a/inc/auth/browserid.php
+++ b/inc/auth/browserid.php
@@ -13,49 +13,6 @@
}

function augmentUserInfo(&$content) {
- if (!isset($_SESSION['auth.browserid'])) {
- global $ABSWEB;
- $content = <<<HTML
-<script src='https://browserid.org/include.js'></script>
-<script>
-function browserid_got_assertion(assertion) {
- if (assertion) {
- $.ajax({
- url: ABSWEB + 'auth/browserid.php',
- contentType: 'application/octet-stream',
- type: 'POST',
- data: assertion,
- success: function(data) {
- if (data.status == 'okay') {
- // Success; reload page (ajax call set the cookie we need)
- window.location.reload();
- } else {
- window.alert("BrowserID authentication failed: " +
- data.reason);
- }
- },
- error: function (xhr, status, err) {
- // the alert is a bit nasty, but browserid tends to
- // deal with most errors by not even invoking this
- // assertion callback
- window.alert("BrowserID authentication failed: " +
- status + " " + err);
- }
- });
- }
-}
-
-function browserid_login() {
- navigator.id.get(browserid_got_assertion, {allowPersistent: true});
- return false;
-}
-$(function () {
- navigator.id.get(browserid_got_assertion, {silent:true});
-});
-</script>
-<a href='javascript:browserid_login()'>Log In</a>
-HTML;
- }
}
function augmentNavigation($id, &$items) {
}
@@ -73,7 +30,7 @@
function doAuthenticate($force = false) {
if ($force) {
global $ABSWEB;
- header("Location: {$ABSWEB}auth/browserid.php");
+ header("Location: {$ABSWEB}auth/");
exit;
}
return null;
@@ -104,8 +61,11 @@
}

function LogOut() {
- session_destroy();
- header('Location: ' . $GLOBALS['ABSWEB']);
- exit;
+ session_start();
+ if (isset($_SESSION['auth.browserid'])) {
+ session_destroy();
+ header('Location: ' . $GLOBALS['ABSWEB']);
+ exit;
+ }
}
}


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e inc/auth/facebook.php
--- a/inc/auth/facebook.php
+++ b/inc/auth/facebook.php
@@ -60,22 +60,8 @@
}

function augmentUserInfo(&$content) {
- global $ABSWEB;
-
- if (isset($_SESSION['auth.facebook'])) {
- return;
- }
-
- $me = $this->getMe();
- if (!$me) {
- $fb = $this->getFB();
- $url = $fb->getLoginUrl(array(
- 'redirect_uri' => $GLOBALS['ABSWEB'] . 'auth/facebook.php'
- ));
-
- $content = <<<HTML
-<a href='$url'>Log In</a>
-HTML;
+ if (!$this->authenticate()) {
+ $content = "<a href='$GLOBALS[ABSWEB]auth/'>Log In</a>";
}
}

@@ -93,9 +79,8 @@

function doAuthenticate($force = false) {
if ($force) {
- header("Location: " . $this->getFB()->getLoginUrl(array(
- 'redirect_uri' => $GLOBALS['ABSWEB'] . 'auth/facebook.php'
- )));
+ global $ABSWEB;
+ header("Location: {$ABSWEB}auth/");
exit;
}
return null;
@@ -126,12 +111,15 @@
}

function LogOut() {
- session_destroy();
- $fb = $this->getFB();
- header('Location: ' . $fb->getLogoutUrl(array(
- 'next' => $GLOBALS['ABSWEB']
- )));
- exit;
+ session_start();
+ if (isset($_SESSION['auth.facebook'])) {
+ session_destroy();
+ $fb = $this->getFB();
+ header('Location: ' . $fb->getLogoutUrl(array(
+ 'next' => $GLOBALS['ABSWEB']
+ )));
+ exit;
+ }
}
}



diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e inc/auth/mtrack.php
--- a/inc/auth/mtrack.php
+++ b/inc/auth/mtrack.php
@@ -11,14 +11,47 @@

function augmentUserInfo(&$content) {
if (!$this->authenticate()) {
- $content = "<a href='$GLOBALS[ABSWEB]auth/mtrack.php'>Log In</a>";
+ $content = "<a href='$GLOBALS[ABSWEB]auth/'>Log In</a>";
}
}

function augmentNavigation($id, &$items) {
}

+ /* If we're running under the REST bits, we may want to use HTTP
+ * auth instead of cookies.
+ *
+ * if we've already got a session, and it is not empty,
+ * we assume that the session was started via browser based auth,
+ * or by some cookie aware client.
+ *
+ * Otherwise, we want to use HTTP auth
+ */
+ function shouldUseHTTPAuth() {
+ if (defined('MTRACK_IS_REST_API')) {
+ if (isset($_COOKIE[session_name()])) {
+ /* client sent us a cookie */
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
function authenticate() {
+ if ($this->shouldUseHTTPAuth()) {
+ if (isset($_SERVER['PHP_AUTH_USER'])) {
+ $user = MTrackUser::loadUser($_SERVER['PHP_AUTH_USER'], true);
+ if (!$user) {
+ return null;
+ }
+ if ($user->verifyPassword($_SERVER['PHP_AUTH_PW'])) {
+ return $user->userid;
+ }
+ }
+ return null;
+ }
+
if (!strlen(session_id()) && php_sapi_name() != 'cli') {
session_start();
}
@@ -30,8 +63,18 @@
}

function doAuthenticate($force = false) {
+ if (defined('MTRACK_IS_REST_API')) {
+ if ($this->shouldUseHTTPAuth()) {
+ header("WWW-Authenticate: Basic realm=\"$_SERVER[SERVER_NAME]\"");
+ exit;
+ }
+ }
if ($force) {
- header("Location: $GLOBALS[ABSWEB]auth/mtrack.php");
+ if ($this->shouldUseHTTPAuth()) {
+ header("WWW-Authenticate: Basic realm=\"$_SERVER[SERVER_NAME]\"");
+ } else {
+ header("Location: $GLOBALS[ABSWEB]auth/");
+ }
exit;
}
return null;
@@ -64,9 +107,14 @@
}

function LogOut() {
- session_destroy();
- header('Location: ' . $GLOBALS['ABSWEB']);
- exit;
+ if (isset($_COOKIE[session_name()])) {
+ session_start();
+ if (isset($_SESSION['auth.mtrack'])) {
+ session_destroy();
+ header('Location: ' . $GLOBALS['ABSWEB']);
+ exit;
+ }
+ }
}

}


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e inc/auth/openid.php
--- a/inc/auth/openid.php
+++ b/inc/auth/openid.php
@@ -10,10 +10,8 @@

function augmentUserInfo(&$content) {
global $ABSWEB;
- if (isset($_SESSION['openid.id'])) {
- //$content .= " | <a href='{$ABSWEB}auth/openid.php/signout'>Log off</a>";
- } else {
- $content = "<a href='{$ABSWEB}auth/openid.php'>Log In</a>";
+ if (!isset($_SESSION['openid.id'])) {
+ $content = "<a href='{$ABSWEB}auth/'>Log In</a>";
}
}

@@ -24,11 +22,8 @@
if (!strlen(session_id()) && php_sapi_name() != 'cli') {
session_start();
}
- if (isset($_SESSION['openid.id'])) {
- if (isset($_SESSION['openid.userid'])) {
- return $_SESSION['openid.userid'];
- }
- return $_SESSION['openid.id'];
+ if (isset($_SESSION['openid.userid'])) {
+ return $_SESSION['openid.userid'];
}
return null;
}
@@ -36,7 +31,7 @@
function doAuthenticate($force = false) {
if ($force) {
global $ABSWEB;
- header("Location: {$ABSWEB}auth/openid.php");
+ header("Location: {$ABSWEB}auth/");
exit;
}
return null;
@@ -67,9 +62,12 @@
}

function LogOut() {
- session_destroy();
- header('Location: ' . $GLOBALS['ABSWEB']);
- exit;
+ session_start();
+ if (isset($_SESSION['openid.userid'])) {
+ session_destroy();
+ header('Location: ' . $GLOBALS['ABSWEB']);
+ exit;
+ }
}
}



diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e inc/configuration.php
--- a/inc/configuration.php
+++ b/inc/configuration.php
@@ -66,8 +66,14 @@
foreach (self::$runtime as $section => $opts) {
fwrite($fp, "[$section]\n");
foreach ($opts as $k => $v) {
- $v = addcslashes($v, "\"\r\n\t");
- fwrite($fp, "$k = \"$v\"\n");
+ switch (gettype($v)) {
+ case 'boolean':
+ $v = $v ? 'true' : 'false';
+ break;
+ default:
+ $v = '"' . addcslashes($v, "\"\r\n\t") . '"';
+ }
+ fwrite($fp, "$k = $v\n");
}
fwrite($fp, "\n");
}
@@ -81,11 +87,10 @@
}

static function _get($section, $option, $b64 = false) {
- $ini = self::$ini;
- if (isset(self::$ini[$section][$option])) {
+ if (isset(self::$runtime[$section][$option])) {
+ $val = self::$runtime[$section][$option];
+ } else if (isset(self::$ini[$section][$option])) {
$val = self::$ini[$section][$option];
- } else if (isset(self::$runtime[$section][$option])) {
- $val = self::$runtime[$section][$option];
} else {
return null;
}


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e inc/user.php
--- a/inc/user.php
+++ b/inc/user.php
@@ -8,18 +8,21 @@
public $fullname;
public $email;
public $timezone;
- public $active;
+ public $active = true;
public $sshkeys = null;
public $aliases = null;
public $prefs = null;
private $stored = false;

- static function loadUser($username) {
+ static function loadUser($username, $storedOnly = false) {
$username = mtrack_canon_username($username);

$data = MTrackDB::q('select userid, fullname, email, timezone, active, sshkeys, prefs from userinfo where userid = ?', $username)
->fetchAll(PDO::FETCH_ASSOC);
if (!isset($data[0])) {
+ if ($storedOnly) {
+ return null;
+ }
$udata = MTrackAuth::getUserData($username);
if (count($udata) == 1 && isset($udata['fullname'])) {
/* we faked it; not a legitimate entry for our purposes */


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -203,6 +203,9 @@
HTML;

}
+
+ $not_configured = false;
+
$party_addrs = MTrackConfig::get('core', 'admin_party_remote_address');
if (MTrackConfig::get('core', 'admin_party') == 1 &&
MTrackAuth::whoami() == 'adminparty' &&
@@ -219,14 +222,13 @@
Authentication is not yet configured;
while it is in this state, any user connecting from <b>$party_addrs</b>
is treated as having admin rights (that includes you, and this
- is why you are seeing this message). All other users are treated
- as anonymous users.</p>
+ is why you are seeing this message). All other users are denied
+ access.</p><p><a class='btn btn-danger' href="{$ABSWEB}admin/auth.php">Click here to Configure Authentication</a></p></div>
HTML;
}
- } elseif (!MTrackAuth::isAuthConfigured() &&
- MTrackConfig::get('core', 'admin_party') == 1)
+ } elseif (MTrackConfig::get('core', 'admin_party') == 1)
{
$localaddr = preg_replace('@^(https?://)([^/]+)/(.*)$@',
"\${1}127.0.0.1/\\3", $ABSWEB);
@@ -241,14 +243,17 @@
to reach the system and configure it, or configure the <b>admin_party_remote_address</b> option to include <b>$remoteaddr</b>.
</div>
HTML;
+ $not_configured = true;
} elseif (!MTrackAuth::isAuthConfigured()) {
echo <<<HTML
<div class='alert alert-error'><a class='close' data-dismiss='alert'>&times;</a><b>Authentication is not yet configured</b>. If you are the admin,
- you will need to edit the config.ini file to configure authentication.
+ you will need to edit the config.ini or var/runtime.config file to
+ configure authentication.
</div>
HTML;
+ $not_configured = true;
}

if (preg_match("/(on|true|1)/i", ini_get('magic_quotes_gpc'))) {
@@ -259,12 +264,18 @@
</div>
HTML;

+ $not_configured = true;
}

echo <<<HTML
</div><div id="content">
HTML;
+
+ if ($not_configured) {
+ mtrack_foot();
+ exit;
+ }
}

function mtrack_foot($visible_markup = true, $show_footer = false)


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -3,322 +3,120 @@
include '../../inc/common.php';

MTrackACL::requireAnyRights('User', 'modify');
-$plugins = MTrackConfig::getSection('plugins');

-function get_openid_admins()
-{
- $admins = array();
- $regadmins = array();
- foreach (MTrackConfig::getSection('user_classes') as $id => $role) {
- if ($role == 'admin') {
- if (preg_match('@^https?://@', $id)) {
- $admins[] = $id;
- } else {
- $regadmins[$id] = $id;
- }
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+
+ function is_cb_set($key) {
+ return isset($_POST[$key]) && $_POST[$key] == 'on';
+ }
+ function set_cb($name, $key) {
+ if (is_cb_set($key)) {
+ MTrackConfig::set('core', $name, true);
+ } else {
+ MTrackConfig::set('core', $name, false);
}
}
- if (count($regadmins)) {
- /* look at aliases to see if there are any that look like OpenIDs */
- foreach (MTrackDB::q('select alias, userid from useraliases')->fetchAll()
- as $row) {
- if (!preg_match('@^https?://@', $row[0])) {
- continue;
- }
- if (isset($regadmins[$row[1]])) {
- $admins[] = $row[0];
- }
- }
+
+ set_cb('admin_party', 'party');
+ set_cb('allow_self_registration', 'register');
+
+ if (is_cb_set('local')) {
+ /* turn it on: no parameters */
+ MTrackConfig::set('plugins', 'MTrackAuth_MTrack', '');
+ } else {
+ MTrackConfig::remove('plugins', 'MTrackAuth_MTrack');
}
- return $admins;
+
+ // This bit is likely made obsolete by planned class/role editor
+ if (is_cb_set('restricted')) {
+ // Force anonymous users to have no rights
+ MTrackConfig::set('user_class_roles', 'anonymous', '');
+ } else {
+ // Reset anonymous users to default rights from config.ini
+ MTrackConfig::remove('user_class_roles', 'anonymous');
+ }
+
+ MTrackConfig::save();
+ header("Location: $GLOBALS[ABSWEB]admin/auth.php");
+ exit;
}

-function get_admins()
-{
- $admins = array();
- foreach (MTrackConfig::getSection('user_classes') as $id => $role) {
- if ($role == 'admin' && !preg_match('@^https?://@', $id)) {
- $admins[] = $id;
- }
- }
- return $admins;
-}
-
-$message = null;
-
-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- if (isset($_POST['setuppublic'])) {
- $admins = get_openid_admins();
- $add_admin = isset($_POST['adminopenid']) ?
- trim($_POST['adminopenid']) : '';
- $localid = isset($_POST['adminuserid']) ?
- trim($_POST['adminuserid']) : '';
- if (count($admins) == 0 && (!strlen($add_admin) || !strlen($localid))) {
- $message = "You MUST add an OpenID for the administrator";
- } else {
- if (strlen($localid)) {
- MTrackConfig::set('user_classes', $localid, 'admin');
- }
- $new = true;
- foreach (MTrackDB::q('select userid from userinfo where userid = ?',
- $localid)->fetchAll() as $row) {
- $new = false;
- break;
- }
- if ($new) {
- MTrackDB::q('insert into userinfo (userid, active) values (?, 1)', $localid);
- }
- $new = true;
- foreach (MTrackDB::q('select userid from useraliases where alias = ?', $add_admin)->fetchAll() as $row) {
- if ($row[0] != $localid) {
- throw new Exception("$add_admin is already associated with $row[0]");
- }
- $new = false;
- }
- if ($new) {
- MTrackDB::q('insert into useraliases (userid, alias) values (?,?)',
- $localid, $add_admin);
- }
-
- MTrackConfig::set('plugins', 'MTrackAuth_OpenID', '');
- if (isset($plugins['MTrackAuth_HTTP'])) {
- MTrackConfig::remove('plugins', 'MTrackAuth_HTTP');
- // Reset anonymous for public access
- MTrackConfig::remove('user_class_roles', 'anonymous');
- }
-
- MTrackConfig::save();
- header("Location: {$ABSWEB}admin/auth.php");
- exit;
- }
- } elseif (isset($_POST['setupprivate'])) {
- $admins = get_admins();
- $add_admin = isset($_POST['adminuser']) ?
- trim($_POST['adminuser']) : '';
- if (count($admins) == 0 && !strlen($add_admin)) {
- $message = "You MUST add a user with admin rights";
- } else {
- $vardir = MTrackConfig::get('core', 'vardir');
- $pfile = "$vardir/http.user";
-
- if (strlen($add_admin)) {
- if (!isset($_SERVER['REMOTE_USER'])) {
- // validate the password
- if ($_POST['adminpass1'] != $_POST['adminpass2']) {
- $message = "Passwords don't match";
- } else {
- $http_auth = new MTrackAuth_HTTP(null, "digest:$pfile");
- $http_auth->setUserPassword($add_admin, $_POST['adminpass1']);
- }
- }
- MTrackConfig::set('user_classes', $add_admin, 'admin');
- }
- if ($message == null) {
- if (!isset($plugins['MTrackAuth_HTTP'])) {
- MTrackConfig::set('plugins', 'MTrackAuth_HTTP',
- "$vardir/http.group, digest:$pfile");
- }
- if (isset($plugins['MTrackAuth_OpenID'])) {
- MTrackConfig::remove('plugins', 'MTrackAuth_OpenID');
- // Set up the roles for private access
- // Use default authenticated permissions
- MTrackConfig::remove('user_class_roles', 'authenticated');
- // Make anonymous have no rights
- MTrackConfig::set('user_class_roles', 'anonymous', '');
- }
- MTrackConfig::save();
- header("Location: {$ABSWEB}admin/auth.php");
- exit;
- }
- }
- }
-}
+$plugins = MTrackConfig::getSection('plugins');

mtrack_head("Administration - Authentication");
mtrack_admin_nav();

-$plugins = MTrackConfig::getSection('plugins');
-$http_configd = isset($plugins['MTrackAuth_HTTP']) ? " (Active)" : '';
-$openid_configd = isset($plugins['MTrackAuth_OpenID']) ? " (Active)" : '';
+$party_addrs = MTrackConfig::get('core', 'admin_party_remote_address');

+$party_on /* dude */ =
+ MTrackConfig::get('core', 'admin_party') == '1' ? ' checked ' : '';

-?>
-<h1>Authentication</h1>
-<?php
-if ($message) {
- $message = htmlentities($message, ENT_QUOTES, 'utf-8');
- echo <<<HTML
-<div class='ui-state-error ui-corner-all'>
- <span class='ui-icon ui-icon-alert'></span>
- $message
-</div>
-HTML;
+$reg_on =
+ MTrackConfig::get('core', 'allow_self_registration') ? ' checked ' : '';
+
+$restricted_on =
+ MTrackConfig::get('user_class_roles', 'anonymous') == '' ? ' checked ' : '';
+
+// FIXME: want to make this effectively the default without making it
+// impossible to turn off. If we know that HTTP auth is not configured,
+// and we know that local auth is not explicitly disabled in runtime config,
+// we want this to be enabled so that it is saved that way when the form
+// is submitted
+
+$local_pw = MTrackAuth::getMech('MTrackAuth_MTrack');
+$local_on = '';
+if ($local_pw) {
+ $local_on = ' checked ';
+} else {
+ if (!isset(MTrackConfig::$runtime['plugins']['MTrackAuth_MTrack']) &&
+ !MTrackAuth::getMech('MTrackAuth_HTTP'))
+ {
+ $local_on = ' checked ';
+ }
}

-/* get me the right class name */
-function acc($what) {
- if ($what == 'http' &&
- (strlen($GLOBALS['http_configd']) +
- strlen($GLOBALS['openid_configd']) == 0)) {
- return 'collapse in';
- }
+echo <<<HTML
+<h1>Authentication</h1>

- return strlen($GLOBALS[$what . '_configd']) ? 'collapse in' : 'collapse';
-}
+<form method='POST'>
+<input type="checkbox" $party_on name="party"> Enable admin party<br>
+Anyone accessing the system from <b>$party_addrs</b> will be treated as an administrator.
+Everyone else will have read-only access and no self-enrolment will be allowed.
+<br>
+<br>
+<input type="checkbox" $reg_on name="register"> Allow users to register themselves and sign up with an account.
+<br>
+<br>
+<input type="checkbox" $local_on name="local"> Use mtrack's own password storage and cookie based authentication (salted SHA-512 passwords). Not compatible with the HTTP Auth module.
+<br>
+<br>
+<input type="checkbox" $restricted_on name="restricted"> Restrict all access to authenticated users.
+<br>
+<br>

-?>
-<div class='alert alert-info'>
-Select one of the following, depending
-on which one best matches your intended mtrack deployment. More options are
-possible via <tt>config.ini</tt>
-</div>
+<button type='submit' class='btn btn-primary'>Apply Auth Settings</button>
+</form>

-<form method='post'>
-<div id="accordion" class="accordion">
- <div class='accordion-group'>
- <div class='accordion-heading'>
- <a class='accordion-toggle' data-toggle="collapse"
- data-parent="#accordion" href="#httpauth">Private (HTTP authentication)<?php echo $http_configd ?></a>
-<div id="httpauth" class='accordion-body <?php echo acc('http'); ?>'>
-<div class='accordion-inner'>
-<p>
- I want to strictly control who has access to mtrack, and prevent
- anonymous users from having any rights.
-</p>
-<?php
-if (isset($_SERVER['REMOTE_USER'])) {
-?>
-<p>
- It looks like your web server is configured to use HTTP authentication
- (you're authenticated as <?php
- echo htmlentities($_SERVER['REMOTE_USER'], ENT_QUOTES, 'utf-8') ?>)
- mtrack will defer to your web server configuration for authentication.
- Contact your system administrator to add or remove users, or to change
- their passwords. You may still use the mtrack user management screens
- to change rights assignments for the users.
-</p>
-<?php
-} else {
-?>
-<p>
- mtrack will use HTTP authentication and store the password and group
- files in the <em>vardir</em>.
-</p>
-<?php
-}
-echo "<h3>Administrators</h3>";
-$admins = get_admins();
-$need_pw = true;
-if (count($admins)) {
- echo "<p>The following users are configured with admin rights:</p>";
- echo "<p>";
- $http = MTrackAuth::getMech('MTrackAuth_HTTP');
- foreach ($admins as $id) {
- $has_pw = false;
- if ($http && $http->readPWFile($id)) {
- $has_pw = true;
- $need_pw = false;
- }
- echo mtrack_username($id) . " ";
- if (!$has_pw) {
- echo "(no password) ";
- }
- }
- echo "</p>";
-}
-if ($need_pw) {
- echo <<<HTML
-<div class='alert'>
-You <em>MUST</em> add at least one user as an administrator (and give
-them a password!),
-otherwise no one will be able to administer the system without editing
-the config.ini file.
-</div>
+<ul>
+ <li><a href="{$ABSWEB}admin/auth/http.php">Configure HTTP Auth</a><br>
+ Configure mtrack to delegate authentication to the web server,
+ or perform authentication using the HTTP authentication mechanism.
+ </li>
+ <li><a href="{$ABSWEB}admin/auth/openid.php">Configure OpenID</a><br>
+ Enable OpenID authentication; allow users to associate OpenID
+ identifiers with their user accounts.
+ </li>
+ <li><a href="{$ABSWEB}admin/auth/facebook.php">Configure Facebook Auth</a><br>
+ Allow users to associate Facebook logins with their user accounts.
+ </li>
+
+ <li><a href="{$ABSWEB}admin/auth/browserid.php">Configure Mozilla Persona, aka BrowserID</a><br>
+ Allow users to associate Persona/BrowserID logins with
+ their user accounts.
+ </li>
+</ul>
+
+
HTML;
-
- echo <<<HTML
-<table>
-<tr>
-<td><b>Add Admin User</b>:</td>
-<td><input type="text" name="adminuser"></td>
-</tr>
-HTML;
-
- if (!isset($_SERVER['REMOTE_USER'])) {
- echo <<<HTML
-<tr>
- <td><b>Set Password</b>:</td>
- <td><input type="password" name="adminpass1"></td>
-</tr>
-<tr>
- <td><b>Confirm Password</b>:</td>
- <td><input type="password" name="adminpass2"></td>
-</tr>
-</table>
-HTML;
- } else {
- echo <<<HTML
-</table>
-<p>
-<em>You can't set the password here, because mtrack has no way to automatically
-find out how to do that. Contact your system administrator to ensure that
-you have a username and password configured for mtrack</em></p>
-HTML;
- }
-}
-?>
- <input class='btn btn-primary' type='submit' name='setupprivate'
- value='Configure Private Authentication'>
-
- </div>
- </div>
-</div>
-</div>
-
-<div class='accordion-group'>
- <div class='accordion-heading'>
- <a class='accordion-toggle' data-toggle="collapse"
- data-parent="#accordion" href="#openidauth">Public (OpenID)<?php echo $openid_configd ?></a>
-<div id="openidauth" class='accordion-body <?php echo acc('openid'); ?>'>
-<div class='accordion-inner'>
-<p>
- I want to allow the public access to mtrack, but only allow people that
- I trust to make certain kinds of changes.
-</p>
-<p>
- mtrack will use OpenID to manage authentication.
-</p>
-<h3>Administrators</h3>
-<?php
-$admins = get_openid_admins();
-if (count($admins)) {
- echo "<p>The following OpenID users are configured with admin rights:</p>";
- echo "<p>";
- foreach ($admins as $id) {
- echo mtrack_username($id) . " ($id) ";
- }
- echo "</p>";
-} else {
- echo <<<HTML
-<div class='alert'>
-You <em>MUST</em> add at least one OpenID as an administrator,
-otherwise no one will be able to administer the system without editing
-the config.ini file.
-</div>
-HTML;
-}
-?>
-<b>Add Admin OpenID</b>: <input type="text" name="adminopenid"><br>
-<b>Local Username</b>: <input type="text" name="adminuserid"><br>
- <input class='btn btn-primary' type='submit' name='setuppublic'
- value='Configure Public Authentication'>
-
- </div>
- </div>
-</div>
-</div>
-
-</form>
-<?php
mtrack_foot();



diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/admin/auth/browserid.php
--- /dev/null
+++ b/web/admin/auth/browserid.php
@@ -0,0 +1,46 @@
+<?php # vim:ts=2:sw=2:et:
+/* For copyright and licensing terms, see the file named LICENSE */
+
+include '../../../inc/common.php';
+
+$plugins = MTrackConfig::getSection('plugins');
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (isset($_POST['browserid_on']) && $_POST['browserid_on'] == 'on') {
+ /* turn it on: no parameters */
+ MTrackConfig::set('plugins', 'MTrackAuth_BrowserID', '');
+ } else {
+ /* turn it off */
+ MTrackConfig::remove('plugins', 'MTrackAuth_BrowserID');
+ }
+
+ MTrackConfig::save();
+ header("Location: {$ABSWEB}admin/auth.php");
+ exit;
+}
+
+mtrack_head("Administration - BrowserID");
+mtrack_admin_nav();
+
+$on = isset($plugins['MTrackAuth_BrowserID']) ? ' checked ' : '';
+
+echo <<<HTML
+<h1>Mozilla Persona / BrowserID</h1>
+<p>
+ The BrowserID module allows users to login using the Mozilla Persona
+ (aka BrowserID) de-centralized login facility instead of
+ managing and maintaining a password within mtrack. Users still need
+ to have an mtrack account name, and enabling BrowserID does <b>not</b>
+ require all users to switch to BrowserID.
+</p>
+<br>
+
+<form method='POST'>
+ <input type='checkbox' name='browserid_on' $on> Enable BrowserID
+ <br>
+ <button class='btn btn-primary' type='submit'>
+ Save BrowserID Settings</button>
+</form>
+HTML;
+
+mtrack_foot();


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/admin/auth/facebook.php
--- /dev/null
+++ b/web/admin/auth/facebook.php
@@ -0,0 +1,91 @@
+<?php # vim:ts=2:sw=2:et:
+/* For copyright and licensing terms, see the file named LICENSE */
+
+include '../../../inc/common.php';
+
+$plugins = MTrackConfig::getSection('plugins');
+
+function is_hex_id($key) {
+ $val = $_POST[$key];
+
+ if (preg_match("/^[a-fA-F0-9]+$/", $val)) {
+ return $val;
+ }
+ return null;
+}
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (isset($_POST['facebook_on']) && $_POST['facebook_on'] == 'on') {
+ /* turn it on */
+ $appid = is_hex_id('appid');
+ $secret = is_hex_id('secret');
+ if ($appid && $secret) {
+ MTrackConfig::set('plugins', 'MTrackAuth_Facebook', "$appid,$secret");
+ }
+ } else {
+ /* turn it off */
+ MTrackConfig::remove('plugins', 'MTrackAuth_Facebook');
+ }
+
+ MTrackConfig::save();
+ header("Location: {$ABSWEB}admin/auth.php");
+ exit;
+}
+
+mtrack_head("Administration - Facebook");
+mtrack_admin_nav();
+
+$on = isset($plugins['MTrackAuth_Facebook']) ? ' checked ' : '';
+
+$FB = MTrackAuth::getMech('MTrackAuth_Facebook');
+if ($FB) {
+ $appid = htmlentities($FB->appid, ENT_QUOTES, 'utf-8');
+ $secret = htmlentities($FB->secret, ENT_QUOTES, 'utf-8');
+} else {
+ $appid = '';
+ $secret = '';
+}
+
+echo <<<HTML
+<h1>Facebook Authentication</h1>
+<p>
+ The Facebook auth module allows users to login using their Facebook
+ login instead of
+ managing and maintaining a password within mtrack. Users still need
+ to have an mtrack account name, and enabling Facebook auth does <b>not</b>
+ require all users to switch to Facebook credentials.
+</p>
+<p>
+ In order to enable Facebook authentication, you need to register your
+ mtrack installation as a App using the <a href="https://developers.facebook.com/apps">Facebook Developers App Site</a>.
+</p>
+<br>
+
+<form method='POST'>
+ <table>
+ <tr>
+ <td>Site URL</td>
+ <td><tt>$ABSWEB</tt></td>
+ </tr>
+ <tr>
+ <td><label for="appid">App ID</label></td>
+ <td><input type="text" name="appid"
+ value="$appid"
+ placeholder="Enter your App ID"></td>
+ </tr>
+ <tr>
+ <td><label for="secret">App Secret</label></td>
+ <td><input type="text" name="secret"
+ value="$secret"
+ placeholder="Enter your App Secret"></td>
+ </tr>
+ <table>
+ <input type='checkbox' name='facebook_on' $on> Enable Facebook Authentication
+ <br>
+ <button class='btn btn-primary' type='submit'>
+ Save Facebook Settings</button>
+</form>
+HTML;
+
+mtrack_foot();
+


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/admin/auth/http.php
--- /dev/null
+++ b/web/admin/auth/http.php
@@ -0,0 +1,137 @@
+<?php # vim:ts=2:sw=2:et:
+/* For copyright and licensing terms, see the file named LICENSE */
+
+include '../../../inc/common.php';
+
+$plugins = MTrackConfig::getSection('plugins');
+
+function get_admins()
+{
+ $admins = array();
+ foreach (MTrackConfig::getSection('user_classes') as $id => $role) {
+ if ($role == 'admin' && !preg_match('@^https?://@', $id)) {
+ $admins[] = $id;
+ }
+ }
+ return $admins;
+}
+
+function get_admins_with_pw_count()
+{
+ $have_pw = 0;
+
+ $admins = get_admins();
+ if (count($admins)) {
+ $http = MTrackAuth::getMech('MTrackAuth_HTTP');
+ foreach ($admins as $id) {
+ if ($http && $http->readPWFile($id)) {
+ $have_pw++;
+ }
+ }
+ }
+ return $have_pw;
+}
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (isset($_POST['http_on']) && $_POST['http_on'] == 'on') {
+ $vardir = MTrackConfig::get('core', 'vardir');
+ $pfile = "$vardir/http.user";
+
+ if (!isset($plugins['MTrackAuth_HTTP'])) {
+ MTrackConfig::set('plugins', 'MTrackAuth_HTTP',
+ "$vardir/http.group, digest:$pfile");
+ }
+ } else {
+ // Turn it off
+ MTrackConfig::remove('plugins', 'MTrackAuth_HTTP');
+ }
+ MTrackConfig::save();
+ header("Location: {$ABSWEB}admin/auth.php");
+ exit;
+}
+
+mtrack_head("HTTP Authentication");
+mtrack_admin_nav();
+
+echo <<<HTML
+<h1>HTTP Authentication</h1>
+<p>
+ This page configures the HTTP Authentication plugin. This plugin
+ can operate in one of two modes; it can either defer to your web
+ server for authentication, or it can implement HTTP Digest Authentication
+ using its own locally stored digest authentication file.
+</p>
+<div class='alert'>
+ <b>Warning</b> - Enabling this module prevents using any other forms
+ of authentication.
+</div>
+HTML;
+
+
+if (isset($_SERVER['REMOTE_USER'])) {
+ $remote = htmlentities($_SERVER['REMOTE_USER'], ENT_QUOTES, 'utf-8');
+ echo <<<HTML
+<p>
+ It looks like your web server is configured to use HTTP authentication
+ (you're authenticated as $remote)
+ mtrack will defer to your web server configuration for authentication.
+ Contact your system administrator to add or remove users, or to change
+ their passwords. You may still use the mtrack user management screens
+ to change rights assignments for the users.
+</p>
+HTML;
+
+} else {
+ echo <<<HTML
+<p>
+ mtrack will use its own HTTP authentication and store the password and group
+ files in the <em>vardir</em>.
+</p>
+<p>
+ This configuration is a legacy configuration and it is recommended that
+ you consider moving to use the main mtrack authentication system instead.
+</p>
+HTML;
+}
+$need_pw = get_admins_with_pw_count() == 0;
+if ($need_pw) {
+ if (isset($_SERVER['REMOTE_USER'])) {
+ echo <<<HTML
+<div class='alert alert-danger'>
+mtrack was unable to find any admin level users based on your current
+configuration. Make sure that you have at least one user configured
+with admin privileges. You may need to manually configure the plugin
+to find the group file it is using.
+</div>
+HTML;
+
+ } else {
+ echo <<<HTML
+<div class='alert alert-danger'>
+You <em>MUST</em> add at least one user as an administrator (and give
+them a password!),
+otherwise no one will be able to administer the system without editing
+the config.ini file.
+<p>
+Use the
+<a href="{$ABSWEB}admin/user.php">Users</a> admin section to manage
+users; note that you need to save this screen before the password
+tab will be enabled.
+</p>
+</div>
+HTML;
+ }
+}
+
+$http_on = isset($plugins['MTrackAuth_HTTP']) ? ' checked ' : '';
+
+echo <<<HTML
+<form method='POST'>
+ <input type='checkbox' name='http_on' $http_on> Enable HTTP Authentication
+ <br>
+ <button class='btn btn-primary' type='submit'>
+ Save HTTP Authentication Settings</button>
+</form>
+HTML;
+
+mtrack_foot();


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/admin/auth/openid.php
--- /dev/null
+++ b/web/admin/auth/openid.php
@@ -0,0 +1,45 @@
+<?php # vim:ts=2:sw=2:et:
+/* For copyright and licensing terms, see the file named LICENSE */
+
+include '../../../inc/common.php';
+
+$plugins = MTrackConfig::getSection('plugins');
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (isset($_POST['openid_on']) && $_POST['openid_on'] == 'on') {
+ /* turn it on: no parameters */
+ MTrackConfig::set('plugins', 'MTrackAuth_OpenID', '');
+ } else {
+ /* turn it off */
+ MTrackConfig::remove('plugins', 'MTrackAuth_OpenID');
+ }
+
+ MTrackConfig::save();
+ header("Location: {$ABSWEB}admin/auth.php");
+ exit;
+}
+
+mtrack_head("Administration - OpenID");
+mtrack_admin_nav();
+
+$on = isset($plugins['MTrackAuth_OpenID']) ? ' checked ' : '';
+
+echo <<<HTML
+<h1>OpenID</h1>
+<p>
+ The OpenID module allows users to login using an OpenID instead of
+ managing and maintaining a password within mtrack. Users still need
+ to have an mtrack account name, and enabling OpenID does <b>not</b>
+ require all users to switch to OpenID.
+</p>
+<br>
+
+<form method='POST'>
+ <input type='checkbox' name='openid_on' $on> Enable OpenID
+ <br>
+ <button class='btn btn-primary' type='submit'>
+ Save OpenID Settings</button>
+</form>
+HTML;
+
+mtrack_foot();


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/auth/browserid.php
--- a/web/auth/browserid.php
+++ b/web/auth/browserid.php
@@ -2,6 +2,11 @@
/* For licensing and copyright terms, see the file named LICENSE */
include '../../inc/common.php';

+if (!MTrackAuth::getMech('MTrackAuth_BrowserID')) {
+ header("Location: $ABSWEB");
+ exit;
+}
+
function verify_assertion($assertion)
{
if (!strlen($assertion)) {
@@ -26,13 +31,28 @@
return json_decode($json);
}

+if (!session_id()) {
+ session_start();
+}
+
$assertion = file_get_contents('php://input');
$res = verify_assertion($assertion);
if ($res->status == 'okay') {
- $user = mtrack_canon_username($res->email);
+ $user = MTrackUser::loadUser($res->email, true);
if ($user) {
- $_SESSION['auth.browserid'] = $user;
- $res->user = $user;
+ $_SESSION['auth.browserid'] = $user->userid;
+ $res->user = $user->userid;
+ } else {
+ unset($_SESSION['auth.browserid']);
+
+ $_SESSION['mtrack.auth.register'] = array(
+ 'mech' => 'MTrackAuth_BrowserID',
+ 'email' => $res->email,
+ 'sreg' => $res,
+ );
+ session_write_close();
+
+ $res->status = 'register';
}
}



diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/auth/index.php
--- /dev/null
+++ b/web/auth/index.php
@@ -0,0 +1,236 @@
+<?php # vim:ts=2:sw=2:et:
+/* For copyright and licensing terms, see the file named LICENSE */
+
+include '../../inc/common.php';
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (MTrackAuth::getMech('MTrackAuth_MTrack')) {
+ /* authenticate against our password database */
+ if (isset($_POST['userid']) && isset($_POST['password'])) {
+ $user = MTrackUser::loadUser($_POST['userid']);
+ if ($user) {
+ if ($user->verifyPassword($_POST['password'])) {
+ if (session_id()) {
+ session_regenerate_id(true);
+ } else {
+ session_start();
+ }
+ $_SESSION['auth.mtrack'] = $user->userid;
+ header("Location: $ABSWEB");
+ exit;
+ }
+ }
+ }
+ $fail = "Invalid username or password";
+ }
+}
+
+mtrack_head('Login');
+
+echo <<<HTML
+<h1>Log In</h1>
+HTML;
+
+// MTrack local passwords
+if (MTrackAuth::getMech('MTrackAuth_MTrack')) {
+ echo <<<HTML
+<form method="POST">
+
+<table>
+ <tr>
+ <td>
+ <label for="userid">User Id</label>
+ </td>
+ <td>
+ <input type="text" name="userid" placeholder="Enter your user id">
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <label for="password">Password</label>
+ </td>
+ <td>
+ <input type="password" name="password" placeholder="Enter your password">
+ </td>
+ </tr>
+</table>
+HTML;
+
+ if ($fail) {
+ $fail = htmlentities($fail, ENT_QUOTES, 'utf-8');
+
+ echo <<<HTML
+<div class="alert alert-danger">
+ <a class="close" data-dismiss="alert">&times;</a>
+ $fail
+</div>
+HTML;
+ }
+
+ echo <<<HTML
+ <button type='submit' class='btn btn-primary'>Log In</button>
+
+</form>
+
+HTML;
+}
+
+if (MTrackAuth::getMech('MTrackAuth_OpenID')) {
+ echo <<<HTML
+<script type="text/javascript" src="{$ABSWEB}js/openid-jquery.js"></script>
+<script type="text/javascript" src="{$ABSWEB}js/openid-en.js"></script>
+<script>
+$(document).ready(function() {
+ openid.img_path = ABSWEB + 'images/';
+ openid.init('openid_identifier');
+});
+</script>
+<style>
+#openid_form {
+ width: 580px;
+}
+
+#openid_form legend {
+ font-weight: bold;
+}
+
+#openid_choice {
+ display: none;
+}
+
+#openid_input_area {
+ clear: both;
+ padding: 10px;
+}
+
+#openid_btns, #openid_btns br {
+ clear: both;
+}
+
+#openid_highlight {
+ padding: 3px;
+ background-color: #FFFCC9;
+ float: left;
+}
+.openid_large_btn {
+ width: 100px;
+ height: 60px;
+/* fix for IE 6 only: http://en.wikipedia.org/wiki/CSS_filter#Underscore_hack */
+ _width: 102px;
+ _height: 62px;
+
+ border: 1px solid #DDD;
+ margin: 3px;
+ float: left;
+}
+
+.openid_small_btn {
+ width: 24px;
+ height: 24px;
+/* fix for IE 6 only: http://en.wikipedia.org/wiki/CSS_filter#Underscore_hack */
+ _width: 26px;
+ _height: 26px;
+
+ border: 1px solid #DDD;
+ margin: 3px;
+ float: left;
+}
+
+a.openid_large_btn:focus {
+ outline: none;
+}
+
+a.openid_large_btn:focus {
+ -moz-outline-style: none;
+}
+
+.openid_selected {
+ border: 4px solid #DDD;
+}
+
+#openid_identifier {
+ width: 20em;
+}
+</style>
+<form method="POST" action="{$ABSWEB}auth/openid.php" id="openid_form">
+ <div id="openid_choice">
+ <p>Please click your account provider:</p>
+ <div id="openid_btns"></div>
+ </div>
+ <div id="openid_input_area">
+ <input type="text" name="openid_identifier" id="openid_identifier">
+ <button type="submit" class="btn btn-primary">
+ Sign-In</button>
+ </div>
+</form>
+HTML;
+}
+
+// BrowserID
+if (MTrackAuth::getMech('MTrackAuth_BrowserID')) {
+ echo <<<HTML
+
+<script src='https://browserid.org/include.js'></script>
+<script>
+function browserid_got_assertion(assertion) {
+ if (assertion) {
+ $.ajax({
+ url: ABSWEB + 'auth/browserid.php',
+ contentType: 'application/octet-stream',
+ type: 'POST',
+ data: assertion,
+ success: function(data) {
+ if (data.status == 'okay') {
+ // Success; reload page (ajax call set the cookie we need)
+ window.location = ABSWEB;
+ } else if (data.status == 'register') {
+ // Success, but user needs to register
+ window.location = ABSWEB + 'auth/register.php';
+ } else {
+ window.alert("BrowserID authentication failed: " +
+ data.reason);
+ }
+ },
+ error: function (xhr, status, err) {
+ // the alert is a bit nasty, but browserid tends to
+ // deal with most errors by not even invoking this
+ // assertion callback
+ window.alert("BrowserID authentication failed: " +
+ status + " " + err);
+ }
+ });
+ }
+}
+
+function browserid_login() {
+ navigator.id.get(browserid_got_assertion, {allowPersistent: true});
+ return false;
+}
+$(function () {
+ //navigator.id.get(browserid_got_assertion, {silent:true});
+});
+</script>
+<a href='javascript:browserid_login()' class='btn'>Log In using BrowserID</a>
+HTML;
+
+}
+
+// Facebook
+$FBA = MTrackAuth::getMech('MTrackAuth_Facebook');
+if ($FBA) {
+ $url = $FBA->getFB()->getLoginUrl(array(
+ 'redirect_uri' => $GLOBALS['ABSWEB'] . 'auth/facebook.php'
+ ));
+
+ echo <<<HTML
+<a href="$url" class="btn">Log In using Facebook</a>
+HTML;
+}
+echo "<pre>\n";
+var_dump($_SESSION);
+echo "</pre>\n";
+
+
+mtrack_foot();
+


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/auth/mtrack.php
--- a/web/auth/mtrack.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php # vim:ts=2:sw=2:et:
-/* For licensing and copyright terms, see the file named LICENSE */
-
-include '../../inc/common.php';
-
-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- if (isset($_POST['userid']) && isset($_POST['password'])) {
- $user = MTrackUser::loadUser($_POST['userid']);
- if ($user) {
- if ($user->verifyPassword($_POST['password'])) {
- if (session_id()) {
- session_regenerate_id(true);
- } else {
- session_start();
- }
- $_SESSION['auth.mtrack'] = $user->userid;
- header("Location: $ABSWEB");
- exit;
- }
- }
- }
- $fail = "Invalid username or password";
-}
-
-mtrack_head('Login');
-
-echo <<<HTML
-<h1>Log In</h1>
-
-<form method="POST">
-
-<table>
- <tr>
- <td>
- <label for="userid">User Id</label>
- </td>
- <td>
- <input type="text" name="userid" placeholder="Enter your user id">
- </td>
- </tr>
-
- <tr>
- <td>
- <label for="password">Password</label>
- </td>
- <td>
- <input type="password" name="password" placeholder="Enter your password">
- </td>
- </tr>
-</table>
-HTML;
-
-if ($fail) {
- $fail = htmlentities($fail, ENT_QUOTES, 'utf-8');
-
- echo <<<HTML
-<div class="alert alert-danger">
- <a class="close" data-dismiss="alert">&times;</a>
- $fail
-</div>
-HTML;
-}
-
-echo <<<HTML
- <button type='submit' class='btn btn-primary'>Log In</button>
-
-</form>
-
-HTML;
-
-mtrack_foot();
-


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/auth/openid.php
--- a/web/auth/openid.php
+++ b/web/auth/openid.php
@@ -6,6 +6,11 @@
require_once 'Auth/OpenID/SReg.php';
require_once 'Auth/OpenID/PAPE.php';

+if (!MTrackAuth::getMech('MTrackAuth_OpenID')) {
+ header("Location: $ABSWEB");
+ exit;
+}
+
$store_location = MTrackConfig::get('openid', 'store_dir');
if (!$store_location) {
$store_location = MTrackConfig::get('core', 'vardir') . '/openid';
@@ -88,29 +93,27 @@
$_SESSION['openid.email'] = $sreg['email'];
}
/* See if we can find a canonical identity for the user */
- foreach (MTrackDB::q('select userid from useraliases where alias = ?',
- $id)->fetchAll() as $row) {
- $_SESSION['openid.userid'] = $row[0];
- break;
+
+ $user = MTrackUser::loadUser($id, true);
+ if ($user) {
+ $_SESSION['openid.userid'] = $user->userid;
+ header("Location: " . $ABSWEB);
+ exit;
}

- if (!isset($_SESSION['openid.userid'])) {
- /* no alias; is there a direct userinfo entry? */
- foreach (MTrackDB::q('select userid from userinfo where userid = ?',
- $id)->fetchAll() as $row) {
- $_SERVER['openid.userid'] = $row[0];
- break;
- }
- }
+ /* prompt the user to fill out some basic details so that we can create
+ * a local identity and associate their OpenID with it */

- if (!isset($_SESSION['openid.userid'])) {
- /* prompt the user to fill out some basic details so that we can create
- * a local identity and associate their OpenID with it */
- header("Location: {$ABSWEB}auth/openid.php/register?" .
- http_build_query($sreg));
- } else {
- header("Location: " . $ABSWEB);
- }
+ $_SESSION['mtrack.auth.register'] = array(
+ 'mech' => 'MTrackAuth_OpenID',
+ 'login' => $name,
+ 'email' => $sreg['email'],
+ 'name' => $sreg['fullname'],
+ 'alias' => $id,
+ 'sreg' => $sreg,
+ );
+
+ header("Location: {$ABSWEB}auth/register.php");
exit;
} else {
$message = 'An error occurred while talking to your OpenID provider';
@@ -119,74 +122,6 @@
session_destroy();
header('Location: ' . $ABSWEB);
exit;
-} else if ($pi == 'register') {
-
- if (!isset($_SESSION['openid.id'])) {
- header("Location: " . $ABSWEB);
- exit;
- }
-
- $userid = isset($_REQUEST['nickname']) ? $_REQUEST['nickname'] : '';
- $email = isset($_REQUEST['email']) ? $_REQUEST['email'] : '';
- $message = null;
-
- /* See if we can find a canonical identity for the user */
- foreach (MTrackDB::q('select userid from useraliases where alias = ?',
- $_SESSION['openid.id'])->fetchAll() as $row) {
- header("Location: " . $ABSWEB);
- exit;
- }
-
- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- if (!strlen($userid)) {
- $message = 'You must enter a userid';
- } else {
- /* is the requested id available? */
- $avail = true;
- foreach (MTrackDB::q('select userid from userinfo where userid = ?',
- $userid)->fetchAll() as $row) {
- $avail = false;
- $message = "Your selected user ID is not available";
- }
- if ($avail) {
- MTrackDB::q('insert into userinfo (userid, email, active) values (?, ?, 1)', $userid, $email);
- /* we know the alias doesn't already exist, because we double-checked
- * for it above */
- MTrackDB::q('insert into useraliases (userid, alias) values (?,?)',
- $userid, $_SESSION['openid.id']);
- header("Location: {$ABSWEB}user.php?user=$userid&edit=1");
- exit;
- }
- }
- }
-
- mtrack_head('Register');
-
- $userid = htmlentities($userid, ENT_QUOTES, 'utf-8');
- $email = htmlentities($email, ENT_QUOTES, 'utf-8');
-
- if ($message) {
- $message = htmlentities($message, ENT_QUOTES, 'utf-8');
- echo <<<HTML
-<div class='ui-state-error ui-corner-all'>
- <span class='ui-icon ui-icon-alert'></span>
- $message
-</div>
-HTML;
- }
-
- echo <<<HTML
-<h1>Set up your local account</h1>
-<form method='post'>
- User ID: <input type='text' name='nickname' value='$userid'><br>
- Email: <input type='text' name='email' value='$email'><br>
- <button type='submit'>Save</button>
-</form>
-
-
-HTML;
- mtrack_foot();
- exit;
}

mtrack_head('Authentication Required');


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/auth/register.php
--- /dev/null
+++ b/web/auth/register.php
@@ -0,0 +1,127 @@
+<?php # vim:ts=2:sw=2:et:
+/* For copyright and licensing terms, see the file named LICENSE */
+
+include '../../inc/common.php';
+
+if (MTrackAuth::whoami() != 'anonymous') {
+ header("Location: $ABSWEB");
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST' &&
+ MTrackConfig::get('core', 'allow_self_registration'))
+{
+ if (empty($_POST['id']) || empty($_POST['email'])
+ || empty($_POST['fullname'])) {
+ $message = 'You must complete all of the fields';
+ } else {
+
+ $userid = $_POST['id'];
+ $email = $_POST['email'];
+ $name = $_POST['fullname'];
+
+ /* is the requested id available? */
+ $user = MTrackUser::loadUser($userid);
+ if ($user) {
+ $message = "Your selected user ID is not available";
+ } else {
+ $user = new MTrackUser;
+ $user->userid = $userid;
+ $user->email = $email;
+ $user->fullname = $name;
+ $user->active = true;
+
+ $reg = $_SESSION['mtrack.auth.register'];
+ if (isset($reg['alias'])) {
+ // FIXME: verify that alias doesn't already exist!
+
+ // We need to do this manually, as the User object save
+ // method checks our rights, and we don't have any right now.
+ MTrackDB::q('insert into useraliases (userid, alias) values (?, ?)',
+ $userid, $reg['alias']);
+ }
+
+ $CS = MTrackChangeset::begin("user:$user->userid", "registered");
+ $user->save($CS);
+ $CS->commit();
+
+ /* now; we are logged in as this user; gate into mtrack auth */
+ $_SESSION['auth.mtrack'] = $user->userid;
+ unset($_SESSION['mtrack.auth.register']);
+
+ header("Location: {$ABSWEB}user.php?user=$userid&edit=1");
+ exit;
+ }
+ }
+}
+
+if (!MTrackConfig::get('core', 'allow_self_registration')) {
+ mtrack_head("Registration Denied");
+
+ echo <<<HTML
+<h1>Registration Denied</h1>
+
+<p>
+ Thanks for visiting, but the settings at this site don't allow
+ the public to register for access. If you believe this result
+ to be in error, contact the site administrator.
+</p>
+HTML;
+ mtrack_foot();
+ exit;
+}
+
+mtrack_head('Register');
+
+$reg = $_SESSION['mtrack.auth.register'];
+
+$userid = htmlentities($reg['login'], ENT_QUOTES, 'utf-8');
+$email = htmlentities($reg['email'], ENT_QUOTES, 'utf-8');
+$fullname = htmlentities($reg['name'], ENT_QUOTES, 'utf-8');
+
+if ($message) {
+ $message = htmlentities($message, ENT_QUOTES, 'utf-8');
+ echo <<<HTML
+<div class='alert alert-danger'>
+ <a class='close' data-dismiss='alert'>&times;</a>
+ $message
+</div>
+HTML;
+}
+
+echo <<<HTML
+<h1>Register your local account</h1>
+
+<p>
+ Please fill out this short form so that we can complete your
+ login. The User ID and Full Name you select below will be how your name
+ appears on the site, and the email address will be used to
+ send you notifications.
+</p>
+
+<br>
+
+<form method='post'>
+<table>
+ <tr>
+ <td>User ID</td>
+ <td><input type='text' name='id' value='$userid'>
+ <em>Once selected, it cannot be changed; choose wisely!</em>
+ </td>
+ </tr>
+ <tr>
+ <td>Full Name</td>
+ <td><input type='text' name='fullname' value='$fullname'></td>
+ </tr>
+ <tr>
+ <td>Email</td>
+ <td><input type='text' name='email' value='$email'></td>
+ </tr>
+</table>
+<button type='submit' class='btn btn-primary'>Save</button>
+</form>
+
+
+HTML;
+
+mtrack_foot();


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/images/openid-inputicon.gif
Binary file web/images/openid-inputicon.gif has changed


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/images/openid-providers-en.png
Binary file web/images/openid-providers-en.png has changed


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/js/openid-en.js
--- /dev/null
+++ b/web/js/openid-en.js
@@ -0,0 +1,96 @@
+/*
+ Simple OpenID Plugin
+ http://code.google.com/p/openid-selector/
+
+ This code is licensed under the New BSD License.
+*/
+
+var providers_large = {
+ google : {
+ name : 'Google',
+ url : 'https://www.google.com/accounts/o8/id'
+ },
+ yahoo : {
+ name : 'Yahoo',
+ url : 'http://me.yahoo.com/'
+ },
+ aol : {
+ name : 'AOL',
+ label : 'Enter your AOL screenname.',
+ url : 'http://openid.aol.com/{username}'
+ },
+ myopenid : {
+ name : 'MyOpenID',
+ label : 'Enter your MyOpenID username.',
+ url : 'http://{username}.myopenid.com/'
+ },
+ openid : {
+ name : 'OpenID',
+ label : 'Enter your OpenID.',
+ url : null
+ }
+};
+
+var providers_small = {
+ livejournal : {
+ name : 'LiveJournal',
+ label : 'Enter your Livejournal username.',
+ url : 'http://{username}.livejournal.com/'
+ },
+ /* flickr: {
+ name: 'Flickr',
+ label: 'Enter your Flickr username.',
+ url: 'http://flickr.com/{username}/'
+ }, */
+ /* technorati: {
+ name: 'Technorati',
+ label: 'Enter your Technorati username.',
+ url: 'http://technorati.com/people/technorati/{username}/'
+ }, */
+ wordpress : {
+ name : 'Wordpress',
+ label : 'Enter your Wordpress.com username.',
+ url : 'http://{username}.wordpress.com/'
+ },
+ blogger : {
+ name : 'Blogger',
+ label : 'Your Blogger account',
+ url : 'http://{username}.blogspot.com/'
+ },
+ verisign : {
+ name : 'Verisign',
+ label : 'Your Verisign username',
+ url : 'http://{username}.pip.verisignlabs.com/'
+ },
+ /* vidoop: {
+ name: 'Vidoop',
+ label: 'Your Vidoop username',
+ url: 'http://{username}.myvidoop.com/'
+ }, */
+ /* launchpad: {
+ name: 'Launchpad',
+ label: 'Your Launchpad username',
+ url: 'https://launchpad.net/~{username}'
+ }, */
+ claimid : {
+ name : 'ClaimID',
+ label : 'Your ClaimID username',
+ url : 'http://claimid.com/{username}'
+ },
+ clickpass : {
+ name : 'ClickPass',
+ label : 'Enter your ClickPass username',
+ url : 'http://clickpass.com/public/{username}'
+ },
+ google_profile : {
+ name : 'Google Profile',
+ label : 'Enter your Google Profile username',
+ url : 'http://www.google.com/profiles/{username}'
+ }
+};
+
+openid.locale = 'en';
+openid.sprite = 'en'; // reused in german& japan localization
+openid.demo_text = 'In client demo mode. Normally would have submitted OpenID:';
+openid.signin_text = 'Sign-In';
+openid.image_title = 'log in with {provider}';


diff -r 5616c8c6e4d3ff81d24a224d179435bf15684fd9 -r e9202c26f4e8247b60da31fe8cacc16acd64d29e web/js/openid-jquery.js
--- /dev/null
+++ b/web/js/openid-jquery.js
@@ -0,0 +1,203 @@
+/*
+ Simple OpenID Plugin
+ http://code.google.com/p/openid-selector/
+
+ This code is licensed under the New BSD License.
+*/
+
+var providers;
+var openid;
+(function ($) {
+openid = {
+ version : '1.3', // version constant
+ demo : false,
+ demo_text : null,
+ cookie_expires : 6 * 30, // 6 months.
+ cookie_name : 'openid_provider',
+ cookie_path : '/',
+
+ img_path : 'images/',
+ locale : null, // is set in openid-<locale>.js
+ sprite : null, // usually equals to locale, is set in
+ // openid-<locale>.js
+ signin_text : null, // text on submit button on the form
+ all_small : false, // output large providers w/ small icons
+ no_sprite : false, // don't use sprite image
+ image_title : '{provider}', // for image title
+
+ input_id : null,
+ provider_url : null,
+ provider_id : null,
+
+ /**
+ * Class constructor
+ *
+ * @return {Void}
+ */
+ init : function(input_id) {
+ providers = $.extend({}, providers_large, providers_small);
+ var openid_btns = $('#openid_btns');
+ this.input_id = input_id;
+ $('#openid_choice').show();
+ $('#openid_input_area').empty();
+ var i = 0;
+ // add box for each provider
+ var id, box;
+ for (id in providers_large) {
+ box = this.getBoxHTML(id, providers_large[id], (this.all_small ? 'small' : 'large'), i++);
+ openid_btns.append(box);
+ }
+ if (providers_small) {
+ openid_btns.append('<br/>');
+ for (id in providers_small) {
+ box = this.getBoxHTML(id, providers_small[id], 'small', i++);
+ openid_btns.append(box);
+ }
+ }
+ $('#openid_form').submit(this.submit);
+ var box_id = this.readCookie();
+ if (box_id) {
+ this.signin(box_id, true);
+ }
+ },
+
+ /**
+ * @return {String}
+ */
+ getBoxHTML : function(box_id, provider, box_size, index) {
+ if (this.no_sprite) {
+ var image_ext = box_size == 'small' ? '.ico.gif' : '.gif';
+ return '<a title="' + this.image_title.replace('{provider}', provider.name) + '" href="javascript:openid.signin(\'' + box_id + '\');"'
+ + ' style="background: #FFF url(' + this.img_path + '../images.' + box_size + '/' + box_id + image_ext + ') no-repeat center center" '
+ + 'class="' + box_id + ' openid_' + box_size + '_btn"></a>';
+ }
+ var x = box_size == 'small' ? -index * 24 : -index * 100;
+ var y = box_size == 'small' ? -60 : 0;
+ return '<a title="' + this.image_title.replace('{provider}', provider.name) + '" href="javascript:openid.signin(\'' + box_id + '\');"'
+ + ' style="background: #FFF url(' + this.img_path + 'openid-providers-' + this.sprite + '.png); background-position: ' + x + 'px ' + y + 'px" '
+ + 'class="' + box_id + ' openid_' + box_size + '_btn"></a>';
+ },
+
+ /**
+ * Provider image click
+ *
+ * @return {Void}
+ */
+ signin : function(box_id, onload) {
+ var provider = providers[box_id];
+ if (!provider) {
+ return;
+ }
+ this.highlight(box_id);
+ this.setCookie(box_id);
+ this.provider_id = box_id;
+ this.provider_url = provider.url;
+ // prompt user for input?
+ if (provider.label) {
+ this.useInputBox(provider);
+ } else {
+ $('#openid_input_area').empty();
+ if (!onload) {
+ $('#openid_form').submit();
+ }
+ }
+ },
+
+ /**
+ * Sign-in button click
+ *
+ * @return {Boolean}
+ */
+ submit : function() {
+ var url = openid.provider_url;
+ if (url) {
+ url = url.replace('{username}', $('#openid_username').val());
+ openid.setOpenIdUrl(url);
+ }
+ if (openid.demo) {
+ alert(openid.demo_text + "\r\n" + document.getElementById(openid.input_id).value);
+ return false;
+ }
+ if (url && url.indexOf("javascript:") == 0) {
+ url = url.substr("javascript:".length);
+ eval(url);
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * @return {Void}
+ */
+ setOpenIdUrl : function(url) {
+ var hidden = document.getElementById(this.input_id);
+ if (hidden != null) {
+ hidden.value = url;
+ } else {
+ $('#openid_form').append('<input type="hidden" id="' + this.input_id + '" name="' + this.input_id + '" value="' + url + '"/>');
+ }
+ },
+
+ /**
+ * @return {Void}
+ */
+ highlight : function(box_id) {
+ // remove previous highlight.
+ var highlight = $('#openid_highlight');
+ if (highlight) {
+ highlight.replaceWith($('#openid_highlight a')[0]);
+ }
+ // add new highlight.
+ $('.' + box_id).wrap('<div id="openid_highlight"></div>');
+ },
+
+ setCookie : function(value) {
+ var date = new Date();
+ date.setTime(date.getTime() + (this.cookie_expires * 24 * 60 * 60 * 1000));
+ var expires = "; expires=" + date.toGMTString();
+ document.cookie = this.cookie_name + "=" + value + expires + "; path=" + this.cookie_path;
+ },
+
+ readCookie : function() {
+ var nameEQ = this.cookie_name + "=";
+ var ca = document.cookie.split(';');
+ for ( var i = 0; i < ca.length; i++) {
+ var c = ca[i];
+ while (c.charAt(0) == ' ')
+ c = c.substring(1, c.length);
+ if (c.indexOf(nameEQ) == 0)
+ return c.substring(nameEQ.length, c.length);
+ }
+ return null;
+ },
+
+ /**
+ * @return {Void}
+ */
+ useInputBox : function(provider) {
+ var input_area = $('#openid_input_area');
+ var html = '';
+ var id = 'openid_username';
+ var value = '';
+ var label = provider.label;
+ var style = '';
+ if (label) {
+ html = '<p>' + label + '</p>';
+ }
+ if (provider.name == 'OpenID') {
+ id = this.input_id;
+ value = 'http://';
+ style = 'background: #FFF url(' + this.img_path + 'openid-inputicon.gif) no-repeat scroll 0 50%; padding-left:18px;';
+ }
+ html += '<input id="' + id + '" type="text" style="' + style + '" name="' + id + '" value="' + value + '" />'
+ + '<input id="openid_submit" type="submit" value="' + this.signin_text + '"/>';
+ input_area.empty();
+ input_area.append(html);
+ $('#' + id).focus();
+ },
+
+ setDemoMode : function(demoMode) {
+ this.demo = demoMode;
+ }
+};
+})(jQuery);



https://bitbucket.org/wez/mtrack/changeset/8cbcc39e37a5/
changeset: 8cbcc39e37a5
user: wez
date: 2012-04-30 01:01:26
summary: use the new mtrack auth module instead of http auth when configuring
passwords during setup.

This effectively deprecates using the HTTP auth module for managing
passwords, and makes its primary purpose integration with the web server
authentication configuration.
affected #: 3 files

diff -r e9202c26f4e8247b60da31fe8cacc16acd64d29e -r 8cbcc39e37a5c2357d8b780b2da47a6571abdcee bin/init.php
--- a/bin/init.php
+++ b/bin/init.php
@@ -350,23 +350,6 @@
MTrackDB::$db = null;
MTrackTicket_CustomFields::$me = null;
MTrackConfig::boot();
-function setup_http_auth($passwords)
-{
- $vardir = MTrackConfig::get('core', 'vardir');
- $pfile = "$vardir/http.user";
- $http_auth = new MTrackAuth_HTTP(null, "digest:$pfile");
- foreach ($passwords as $user => $pass) {
- $http_auth->setUserPassword($user, $pass);
- MTrackConfig::set('user_classes', $user, 'admin');
- }
- MTrackConfig::set('plugins', 'MTrackAuth_HTTP',
- "$vardir/http.group, digest:$pfile");
- MTrackConfig::save();
-}
-
-if (count($passwords)) {
- setup_http_auth($passwords);
-}

include dirname(__FILE__) . '/schema-tool.php';

@@ -435,6 +418,19 @@

$CS = MTrackChangeset::begin('~setup~', 'initial setup');

+if (count($passwords)) {
+ foreach ($passwords as $user => $pass) {
+ $U = new MTrackUser;
+ $U->userid = $user;
+ $U->fullname = $user;
+ $U->save($CS);
+ $U->setPassword($pass);
+ MTrackConfig::set('user_classes', $user, 'admin');
+ }
+ MTrackConfig::set('plugins', 'MTrackAuth_MTrack', '');
+ MTrackConfig::save();
+}
+
foreach ($projects as $pname) {
$p = new MTrackProject;
$p->shortname = $pname;


diff -r e9202c26f4e8247b60da31fe8cacc16acd64d29e -r 8cbcc39e37a5c2357d8b780b2da47a6571abdcee inc/Test/init.php
--- a/inc/Test/init.php
+++ b/inc/Test/init.php
@@ -84,7 +84,7 @@
);
if (!strncmp($url, INCUB_URL, strlen(INCUB_URL))) {
$opts[CURLOPT_USERPWD] = 'admin:admin';
- $opts[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST;
+ $opts[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
}

if (is_object($payload) || is_array($payload)) {


diff -r e9202c26f4e8247b60da31fe8cacc16acd64d29e -r 8cbcc39e37a5c2357d8b780b2da47a6571abdcee inc/user.php
--- a/inc/user.php
+++ b/inc/user.php
@@ -14,6 +14,10 @@
public $prefs = null;
private $stored = false;

+ function __construct() {
+ $this->prefs = new stdclass;
+ }
+
static function loadUser($username, $storedOnly = false) {
$username = mtrack_canon_username($username);




https://bitbucket.org/wez/mtrack/changeset/b04c1b04845f/
changeset: b04c1b04845f
user: wez
date: 2012-04-30 01:03:22
summary: merge in git fixups
affected #: 1 file

diff -r 8cbcc39e37a5c2357d8b780b2da47a6571abdcee -r b04c1b04845f3a28a4e25ec7d379bcd8349d521e inc/scm/git.php
--- a/inc/scm/git.php
+++ b/inc/scm/git.php
@@ -95,7 +95,7 @@

function __destruct() {
if ($this->push) {
- echo stream_get_contents($this->git('push'));
+ echo stream_get_contents($this->git('push', 'origin', 'master'));
}
mtrack_rmdir($this->dir);
}
@@ -127,8 +127,7 @@
if (!strlen($reason)) {
$reason = 'Changed';
}
- putenv("GIT_AUTHOR_NAME=$CS->who");
- putenv("GIT_AUTHOR_EMAIL=$CS->who");
+ MTrackSCMGit::setGitEnvironment($CS->who);
stream_get_contents($this->git('commit', '-a',
'-m', $reason
)
@@ -184,16 +183,26 @@
return null;
}

+ /* I've had reports that Git becomes unhappy if it can't find something
+ * that looks like an email address, so try to normalize towards that */
+ static function setGitEnvironment($user) {
+ $userdata = MTrackAuth::getUserData($user);
+ if (preg_match("/@/", $userdata['email'])) {
+ $who = $userdata['email'];
+ } else {
+ $who = "$user@local";
+ }
+ putenv("GIT_AUTHOR_NAME=$who");
+ putenv("GIT_AUTHOR_EMAIL=$who");
+ }
+
public function reconcileRepoSettings(MTrackSCM $r = null) {
if ($r == null) {
$r = $this;
}

if (!is_dir($r->repopath)) {
- $userdata = MTrackAuth::getUserData(MTrackAuth::whoami());
- $who = $userdata['email'];
- putenv("GIT_AUTHOR_NAME=$who");
- putenv("GIT_AUTHOR_EMAIL=$who");
+ self::setGitEnvironment(MTrackAuth::whoami());

if ($r->clonedfrom) {
$S = MTrackRepo::loadById($r->clonedfrom);
@@ -206,21 +215,12 @@
}

} else {
- /* a little peculiar, but bear with it.
- * We need to have a bare repo so that git doesn't mess around
- * trying to deal with a checkout in the repo dir.
- * So we need to create two repos; one bare, one not bare.
- * We populate the non-bare repo with a dummy file just to have
- * something to commit, then push non-bare -> bare, and remove non-bare.
- */
-
$stm = mtrack_run_tool('git', 'read',
array('init', '--bare', $r->repopath));
$out = stream_get_contents($stm);
if (pclose($stm)) {
throw new Exception("git init failed: $out");
}
-
}

$php = MTrackConfig::get('tools', 'php');



https://bitbucket.org/wez/mtrack/changeset/b0b5af18d8ea/
changeset: b0b5af18d8ea
user: wez
date: 2012-04-30 02:19:37
summary: turn on mtrack's own auth by default during installation.

Emit a note about permissions at the end of the init script; maybe
someone will read it and it will save them time when they run into the
permissions issues that they're bound to forget.

Make a noise when trying to save runtime.config; with the new admin
party flow, you're forced to turn it off using the UI, so this improves
error discovery a great deal.
affected #: 3 files

diff -r b04c1b04845f3a28a4e25ec7d379bcd8349d521e -r b0b5af18d8eaeaa22b1c24ebd52457dcba0b0030 bin/init.php
--- a/bin/init.php
+++ b/bin/init.php
@@ -427,9 +427,11 @@
$U->setPassword($pass);
MTrackConfig::set('user_classes', $user, 'admin');
}
- MTrackConfig::set('plugins', 'MTrackAuth_MTrack', '');
- MTrackConfig::save();
}
+// Make our auth the default; folks with custom requirements
+// can deploy their own config.ini that removes this line
+MTrackConfig::set('plugins', 'MTrackAuth_MTrack', '');
+MTrackConfig::save();

foreach ($projects as $pname) {
$p = new MTrackProject;
@@ -562,3 +564,12 @@
$db->commit();
echo "Done\n";
unlink("$vardir/.initializing");
+
+echo <<<TEXT
+
+ ** Important! Make sure that you chown and/or chmod $vardir so that it is
+ ** writable to your web server user
+
+
+TEXT;
+


diff -r b04c1b04845f3a28a4e25ec7d379bcd8349d521e -r b0b5af18d8eaeaa22b1c24ebd52457dcba0b0030 inc/configuration.php
--- a/inc/configuration.php
+++ b/inc/configuration.php
@@ -61,6 +61,9 @@
} else {
$fp = fopen($filename, 'w');
}
+ if (!$fp) {
+ throw new Exception("unable to open $filename for writing! check permissions and ownership!");
+ }
flock($fp, LOCK_EX);
ftruncate($fp, 0);
foreach (self::$runtime as $section => $opts) {


diff -r b04c1b04845f3a28a4e25ec7d379bcd8349d521e -r b0b5af18d8eaeaa22b1c24ebd52457dcba0b0030 web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -57,22 +57,10 @@
$restricted_on =
MTrackConfig::get('user_class_roles', 'anonymous') == '' ? ' checked ' : '';

-// FIXME: want to make this effectively the default without making it
-// impossible to turn off. If we know that HTTP auth is not configured,
-// and we know that local auth is not explicitly disabled in runtime config,
-// we want this to be enabled so that it is saved that way when the form
-// is submitted
-
$local_pw = MTrackAuth::getMech('MTrackAuth_MTrack');
$local_on = '';
if ($local_pw) {
$local_on = ' checked ';
-} else {
- if (!isset(MTrackConfig::$runtime['plugins']['MTrackAuth_MTrack']) &&
- !MTrackAuth::getMech('MTrackAuth_HTTP'))
- {
- $local_on = ' checked ';
- }
}

echo <<<HTML



https://bitbucket.org/wez/mtrack/changeset/73d4f0b422fb/
changeset: 73d4f0b422fb
user: wez
date: 2012-04-30 02:38:33
summary: stronger wording around the http auth module to guide people away from it

Re-cast it as a deferred authentication module, as its other
functionality is handled by the mtrack auth module.
affected #: 2 files

diff -r b0b5af18d8eaeaa22b1c24ebd52457dcba0b0030 -r 73d4f0b422fbb4d8a2e26a576d0c42bb69b86984 web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -75,10 +75,12 @@
<input type="checkbox" $reg_on name="register"> Allow users to register themselves and sign up with an account.
<br><br>
-<input type="checkbox" $local_on name="local"> Use mtrack's own password storage and cookie based authentication (salted SHA-512 passwords). Not compatible with the HTTP Auth module.
+<input type="checkbox" $local_on name="local"> Use mtrack's own password storage and cookie based authentication.<br>
+Uses salted SHA-512 passwords and enables HTTP Basic authentication for REST API access.
<br><br>
-<input type="checkbox" $restricted_on name="restricted"> Restrict all access to authenticated users.
+<input type="checkbox" $restricted_on name="restricted"> Restrict all access to authenticated users.<br>
+Anonymous users have their permissions revoked.
<br><br>

@@ -86,10 +88,6 @@
</form><ul>
- <li><a href="{$ABSWEB}admin/auth/http.php">Configure HTTP Auth</a><br>
- Configure mtrack to delegate authentication to the web server,
- or perform authentication using the HTTP authentication mechanism.
- </li><li><a href="{$ABSWEB}admin/auth/openid.php">Configure OpenID</a><br>
Enable OpenID authentication; allow users to associate OpenID
identifiers with their user accounts.
@@ -102,6 +100,10 @@
Allow users to associate Persona/BrowserID logins with
their user accounts.
</li>
+ <li><a href="{$ABSWEB}admin/auth/http.php">Defer Auth to the Web Server</a><br>
+ Configure mtrack to defer to the web server for its authentication.
+ </li>
+
</ul>




diff -r b0b5af18d8eaeaa22b1c24ebd52457dcba0b0030 -r 73d4f0b422fbb4d8a2e26a576d0c42bb69b86984 web/admin/auth/http.php
--- a/web/admin/auth/http.php
+++ b/web/admin/auth/http.php
@@ -50,21 +50,22 @@
exit;
}

-mtrack_head("HTTP Authentication");
+mtrack_head("Defer Authentication");
mtrack_admin_nav();

echo <<<HTML
-<h1>HTTP Authentication</h1>
+<h1>Defer Authentication</h1><p>
- This page configures the HTTP Authentication plugin. This plugin
- can operate in one of two modes; it can either defer to your web
- server for authentication, or it can implement HTTP Digest Authentication
- using its own locally stored digest authentication file.
+ This page configures the legacy HTTP Authentication plugin. This plugin
+ uses a PHP feature that allows the web server to be configured to handle
+ authentication and to pass the authentication information down to the
+ application.
</p>
-<div class='alert'>
- <b>Warning</b> - Enabling this module prevents using any other forms
- of authentication.
-</div>
+
+<p>
+ This module is not intended to be used in
+ conjunction with other authentication modules.
+</p>
HTML;


@@ -83,14 +84,15 @@

} else {
echo <<<HTML
-<p>
+<br>
+<div class='alert alert-danger'>
mtrack will use its own HTTP authentication and store the password and group
files in the <em>vardir</em>.
-</p><p>
- This configuration is a legacy configuration and it is recommended that
+ <b>This is a legacy configuration</b> and it is recommended that
you consider moving to use the main mtrack authentication system instead.
</p>
+</div>
HTML;
}
$need_pw = get_admins_with_pw_count() == 0;
@@ -127,10 +129,10 @@

echo <<<HTML
<form method='POST'>
- <input type='checkbox' name='http_on' $http_on> Enable HTTP Authentication
+ <input type='checkbox' name='http_on' $http_on> Enable Deferred Authentication
<br><button class='btn btn-primary' type='submit'>
- Save HTTP Authentication Settings</button>
+ Apply Authentication Settings</button></form>
HTML;




https://bitbucket.org/wez/mtrack/changeset/8ba6a7838cc2/
changeset: 8ba6a7838cc2
user: wez
date: 2012-04-30 02:44:33
summary: add the --user-pass option to the usage in the installer script
affected #: 1 file

diff -r 73d4f0b422fbb4d8a2e26a576d0c42bb69b86984 -r 8ba6a7838cc2ba8d0b7faf132ed39d6c64a1d099 bin/init.php
--- a/bin/init.php
+++ b/bin/init.php
@@ -87,9 +87,9 @@
$authorfile = array_shift($argv);
continue;
}
- if ($arg == '--http-user-pass') {
+ if ($arg == '--http-user-pass' || $arg == '--user-pass') {
if (count($argv) < 2) {
- usage("Missing arguments to --http-user-pass");
+ usage("Missing arguments to $arg");
}
$passwords[array_shift($argv)] = array_shift($argv);
continue;
@@ -269,6 +269,8 @@
mtrack only supports SQLite and PostgreSQL in
this version.

+ --user-pass {user} {pw} Create a user record for {user} and set their
+ password to {pw}.

Supported repo types:




https://bitbucket.org/wez/mtrack/changeset/e74a0ff14bfc/
changeset: e74a0ff14bfc
user: wez
date: 2012-04-30 02:52:40
summary: make sure we hide the activity tab body when creating a new record

Otherwise things look very noisy during initial setup: for instance,
my shell login is "wez" and I init the system; I'll have entries
show up in the timeline for me. When I get into the UI, the first
thing I need to do is give "wez" admin rights and set a password.
If the timeline is bleeding through this looks bad...
affected #: 1 file

diff -r 8ba6a7838cc2ba8d0b7faf132ed39d6c64a1d099 -r e74a0ff14bfc888d715a646cd4a15db229dbf054 web/user.php
--- a/web/user.php
+++ b/web/user.php
@@ -127,9 +127,11 @@
<li><a href='#auth-tab'>Password</a></li><% } %></ul>
+ <% if (!$isNew) { %><div id='activity-tab' class='tab'><div id='timeline'></div></div>
+ <% } %><div id='aliases-tab' class='tab'><em>This user is also known by the following identities
when assessing changes in the various repositories</em><br>



https://bitbucket.org/wez/mtrack/changeset/21cf380ce06e/
changeset: 21cf380ce06e
user: wez
date: 2012-04-30 02:53:20
summary: presentation tweak-ups
affected #: 2 files

diff -r e74a0ff14bfc888d715a646cd4a15db229dbf054 -r 21cf380ce06e31ebb2096d0798a3029061728f6d web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -68,7 +68,7 @@

<form method='POST'><input type="checkbox" $party_on name="party"> Enable admin party<br>
-Anyone accessing the system from <b>$party_addrs</b> will be treated as an administrator.
+Anyone accessing the system from <b>$party_addrs</b> will be treated as an administrator.<br>
Everyone else will have read-only access and no self-enrolment will be allowed.
<br><br>


diff -r e74a0ff14bfc888d715a646cd4a15db229dbf054 -r 21cf380ce06e31ebb2096d0798a3029061728f6d web/auth/index.php
--- a/web/auth/index.php
+++ b/web/auth/index.php
@@ -227,10 +227,6 @@
<a href="$url" class="btn">Log In using Facebook</a>
HTML;
}
-echo "<pre>\n";
-var_dump($_SESSION);
-echo "</pre>\n";
-

mtrack_foot();




https://bitbucket.org/wez/mtrack/changeset/c0cbdc4270ab/
changeset: c0cbdc4270ab
user: wez
date: 2012-04-30 02:57:55
summary: don't need special privs to use the ticket search
affected #: 1 file

diff -r 21cf380ce06e31ebb2096d0798a3029061728f6d -r c0cbdc4270ab825ce03d089115411a9ebe5d2cdc web/reports.php
--- a/web/reports.php
+++ b/web/reports.php
@@ -38,11 +38,13 @@
<button class='btn btn-primary' type="submit" name="edit"
onclick="document.location.href = ABSWEB + 'report.php?edit=1'; return false;"
><i class='icon-plus icon-white'></i> Create Report</button>
+<?php
+}
+echo <<<HTML
<button class='btn btn-success' type="submit" name="edit"
onclick="document.location.href = ABSWEB + 'query.php'; return false;"
><i class='icon-white icon-search'></i> Ticket Search</button>
-<?php
-}
+HTML;

mtrack_foot();




https://bitbucket.org/wez/mtrack/changeset/b3690abb95e0/
changeset: b3690abb95e0
user: wez
date: 2012-04-30 03:20:04
summary: Enable self-registration for mtrack local auth
affected #: 2 files

diff -r c0cbdc4270ab825ce03d089115411a9ebe5d2cdc -r b3690abb95e0eb937825ef83d9a8c404f3aa1846 web/auth/index.php
--- a/web/auth/index.php
+++ b/web/auth/index.php
@@ -70,10 +70,15 @@

echo <<<HTML
<button type='submit' class='btn btn-primary'>Log In</button>
+HTML;

-</form>
+ if (MTrackConfig::get('core', 'allow_self_registration')) {
+ echo <<<HTML
+ <a class='btn' href="{$ABSWEB}auth/register.php">Sign Up</a>
+HTML;
+ }

-HTML;
+ echo "</form>";
}

if (MTrackAuth::getMech('MTrackAuth_OpenID')) {


diff -r c0cbdc4270ab825ce03d089115411a9ebe5d2cdc -r b3690abb95e0eb937825ef83d9a8c404f3aa1846 web/auth/register.php
--- a/web/auth/register.php
+++ b/web/auth/register.php
@@ -8,13 +8,31 @@
exit;
}

+function validate_input()
+{
+ if (empty($_POST['id']) || empty($_POST['email'])
+ || empty($_POST['fullname']))
+ {
+ return 'You must complete all of the fields';
+ }
+
+ if (isset($_POST['password']) || isset($_POST['password2'])) {
+ if ($_POST['password'] != $_POST['password2']) {
+ return "Passwords don't match";
+ }
+ if (!strlen($_POST['password'])) {
+ return "Password must not be empty";
+ }
+ }
+
+ return null;
+}
+
if ($_SERVER['REQUEST_METHOD'] == 'POST' &&
MTrackConfig::get('core', 'allow_self_registration'))
{
- if (empty($_POST['id']) || empty($_POST['email'])
- || empty($_POST['fullname'])) {
- $message = 'You must complete all of the fields';
- } else {
+ $message = validate_input();
+ if ($message === null) {

$userid = $_POST['id'];
$email = $_POST['email'];
@@ -33,16 +51,23 @@

$reg = $_SESSION['mtrack.auth.register'];
if (isset($reg['alias'])) {
- // FIXME: verify that alias doesn't already exist!
-
- // We need to do this manually, as the User object save
- // method checks our rights, and we don't have any right now.
- MTrackDB::q('insert into useraliases (userid, alias) values (?, ?)',
- $userid, $reg['alias']);
+ // verify that alias doesn't already exist!
+ $alias = MTrackUser::loadUser($reg['alias'], true);
+ if (!$alias) {
+ // We need to do this manually, as the User object save
+ // method checks our rights, and we don't have any right now.
+ MTrackDB::q('insert into useraliases (userid, alias) values (?, ?)',
+ $userid, $reg['alias']);
+ }
}

$CS = MTrackChangeset::begin("user:$user->userid", "registered");
$user->save($CS);
+
+ if (isset($_POST['password']) && strlen($_POST['password'])) {
+ $user->setPassword($_POST['password']);
+ }
+
$CS->commit();

/* now; we are logged in as this user; gate into mtrack auth */
@@ -73,11 +98,39 @@

mtrack_head('Register');

-$reg = $_SESSION['mtrack.auth.register'];
+if (isset($_POST['id'])) {
+ $userid = htmlentities($_POST['id'], ENT_QUOTES, 'utf-8');
+} else {
+ $userid = null;
+}

-$userid = htmlentities($reg['login'], ENT_QUOTES, 'utf-8');
-$email = htmlentities($reg['email'], ENT_QUOTES, 'utf-8');
-$fullname = htmlentities($reg['name'], ENT_QUOTES, 'utf-8');
+if (isset($_POST['email'])) {
+ $email = htmlentities($_POST['email'], ENT_QUOTES, 'utf-8');
+} else {
+ $email = null;
+}
+
+if (isset($_POST['fullname'])) {
+ $fullname = htmlentities($_POST['fullname'], ENT_QUOTES, 'utf-8');
+} else {
+ $fullname = null;
+}
+
+if (isset($_SESSION['mtrack.auth.register'])) {
+ $reg = $_SESSION['mtrack.auth.register'];
+
+ if (!$userid) {
+ $userid = htmlentities($reg['login'], ENT_QUOTES, 'utf-8');
+ }
+ if (!$email) {
+ $email = htmlentities($reg['email'], ENT_QUOTES, 'utf-8');
+ }
+ if (!$fullname) {
+ $fullname = htmlentities($reg['name'], ENT_QUOTES, 'utf-8');
+ }
+} else {
+ $reg = null;
+}

if ($message) {
$message = htmlentities($message, ENT_QUOTES, 'utf-8');
@@ -117,6 +170,24 @@
<td>Email</td><td><input type='text' name='email' value='$email'></td></tr>
+HTML;
+
+// We only strictly need to show the password box for users
+// that didn't come in via an external authentication mechanism
+if (!$reg) {
+ echo <<<HTML
+ <tr>
+ <td>Password</td>
+ <td><input type='password' name='password'
+ placeholder="Choose a password"><br>
+ <input type='password' name='password2'
+ placeholder="Confirm that password">
+ </td>
+ </tr>
+HTML;
+}
+
+echo <<<HTML
</table><button type='submit' class='btn btn-primary'>Save</button></form>



https://bitbucket.org/wez/mtrack/changeset/c13f2f710c9c/
changeset: c13f2f710c9c
user: wez
date: 2012-04-30 04:10:31
summary: avoid showing Log In when we're logged in
affected #: 4 files

diff -r b3690abb95e0eb937825ef83d9a8c404f3aa1846 -r c13f2f710c9c863a77339678d67ea524e1e32757 inc/auth/browserid.php
--- a/inc/auth/browserid.php
+++ b/inc/auth/browserid.php
@@ -13,6 +13,9 @@
}

function augmentUserInfo(&$content) {
+ if (MTrackAuth::whoami() == 'anonymous' && !$this->authenticate()) {
+ $content = "<a href='$GLOBALS[ABSWEB]auth/'>Log In</a>";
+ }
}
function augmentNavigation($id, &$items) {
}


diff -r b3690abb95e0eb937825ef83d9a8c404f3aa1846 -r c13f2f710c9c863a77339678d67ea524e1e32757 inc/auth/facebook.php
--- a/inc/auth/facebook.php
+++ b/inc/auth/facebook.php
@@ -60,7 +60,7 @@
}

function augmentUserInfo(&$content) {
- if (!$this->authenticate()) {
+ if (MTrackAuth::whoami() == 'anonymous' && !$this->authenticate()) {
$content = "<a href='$GLOBALS[ABSWEB]auth/'>Log In</a>";
}
}


diff -r b3690abb95e0eb937825ef83d9a8c404f3aa1846 -r c13f2f710c9c863a77339678d67ea524e1e32757 inc/auth/mtrack.php
--- a/inc/auth/mtrack.php
+++ b/inc/auth/mtrack.php
@@ -10,7 +10,7 @@
}

function augmentUserInfo(&$content) {
- if (!$this->authenticate()) {
+ if (MTrackAuth::whoami() == 'anonymous' && !$this->authenticate()) {
$content = "<a href='$GLOBALS[ABSWEB]auth/'>Log In</a>";
}
}


diff -r b3690abb95e0eb937825ef83d9a8c404f3aa1846 -r c13f2f710c9c863a77339678d67ea524e1e32757 inc/auth/openid.php
--- a/inc/auth/openid.php
+++ b/inc/auth/openid.php
@@ -9,9 +9,8 @@
}

function augmentUserInfo(&$content) {
- global $ABSWEB;
- if (!isset($_SESSION['openid.id'])) {
- $content = "<a href='{$ABSWEB}auth/'>Log In</a>";
+ if (MTrackAuth::whoami() == 'anonymous' && !$this->authenticate()) {
+ $content = "<a href='$GLOBALS[ABSWEB]auth/'>Log In</a>";
}
}




https://bitbucket.org/wez/mtrack/changeset/90e8ca20b05c/
changeset: 90e8ca20b05c
user: wez
date: 2012-04-30 04:27:07
summary: merge in svn related fixes
affected #: 3 files

diff -r c13f2f710c9c863a77339678d67ea524e1e32757 -r 90e8ca20b05c5a559c5a0b36950e9b401f0b7a1a bin/svn-commit-hook
--- a/bin/svn-commit-hook
+++ b/bin/svn-commit-hook
@@ -24,7 +24,9 @@
}


-class SvnCommitHookBridge implements IMTrackCommitHookBridge {
+class SvnCommitHookBridge implements IMTrackCommitHookBridge,
+ IMTrackCommitHookBridge2
+{
var $repo;
var $svnlook;
var $svnrepo;
@@ -69,6 +71,17 @@
$rev = trim(str_replace('-r ', '', $this->svntxn));
return '[changeset:' . $this->repo->getBrowseRootName() . ",$rev]";
}
+
+ function getChanges() {
+ $c = new MTrackCommitHookChangeEvent;
+ $rev = trim(str_replace('-r ', '', $this->svntxn));
+ $c->hash = $rev;
+ $c->rev = $this->getChangesetDescriptor();
+ $c->changelog = $this->getCommitMessage();
+ $c->changeby = MTrackAuth::whoami();
+ $c->ctime = time();
+ return array($c);
+ }
}

try {


diff -r c13f2f710c9c863a77339678d67ea524e1e32757 -r 90e8ca20b05c5a559c5a0b36950e9b401f0b7a1a inc/timeline.php
--- a/inc/timeline.php
+++ b/inc/timeline.php
@@ -338,12 +338,21 @@
/* doesn't ref a ticket; ensure that we see which
* changeset it came from if it doesn't already
* mention it */
+
+ if ($ent->rev === null) {
+ // Ugh, workaround a bug that recorded the rev
+ // as null in the audit.
+ continue;
+ }
+
$cslink =
"[changeset:" . $R->getBrowseRootName() . ',' .
$ent->rev . ']';
if (strpos($row['reason'], $cslink) === false) {
- $add = ' ' . $cslink . ' ' . $ent->changelog;
- $row['reason'] .= $add;
+ $row['reason'] .= " (In $cslink)";
+ }
+ if (strpos($row['reason'], trim($ent->changelog)) === false) {
+ $row['reason'] .= ' ' . $ent->changelog;
}
}
}


diff -r c13f2f710c9c863a77339678d67ea524e1e32757 -r 90e8ca20b05c5a559c5a0b36950e9b401f0b7a1a inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -1110,7 +1110,9 @@
$anchor = $abase . '.' . $nlines;
$row .= "<td class='linelink'><a name='$anchor'></a><a href='#$anchor' title='link to this line'>#</a></td>";

- $line = htmlspecialchars($line, ENT_QUOTES, 'utf-8');
+ // deliberately don't inform it of the charset; we have no idea and we
+ // only really care about the obvious HTML metacharacters, not the entities
+ $line = htmlspecialchars($line);
$row .= "<td class='line' width='100%'>$line</td></tr>\n";
$html .= $row;




https://bitbucket.org/wez/mtrack/changeset/8c08655f75fe/
changeset: 8c08655f75fe
user: wez
date: 2012-04-30 04:59:24
summary: be more consistent about enacting logout
affected #: 4 files

diff -r 90e8ca20b05c5a559c5a0b36950e9b401f0b7a1a -r 8c08655f75fea5d3c3095ea3aa9ae3057bdf3166 inc/auth/browserid.php
--- a/inc/auth/browserid.php
+++ b/inc/auth/browserid.php
@@ -64,11 +64,13 @@
}

function LogOut() {
- session_start();
- if (isset($_SESSION['auth.browserid'])) {
- session_destroy();
- header('Location: ' . $GLOBALS['ABSWEB']);
- exit;
+ if (isset($_COOKIE[session_name()])) {
+ if (!session_id()) session_start();
+ if (isset($_SESSION['auth.browserid'])) {
+ session_destroy();
+ header('Location: ' . $GLOBALS['ABSWEB']);
+ exit;
+ }
}
}
}


diff -r 90e8ca20b05c5a559c5a0b36950e9b401f0b7a1a -r 8c08655f75fea5d3c3095ea3aa9ae3057bdf3166 inc/auth/facebook.php
--- a/inc/auth/facebook.php
+++ b/inc/auth/facebook.php
@@ -111,14 +111,16 @@
}

function LogOut() {
- session_start();
- if (isset($_SESSION['auth.facebook'])) {
- session_destroy();
- $fb = $this->getFB();
- header('Location: ' . $fb->getLogoutUrl(array(
- 'next' => $GLOBALS['ABSWEB']
- )));
- exit;
+ if (isset($_COOKIE[session_name()])) {
+ if (!session_id()) session_start();
+ if (isset($_SESSION['auth.facebook'])) {
+ session_destroy();
+ $fb = $this->getFB();
+ header('Location: ' . $fb->getLogoutUrl(array(
+ 'next' => $GLOBALS['ABSWEB']
+ )));
+ exit;
+ }
}
}
}


diff -r 90e8ca20b05c5a559c5a0b36950e9b401f0b7a1a -r 8c08655f75fea5d3c3095ea3aa9ae3057bdf3166 inc/auth/mtrack.php
--- a/inc/auth/mtrack.php
+++ b/inc/auth/mtrack.php
@@ -108,7 +108,7 @@

function LogOut() {
if (isset($_COOKIE[session_name()])) {
- session_start();
+ if (!session_id()) session_start();
if (isset($_SESSION['auth.mtrack'])) {
session_destroy();
header('Location: ' . $GLOBALS['ABSWEB']);


diff -r 90e8ca20b05c5a559c5a0b36950e9b401f0b7a1a -r 8c08655f75fea5d3c3095ea3aa9ae3057bdf3166 inc/auth/openid.php
--- a/inc/auth/openid.php
+++ b/inc/auth/openid.php
@@ -61,11 +61,13 @@
}

function LogOut() {
- session_start();
- if (isset($_SESSION['openid.userid'])) {
- session_destroy();
- header('Location: ' . $GLOBALS['ABSWEB']);
- exit;
+ if (isset($_COOKIE[session_name()])) {
+ if (!session_id()) session_start();
+ if (isset($_SESSION['openid.userid'])) {
+ session_destroy();
+ header('Location: ' . $GLOBALS['ABSWEB']);
+ exit;
+ }
}
}
}



https://bitbucket.org/wez/mtrack/changeset/82d9738737f7/
changeset: 82d9738737f7
user: wez
date: 2012-04-30 05:00:45
summary: don't stick on the logout page
affected #: 1 file

diff -r 8c08655f75fea5d3c3095ea3aa9ae3057bdf3166 -r 82d9738737f7f8cabf96e01711cc5fafa4525941 web/logout.php
--- a/web/logout.php
+++ b/web/logout.php
@@ -2,3 +2,4 @@
/* For licensing and copyright terms, see the file named LICENSE */
include '../inc/common.php';
MTrackAuth::LogOut();
+header("Location: $GLOBALS[ABSWEB]");



https://bitbucket.org/wez/mtrack/changeset/f594d479ffcd/
changeset: f594d479ffcd
user: wez
date: 2012-04-30 05:25:03
summary: refresh reCAPTCHA support (apparently they got bought by google since I
last looked) and hook it up to the user registration pages.
affected #: 6 files

diff -r 82d9738737f7f8cabf96e01711cc5fafa4525941 -r f594d479ffcdd16c10e6e74e95e2a032efe94f3f defaults/help/plugin/Recaptcha.md
--- a/defaults/help/plugin/Recaptcha.md
+++ b/defaults/help/plugin/Recaptcha.md
@@ -15,7 +15,7 @@
```

The first parameter is your publickey key and the second is your privatekey.
-You can obtain keys from [http://recaptcha.net/api/getkey?app=mtrack recaptcha.net].
+You can obtain keys from [https://www.google.com/recaptcha/admin/create recaptcha.net].

The userclass parameter indicates which classes (separated by a pipe character)
of user should have the captcha applied. The default value is


diff -r 82d9738737f7f8cabf96e01711cc5fafa4525941 -r f594d479ffcdd16c10e6e74e95e2a032efe94f3f inc/captcha.php
--- a/inc/captcha.php
+++ b/inc/captcha.php
@@ -55,10 +55,13 @@
}
$pub = $this->pub;
$err = $this->errcode === null ? '' : "&error=$this->errcode";
+ $http = $_SERVER['HTTPS'] ? 'https' : 'http';
return <<<HTML
-<script type='text/javascript' src="https://api-secure.recaptcha.net/challenge?k=$pub$err"></script>
+<script type='text/javascript'
+ src="$http://www.google.com/recaptcha/api/challenge?k=$pub$err">
+</script><noscript>
- <iframe src="https://api-secure.recaptcha.net/noscript?k=$pub$err"
+ <iframe src="$http://www.google.com/recaptcha/api/noscript?k=$pub$err"
height="300" width="500" frameborder="0"></iframe><br/><textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
@@ -97,14 +100,14 @@
* second line: error code
*/
$res = array();
- foreach (file('http://api-verify.recaptcha.net/verify', 0, $ctx) as $line) {
+ foreach (file('http://www.google.com/recaptcha/api/verify', 0, $ctx) as $line) {
$res[] = trim($line);
}
if ($res[0] == 'true') {
return true;
}
$this->errcode = $res[1];
- return false;
+ return array(false, $this->errcode);
}

}


diff -r 82d9738737f7f8cabf96e01711cc5fafa4525941 -r f594d479ffcdd16c10e6e74e95e2a032efe94f3f web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -88,6 +88,9 @@
</form><ul>
+ <li><a href="{$ABSWEB}admin/auth/captcha.php">Configure CAPTCHA</a><br>
+ Add bot protection to the registration form.
+ </li><li><a href="{$ABSWEB}admin/auth/openid.php">Configure OpenID</a><br>
Enable OpenID authentication; allow users to associate OpenID
identifiers with their user accounts.


diff -r 82d9738737f7f8cabf96e01711cc5fafa4525941 -r f594d479ffcdd16c10e6e74e95e2a032efe94f3f web/admin/auth/captcha.php
--- /dev/null
+++ b/web/admin/auth/captcha.php
@@ -0,0 +1,90 @@
+<?php # vim:ts=2:sw=2:et:
+/* For copyright and licensing terms, see the file named LICENSE */
+
+include '../../../inc/common.php';
+
+$plugins = MTrackConfig::getSection('plugins');
+
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ if (isset($_POST['captcha_on']) && $_POST['captcha_on'] == 'on') {
+ /* turn it on */
+ $pub = $_POST['pub'];
+ $priv = $_POST['priv'];
+ if ($pub && $priv) {
+ MTrackConfig::set('plugins', 'MTrackCaptcha_Recaptcha', "$pub,$priv");
+ }
+ } else {
+ /* turn it off */
+ MTrackConfig::remove('plugins', 'MTrackCaptcha_Recaptcha');
+ }
+
+ MTrackConfig::save();
+ header("Location: {$ABSWEB}admin/auth.php");
+ exit;
+}
+
+mtrack_head("Administration - reCAPTCHA");
+mtrack_admin_nav();
+
+$impl = MTrackCaptcha::$impl;
+
+if ($impl && $impl instanceof MTrackCaptcha_Recaptcha) {
+ $on = ' checked ';
+ $pub = htmlentities($impl->pub, ENT_QUOTES, 'utf-8');
+ $priv = htmlentities($impl->priv, ENT_QUOTES, 'utf-8');
+} else {
+ $on = '';
+ $pub = '';
+ $priv = '';
+}
+
+$dom = urlencode(htmlentities($_SERVER['SERVER_NAME'], ENT_QUOTES, 'utf-8'));
+
+echo <<<HTML
+<h1>reCAPTCHA</h1>
+<p>
+reCAPTCHA is a free <a href="http://www.captcha.net/">CAPTCHA</a>
+service that helps to digitize books, newspapers
+and old time radio shows.
+</p>
+<p>
+To use reCAPTCHA, you need to obtain an pair of API keys; you can do so from
+<a href="https://www.google.com/recaptcha/admin/create?domains=$dom&amp;app=mtrack"
+ >the recaptcha admin site</a>.
+</p>
+<p>
+At this time, CAPTCHA's are only used for the user registration pages;
+this is the gate for untrusted and unauthenticated users. Once a user
+has successfully registered, we assume that they are not a bot.
+</p>
+<br>
+
+<form method='POST'>
+ <table>
+ <tr>
+ <td>Site Domain</td>
+ <td><tt>$dom</tt></td>
+ </tr>
+ <tr>
+ <td><label for="pub">Public Key</label></td>
+ <td><input type="text" name="pub"
+ value="$pub" size="64"
+ placeholder="Enter your Public Key"></td>
+ </tr>
+ <tr>
+ <td><label for="priv">Private Key</label></td>
+ <td><input type="text" name="priv"
+ value="$priv" size="64"
+ placeholder="Enter your Private Key"></td>
+ </tr>
+ <table>
+ <input type='checkbox' name='captcha_on' $on> Enable reCAPTCHA
+ <br>
+ <button class='btn btn-primary' type='submit'>
+ Save Settings</button>
+</form>
+HTML;
+
+mtrack_foot();
+
+


diff -r 82d9738737f7f8cabf96e01711cc5fafa4525941 -r f594d479ffcdd16c10e6e74e95e2a032efe94f3f web/auth/index.php
--- a/web/auth/index.php
+++ b/web/auth/index.php
@@ -3,6 +3,8 @@

include '../../inc/common.php';

+$fail = null;
+
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (MTrackAuth::getMech('MTrackAuth_MTrack')) {
/* authenticate against our password database */


diff -r 82d9738737f7f8cabf96e01711cc5fafa4525941 -r f594d479ffcdd16c10e6e74e95e2a032efe94f3f web/auth/register.php
--- a/web/auth/register.php
+++ b/web/auth/register.php
@@ -10,6 +10,10 @@

function validate_input()
{
+ $cap = MTrackCaptcha::check('');
+ if (is_array($cap) && $cap[0] === false) {
+ return "Captcha validation failed: " . $cap[1];
+ }
if (empty($_POST['id']) || empty($_POST['email'])
|| empty($_POST['fullname']))
{
@@ -28,6 +32,7 @@
return null;
}

+$message = null;
if ($_SERVER['REQUEST_METHOD'] == 'POST' &&
MTrackConfig::get('core', 'allow_self_registration'))
{
@@ -186,9 +191,11 @@
</tr>
HTML;
}
+echo "</table>";
+
+echo MTrackCaptcha::emit('');

echo <<<HTML
-</table><button type='submit' class='btn btn-primary'>Save</button></form>




https://bitbucket.org/wez/mtrack/changeset/628b0ebfbe04/
changeset: 628b0ebfbe04
user: wez
date: 2012-04-30 05:31:13
summary: suggest captcha when turning on self-registration
affected #: 1 file

diff -r f594d479ffcdd16c10e6e74e95e2a032efe94f3f -r 628b0ebfbe042018744441ddfcc81cc7e564a6b7 web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -72,7 +72,8 @@
Everyone else will have read-only access and no self-enrolment will be allowed.
<br><br>
-<input type="checkbox" $reg_on name="register"> Allow users to register themselves and sign up with an account.
+<input type="checkbox" $reg_on name="register"> Allow users to register themselves and sign up with an account.<br>
+You may also want to enable <a href="{$ABSWEB}admin/auth/captcha.php">CAPTCHA</a>.
<br><br><input type="checkbox" $local_on name="local"> Use mtrack's own password storage and cookie based authentication.<br>



https://bitbucket.org/wez/mtrack/changeset/df32b68f7c5b/
changeset: df32b68f7c5b
user: wez
date: 2012-04-30 05:35:04
summary: improve the login page appearance slightly.

Still looks very busy when all the options are turned on, but that's
a hard problem of its own.
affected #: 1 file

diff -r 628b0ebfbe042018744441ddfcc81cc7e564a6b7 -r df32b68f7c5b702dbc9c45cd87df72dc36c005c5 web/auth/index.php
--- a/web/auth/index.php
+++ b/web/auth/index.php
@@ -36,6 +36,7 @@
// MTrack local passwords
if (MTrackAuth::getMech('MTrackAuth_MTrack')) {
echo <<<HTML
+<p>Log-in using your mtrack username and password</p><form method="POST"><table>
@@ -160,6 +161,7 @@
width: 20em;
}
</style>
+<br><form method="POST" action="{$ABSWEB}auth/openid.php" id="openid_form"><div id="openid_choice"><p>Please click your account provider:</p>
@@ -218,7 +220,9 @@
//navigator.id.get(browserid_got_assertion, {silent:true});
});
</script>
+<br><a href='javascript:browserid_login()' class='btn'>Log In using BrowserID</a>
+<br>
HTML;

}
@@ -231,7 +235,9 @@
));

echo <<<HTML
+<br><a href="$url" class="btn">Log In using Facebook</a>
+<br>
HTML;
}




https://bitbucket.org/wez/mtrack/changeset/3807c687636a/
changeset: 3807c687636a
user: wez
date: 2012-04-30 05:46:47
summary: minimum version of mercurial we support is 1.5.2; needed for xml
template output.
affected #: 1 file

diff -r df32b68f7c5b702dbc9c45cd87df72dc36c005c5 -r 3807c687636a5a4875f44b2d4db0873da5179057 defaults/help/Install.md
--- a/defaults/help/Install.md
+++ b/defaults/help/Install.md
@@ -10,6 +10,7 @@
* The `diff` and `diff3` command line tools
* Subversion command line tools (`svn` and `svnlook`) for Subversion repo support
* Mercurial command line tools (`hg`) for Mercurial repo support
+ (Minimum version: 1.5.2)
* Access to the *cron* mechanism or equivalent on your system to schedule background tasks
* The `sendmail` command line tool for change notification emails




https://bitbucket.org/wez/mtrack/changeset/d65ebf6132d2/
changeset: d65ebf6132d2
user: wez
date: 2012-04-30 06:06:32
summary: correctly wire up facebook auth into registration flow
affected #: 2 files

diff -r 3807c687636a5a4875f44b2d4db0873da5179057 -r d65ebf6132d25e21786f4e2cbb10aebfedb5bb0f inc/auth/facebook.php
--- a/inc/auth/facebook.php
+++ b/inc/auth/facebook.php
@@ -69,11 +69,12 @@
}

function authenticate() {
- $me = $this->getMe();
- if ($me) {
- return mtrack_canon_username($me['email']);
+ if (!strlen(session_id()) && php_sapi_name() != 'cli') {
+ session_start();
}
-
+ if (isset($_SESSION['auth.facebook.userid'])) {
+ return $_SESSION['auth.facebook.userid'];
+ }
return null;
}



diff -r 3807c687636a5a4875f44b2d4db0873da5179057 -r d65ebf6132d25e21786f4e2cbb10aebfedb5bb0f web/auth/facebook.php
--- a/web/auth/facebook.php
+++ b/web/auth/facebook.php
@@ -9,5 +9,27 @@

MTrackAuth::whoami();

+$FB = MTrackAuth::getMech('MTrackAuth_Facebook');
+if ($FB) {
+ $me = $FB->getMe();
+ if (!isset($_SESSION['auth.facebook.userid'])) {
+ $user = MTrackUser::loadUser($me['email'], true);
+ if (!$user) {
+ $_SESSION['mtrack.auth.register'] = array(
+ 'mech' => 'MTrackAuth_Facebook',
+ 'login' => $me['username'],
+ 'email' => $me['email'],
+ 'name' => $me['name'],
+ 'sreg' => $me
+ );
+ session_write_close();
+ header("Location: {$ABSWEB}auth/register.php");
+ exit;
+ }
+ $_SESSION['auth.facebook.userid'] = $user->userid;
+ session_write_close();
+ }
+}
+
header('Location: ' . $ABSWEB);




https://bitbucket.org/wez/mtrack/changeset/67dab8a56fd8/
changeset: 67dab8a56fd8
user: wez
date: 2012-05-01 02:45:13
summary: add mercurial minimum version check
affected #: 1 file

diff -r d65ebf6132d25e21786f4e2cbb10aebfedb5bb0f -r 67dab8a56fd8b00ff77a91a1289083ff3d85e87c bin/init.php
--- a/bin/init.php
+++ b/bin/init.php
@@ -295,6 +295,29 @@
chmod("$vardir/attach", 02777);
}

+function check_working_tool(&$tools, $toolname, $path)
+{
+ if ($toolname == 'hg') {
+ // Check for working mercurial version
+ $ver = shell_exec(escapeshellarg($path) . " version");
+ if (preg_match("/\(version ([^)]+)\)/", $ver, $M)) {
+ $version = $M[1];
+ } else {
+ echo "Could not determine mercurial version number:\n$path\n$ver\n";
+ echo "I need at least version 1.5.2\n";
+ return false;
+ }
+ if (version_compare($version, "1.5.2", "<")) {
+ echo "Mimimum supported mercurial is version 1.5.2, you have $version\n";
+ return false;
+ }
+ }
+
+
+ $tools[$toolname] = $path;
+ return true;
+}
+
putenv("MTRACK_CONFIG_FILE=" . $config_file_name);
if (!file_exists($config_file_name)) {
/* create a new config file */
@@ -325,18 +348,26 @@
/* find reasonable defaults for tools */
$tools = array();
foreach ($tools_to_find as $toolname) {
+ $found = false;
foreach (explode(PATH_SEPARATOR, getenv('PATH')) as $pdir) {
if (DIRECTORY_SEPARATOR == '\\' &&
file_exists($pdir . DIRECTORY_SEPARATOR . $toolname . '.exe')) {
- $tools[$toolname] = $pdir . DIRECTORY_SEPARATOR . $toolname . '.exe';
- break;
+ $found = true;
+ if (check_working_tool($tools, $toolname,
+ $pdir . DIRECTORY_SEPARATOR . $toolname . '.exe')) {
+ break;
+ }
} else if (file_exists($pdir . DIRECTORY_SEPARATOR . $toolname)) {
- $tools[$toolname] = $pdir . DIRECTORY_SEPARATOR . $toolname;
- break;
+ $found = true;
+ if (check_working_tool($tools, $toolname,
+ $pdir . DIRECTORY_SEPARATOR . $toolname)) {
+ break;
+ }
}
}
- if (!isset($tools[$toolname])) {
- // let the system find it in the path at runtime
+ if (!$found && !isset($tools[$toolname])) {
+ // let the system find it in the path at runtime, but only
+ // if we didn't find it above and decide that it was broken
$tools[$toolname] = $toolname;
}
}



https://bitbucket.org/wez/mtrack/changeset/a1bf93b79ba6/
changeset: a1bf93b79ba6
user: wez
date: 2012-05-01 03:01:57
summary: add permissions warning when admin party is enabled
affected #: 2 files

diff -r 67dab8a56fd8b00ff77a91a1289083ff3d85e87c -r a1bf93b79ba6ccf15cfb01e9139318bbe2a89bd0 inc/configuration.php
--- a/inc/configuration.php
+++ b/inc/configuration.php
@@ -13,6 +13,15 @@
return $location;
}

+ static function getRuntimeConfigPath() {
+ /* locate the runtime editable config data */
+ $filename = self::_get('core', 'runtime.config');
+ if (!$filename) {
+ $filename = self::_get('core', 'vardir') . '/runtime.config';
+ }
+ return $filename;
+ }
+
static function parseIni() {
if (self::$ini !== null) {
return self::$ini;
@@ -24,10 +33,7 @@
}

/* locate the runtime editable config data */
- $filename = self::_get('core', 'runtime.config');
- if (!$filename) {
- $filename = self::_get('core', 'vardir') . '/runtime.config';
- }
+ $filename = self::getRuntimeConfigPath();
if (file_exists($filename)) {
$fp = fopen($filename, 'r');
flock($fp, LOCK_SH);
@@ -52,10 +58,7 @@
}

static function save() {
- $filename = self::_get('core', 'runtime.config');
- if (!$filename) {
- $filename = self::_get('core', 'vardir') . '/runtime.config';
- }
+ $filename = self::getRuntimeConfigPath();
if (file_exists($filename)) {
$fp = fopen($filename, 'r+');
} else {


diff -r 67dab8a56fd8b00ff77a91a1289083ff3d85e87c -r a1bf93b79ba6ccf15cfb01e9139318bbe2a89bd0 inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -214,6 +214,12 @@
if ($_SERVER['SCRIPT_FILENAME'] !=
dirname(dirname(__FILE__)) . '/web/admin/auth.php') {

+ $perms = '';
+ $path = MTrackConfig::getRuntimeConfigPath();
+ if (!is_writable($path)) {
+ $perms = "<br><p><b>$path</b> is NOT writable; please ensure that the permissions on your vardir are correct!</p>";
+ }
+
echo <<<HTML
<div class='alert alert-error'><a class='close' data-dismiss='alert'>&times;</a>
@@ -224,6 +230,8 @@
is treated as having admin rights (that includes you, and this
is why you are seeing this message). All other users are denied
access.</p>
+ $perms
+ <br><p><a class='btn btn-danger' href="{$ABSWEB}admin/auth.php">Click here to Configure Authentication</a></p></div>
HTML;



https://bitbucket.org/wez/mtrack/changeset/f1500affcceb/
changeset: f1500affcceb
user: wez
date: 2012-05-04 06:19:48
summary: some adjustments ready for the new mtrack web page
affected #: 3 files

diff -r a1bf93b79ba6ccf15cfb01e9139318bbe2a89bd0 -r f1500affcceb986eda40cf7269c25b60f2d76b27 bin/render-markdown
--- /dev/null
+++ b/bin/render-markdown
@@ -0,0 +1,17 @@
+<?php # vim:ts=2:sw=2:et:ft=php:
+/* For copyright and licensing terms, see the file named LICENSE */
+
+# This script is a utility to help me generate the mtrack home page.
+# It is not used by mtrack itself.
+
+if (function_exists('date_default_timezone_set')) {
+ date_default_timezone_set('UTC');
+}
+
+include dirname(__FILE__) . '/../inc/common.php';
+$ABSWEB = '';
+
+$data = stream_get_contents(STDIN);
+
+echo MTrackWiki::format_to_html($data);
+


diff -r a1bf93b79ba6ccf15cfb01e9139318bbe2a89bd0 -r f1500affcceb986eda40cf7269c25b60f2d76b27 defaults/help/ConfigIni.md
--- a/defaults/help/ConfigIni.md
+++ b/defaults/help/ConfigIni.md
@@ -1,79 +1,78 @@
-```trac
-= config.ini =
+# config.ini

The configuration file defines an mtrack instance. mtrack will look for this
-file by first inspecting the {{{$MTRACK_CONFIG_FILE}}} environmental variable.
-If it is not set it will default to looking for {{{config.ini}}} in the mtrack
+file by first inspecting the `$MTRACK_CONFIG_FILE` environmental variable.
+If it is not set it will default to looking for `config.ini` in the mtrack
source directory.

-{{{config.ini}}} is parsed using the following rule:
+`config.ini` is parsed using the following rule:

- * {{{[name]}}} indicates that the following values belong to the ''name'' section. You may switch section multiple times in the file if you wish.
- * Lines beginning with a semicolon {{{;}}} character are comments and are ignored by the parser
- * values are specified by lines of the form {{{name = value}}}. The value belongs to the previously indicated section.
+ * `[name]` indicates that the following values belong to the ''name'' section. You may switch section multiple times in the file if you wish.
+ * Lines beginning with a semicolon `;` character are comments and are ignored by the parser
+ * values are specified by lines of the form `name = value`. The value belongs to the previously indicated section.
* Unquoted tokens on the right hand size of an equals sign are replaced by the value of a matching PHP constant
- * Values of the form {{{${name}}}} are substituted with the value of the corresponding PHP configuration directive, or if none is found, the corresponding environmental variable value
- * Values of the form {{{@{section:myname}}}} are substituted with the value of the option defined in this configuration file. For example, the ''myname'' value in the ''section'' section)
+ * Values of the form `${name`} are substituted with the value of the corresponding PHP configuration directive, or if none is found, the corresponding environmental variable value
+ * Values of the form `@{section:myname`} are substituted with the value of the option defined in this configuration file. For example, the ''myname'' value in the ''section'' section)

-== [core] == #core
+# [core]

-The following options are defined for the {{{core}}} section.
+The following options are defined for the `core` section.

- vardir::
- The location of the {{{var}}} directory, which holds all of the mtrack
+vardir
+: The location of the `var` directory, which holds all of the mtrack
runtime state

- dblocation::
- Where the mtrack sqlite database can be found. This is usually defined
- to be {{{"@{core:vardir}/mtrac.db"}}} which means that it lives in the
- {{{var}}} directory.
+dblocation
+: Where the mtrack sqlite database can be found. This is usually defined
+ to be `"@{core:vardir}/mtrac.db"` which means that it lives in the
+ `var` directory.

- searchdb::
- Where the mtrack full-text search database can be found. This is usually
- defined to be {{{"@{core:vardir}/search.db"}}} which means that it lives in
- the {{{var}}} directory.
+searchdb
+: Where the mtrack full-text search database can be found. This is usually
+ defined to be `"@{core:vardir}/search.db"` which means that it lives in
+ the `var` directory.

- projectname::
- The name of the mtrack instance. This is displayed in the top left of
+projectname
+: The name of the mtrack instance. This is displayed in the top left of
the navigation area if the ''projectlogo'' is not defined.

- timezone::
- The default timezone to use when rendering dates.
+timezone
+: The default timezone to use when rendering dates.

- projectlogo::
- Specifies an URL that will be used in an image tag displayed in the top
+projectlogo
+: Specifies an URL that will be used in an image tag displayed in the top
left of the navigation area.

- weburl::
- Specifies the canonical URL (including trailing slash) for this mtrack
+weburl
+: Specifies the canonical URL (including trailing slash) for this mtrack
instance. This is used when generating links in notification email,
but will also be used when generating links in the web application.

- default.repo::
- Specifies the shortname of the repo to use when generating changeset
+default.repo
+: Specifies the shortname of the repo to use when generating changeset
links that don't otherwise specify one. You only need this when you
have multiple repos. mtrack will default to the first repo.

- default_email_domain::
- Domain name to use when inferring the email address for users that do
+default_email_domain
+: Domain name to use when inferring the email address for users that do
not have an email address configured in the userinfo table.

- includes::
- Comma separated list of files to be included. The intended use is for
+includes
+: Comma separated list of files to be included. The intended use is for
loading plugins without modifying the mtrack code.

-== [ticket] == #ticket
+# [ticket]

- default.classification::
- When creating a new ticket, specifies which classification to pre-select
+default.classification
+: When creating a new ticket, specifies which classification to pre-select

- default.severity::
- When creating a new ticket, specifies which severity to pre-select
+default.severity
+: When creating a new ticket, specifies which severity to pre-select

- default.priority::
- When creating a new ticket, specifies which priority to pre-select
+default.priority
+: When creating a new ticket, specifies which priority to pre-select

-== [user_class_roles] == #user_class_roles
+# [user_class_roles]

This section allows you to define classes of users. Unauthenticated users are
placed in the ''anonymous'' user class. Authenticated users are placed in the
@@ -84,30 +83,30 @@

The default configuration for this section is reproduced below:

-{{{
+```ini
; Defines some basic, reasonable, permission sets for 3 classes of user.
; These are used in addition to whatever is selected by auth plugins
[user_class_roles]
anonymous = ReportViewer,BrowserViewer,WikiViewer,TimelineViewer,RoadmapViewer,TicketViewer
authenticated = ReportViewer,BrowserViewer,WikiCreator,TimelineViewer,RoadmapViewer,TicketCreator
admin = ReportCreator,BrowserCreator,WikiCreator,TimelineViewer,RoadmapCreator,TicketCreator,EnumerationCreator,ComponentCreator,ProjectCreator
-}}}
+```

This give anonymous users read-only access to the major areas of mtrack.
Authenticated users are given write access to the major areas.

This also defines a class called ''admin'' that has full access to all areas of mtrack.

-== [user_classes] == #user_classes
+## [user_classes]

This names in this section correspond to user names. The value is the user class that is explicitly assigned to that user.

For example:

-{{{
+```ini
[user_classes]
wez = admin
-}}}
+```

places the ''wez'' user in to the ''admin'' user class. When combined with the
above [help:ConfigIni#user_class_roles user_class_roles] causes ''wez'' to
@@ -117,7 +116,7 @@
Configuring user_classes is not necessary if you are using an authentication
scheme where you control which groups are assigned to the users.

-== [tools] == #tools
+## [tools]

The tools section controls where mtrack finds the various command line tools
that it may need to run.
@@ -128,7 +127,7 @@
unless you have an alternate version of a given that is not in a standard
location.

-== ![nav:mainnav] == #nav:mainnav
+## \[nav:mainnav]

If you want to turn off, rename or add navigation links you can do so by
making changes to this section.
@@ -138,46 +137,48 @@

To remove the wiki link from navigation:

-{{{
+```ini
[nav:mainnav]
/wiki.php =
-}}}
+```

To rename the wiki link:

-{{{
+```ini
[nav:mainnav]
/wiki.php = Awesome Wiki
-}}}
+```

To add a new navigation item:

-{{{
+```ini
[nav:mainnav]
http://bitbucket.org/wez/mtrack/ = mtrack home
-}}}
+```

-== [plugins] == #plugins
+## [plugins]

mtrack has a simple plugin system. After a plugin has been installed, it needs
to be configured by adding an entry to this section of the configuration file.

-The names in this section correspond to the names of the plugin classes. The value is interpreted as a comma separated list of strings that will be passed as arguments to the constructor of that class.
+The names in this section correspond to the names of the plugin classes.
+The value is interpreted as a comma separated list of strings that will
+be passed as arguments to the constructor of that class.

For example:

-{{{
+```ini
[plugins]
MTrackAuth_HTTP = /Users/wez/Sites/svn.htgroup, /Users/wez/Sites/svn.htpasswd
-}}}
+```

this will cause mtrack to run the equivalent of the following php code:

-{{{
+```php
+<?php
$obj = new MTrackAuth_HTTP(
'/Users/wez/Sites/svn.htgroup',
'/Users/wez/Sites/svn.htpasswd');
-}}}
+```

For more information about plugins, see [help:Plugins].
-```


diff -r a1bf93b79ba6ccf15cfb01e9139318bbe2a89bd0 -r f1500affcceb986eda40cf7269c25b60f2d76b27 defaults/help/bin/Modify.md
--- a/defaults/help/bin/Modify.md
+++ b/defaults/help/bin/Modify.md
@@ -1,34 +1,33 @@
-```trac
-= bin/modify.php =
+# bin/modify.php

This script can be used to modify an existing mtrack instance. You should try
to use the administration interface where possible.

-== Synopsis ==
+## Synopsis

-{{{
+```bash
% cd $MTRACK
% php bin/modify.php ...parameters...
-}}}
+```

-== Parameters ==
+## Parameters

-=== --repo !{name} !{type} !{repopath} === #repo
+### --repo {name} {type} {repopath}

Adds a source repository. This works in the same way as the
[help:bin/Init#repo repo option for bin/init.php].

-=== --link !{project} !{repo} !{location} === #link
+### --link {project} {repo} {location}

-Defines a link between the project identified by short name !{project} and the
-repository named !{name} by the source location identified by the regex
-!{location}.
+Defines a link between the project identified by short name {project} and the
+repository named {name} by the source location identified by the regex
+{location}.

This works in the same way as the [help:bin/Init#link link option for bin/init.php].

-=== --trac !{project} !{tracenv} === #trac
+### --trac {project} {tracenv}

-Imports data from a the trac environment on the local filesystem at !{tracenv}, and associate it with the project named !{project}.
+Imports data from a the trac environment on the local filesystem at {tracenv}, and associate it with the project named {project}.

This works in the same way as the
[help:bin/Init#trac trac option for bin/init.php],
@@ -36,11 +35,9 @@
secondary instances''. This means that the tickets and wiki pages will all be
prefixed with the project name.

-=== --config-file !{filename} === #config-file
+### --config-file {filename}

Where to find the pre-existing configuration file.

-If not specified, defaults to {{{config.ini}}} in the mtrack directory.
+If not specified, defaults to `config.ini` in the mtrack directory.

-
-```



https://bitbucket.org/wez/mtrack/changeset/3944d0a96e0b/
changeset: 3944d0a96e0b
user: wez
date: 2012-05-04 06:20:44
summary: merge timeline fixes
affected #: 2 files

diff -r f1500affcceb986eda40cf7269c25b60f2d76b27 -r 3944d0a96e0bd8169ba74691c0d8d014a3c8a6a4 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -1420,7 +1420,14 @@
if ($revised === null) {
$delta = -$amount;
} else {
- $delta = $revised - $this->getRemaining();
+ /* we used to use getRemaining() here, but that makes negative
+ * remaining time round to 0; we need to compensate for the true
+ * values in the table in case they have gone negative, so we
+ * do a true sum */
+ list($rem) = MTrackDB::q(
+ 'select sum(remaining) from effort where tid = ?',
+ $this->tid)->fetchAll(PDO::FETCH_COLUMN, 0);
+ $delta = $revised - ($rem + $this->estimated);
}
$this->effort[] = array($amount, $delta);
$this->spent += $amount;


diff -r f1500affcceb986eda40cf7269c25b60f2d76b27 -r 3944d0a96e0bd8169ba74691c0d8d014a3c8a6a4 inc/timeline.php
--- a/inc/timeline.php
+++ b/inc/timeline.php
@@ -160,7 +160,7 @@

echo "<div class='timeline'>";
$last_date = null;
- foreach ($events as $row) {
+ foreach ($events as $event_index => $row) {
if (--$limit == 0) {
break;
}
@@ -259,6 +259,16 @@
* but have been superseded by the repo entry instead */
continue 2;
case 'milestone':
+ if ($event_index > 0) {
+ /* Planning screen generates a lot of noisy entries */
+ $prior = $events[$event_index-1];
+ if ($prior['object'] == "milestone:$id" &&
+ $prior['reason'] == $row['reason']) {
+ // Don't count it against the limit
+ $limit++;
+ continue 2;
+ }
+ }
$eventclass = ' editmilestone';
$item = mtrack_object_id_link('milestone', $id);
break;



https://bitbucket.org/wez/mtrack/changeset/1551ab0a0fda/
changeset: 1551ab0a0fda
user: wez
date: 2012-05-05 02:53:32
summary: merge in git and notification fix
affected #: 3 files

diff -r 3944d0a96e0bd8169ba74691c0d8d014a3c8a6a4 -r 1551ab0a0fdab517ee8bb8ddd19f10dbc4fcfeea bin/git-commit-hook
--- a/bin/git-commit-hook
+++ b/bin/git-commit-hook
@@ -92,6 +92,7 @@
}
$c = new MTrackCommitHookChangeEvent;
$c->rev = '[changeset:' . $this->repo->getBrowseRootName() . ",$rev]";
+ $c->hash = $rev;
$c->files = $files;
$c->changelog = join("\n", $log);
$c->changeby = MTrackAuth::whoami();


diff -r 3944d0a96e0bd8169ba74691c0d8d014a3c8a6a4 -r 1551ab0a0fdab517ee8bb8ddd19f10dbc4fcfeea bin/send-notifications.php
--- a/bin/send-notifications.php
+++ b/bin/send-notifications.php
@@ -199,7 +199,7 @@

$code_by_repo = array();
foreach ($items as $obj) {
- if (!($obj instanceof MTrackSCMEvent)) {
+ if (!($obj instanceof MTrackSCMEvent) && !isset($obj->repo)) {
if (!isset($obj->ent)) {
continue;
}


diff -r 3944d0a96e0bd8169ba74691c0d8d014a3c8a6a4 -r 1551ab0a0fdab517ee8bb8ddd19f10dbc4fcfeea inc/watch.php
--- a/inc/watch.php
+++ b/inc/watch.php
@@ -494,6 +494,8 @@
if ($obj instanceof MTrackSCMEvent) {
/* group by repo */
$nkey = "repo:" . $obj->repo->repoid;
+ } else if (isset($obj->repo) && $obj->repo instanceof MTrackSCM) {
+ $nkey = "repo:" . $obj->repo->repoid;
} else {
$nkey = $obj->object;
}



https://bitbucket.org/wez/mtrack/changeset/d2f49f5ab73e/
changeset: d2f49f5ab73e
user: wez
date: 2012-05-05 02:56:43
summary: if the current user doesn't yet have a fullname in their profile,
default the Cc label to their id so that we avoid a bogus blank
Cc value.
affected #: 1 file

diff -r 1551ab0a0fdab517ee8bb8ddd19f10dbc4fcfeea -r d2f49f5ab73ebc8a8f59b84c0a9f0a35e80cc953 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -1502,7 +1502,7 @@
$o = new stdclass;
$o->id = $who;
$o->email = $data['email'];
- $o->label = $data['fullname'];
+ $o->label = isset($data['fullname']) ? $data['fullname'] : $who;
$res[$who] = $o;
}
return $res;



https://bitbucket.org/wez/mtrack/changeset/755e1f3b3cee/
changeset: 755e1f3b3cee
user: wez
date: 2012-05-05 02:59:32
summary: remove old ticket page. when canceling new ticket, redirect to root.
affected #: 2 files

diff -r d2f49f5ab73ebc8a8f59b84c0a9f0a35e80cc953 -r 755e1f3b3cee53714a8975060791cb34f6168513 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -3673,7 +3673,9 @@
window.location = ABSWEB + 'ticket.php/' + model.get('nsident');
},
hidden: function () {
- refresh_changes();
+ if (view.model.isNew()) {
+ window.location = ABSWEB;
+ }
}
});
}


diff -r d2f49f5ab73ebc8a8f59b84c0a9f0a35e80cc953 -r 755e1f3b3cee53714a8975060791cb34f6168513 web/ticketold.php
--- a/web/ticketold.php
+++ /dev/null
@@ -1,550 +0,0 @@
-<?php # vim:ts=2:sw=2:et:
-/* For licensing and copyright terms, see the file named LICENSE */
-include '../inc/common.php';
-
-if ($pi = mtrack_get_pathinfo()) {
- $id = $pi;
-} else {
- $id = $_GET['id'];
-}
-
-if ($id == 'new') {
- $issue = new MTrackIssue;
- $issue->priority = 'normal';
-} else {
- if (strlen($id) == 32) {
- $issue = MTrackIssue::loadById($id);
- } else {
- $issue = MTrackIssue::loadByNSIdent($id);
- }
- if (!$issue) {
- throw new Exception("Invalid ticket $id");
- }
-}
-
-$field_data = MTrackAPI::invoke('GET', '/ticket/meta/fields', null,
- array('tid' => $issue->tid))->result;
-
-$FIELDSET = json_encode($field_data);
-
-if ($id == 'new') {
- MTrackACL::requireAllRights("Tickets", 'create');
- $editable = 'true';
- mtrack_head("New ticket");
- $TICKET = json_encode(MTrackIssue::rest_return_ticket($issue));
- $CHANGES = json_encode(array());
- $ATTACH = json_encode(array());
-} else {
- MTrackACL::requireAllRights("ticket:" . $issue->tid, 'read');
- $editable = json_encode(
- MTrackACL::hasAllRights("ticket:" . $issue->tid, 'modify'));
- if ($issue->nsident) {
- mtrack_head("#$issue->nsident " . $issue->summary);
- } else {
- mtrack_head("#$id " . $issue->summary);
- }
- $TICKET = json_encode(MTrackAPI::invoke('GET', "/ticket/$id")->result);
- $CHANGES = json_encode(MTrackAPI::invoke(
- 'GET', "/ticket/$id/changes")->result);
- $ATTACH = json_encode(MTrackAPI::invoke(
- 'GET', "/ticket/$id/attach")->result);
-}
-
-echo <<<HTML
-<div id="attachment-form" class="popupForm" style="display:none">
- <form action="${ABSWEB}post-attachment.php" method="POST"
- id="upload-form" enctype="multipart/form-data" target="upload_target">
- <input type="hidden" name="object" value="ticket:X">
- <label for='attachments[]'>Select file(s) to be attached</label>
- <input name="attachments[]" class='btn multi' type="file">
- <iframe id="upload_target" name="upload_target" src="${ABSWEB}/mtrack.css">
- </iframe>
- <input type="submit" class='btn btn-primary' id="confirm-upload" value="Upload">
- <button class='btn' id="cancel-upload">Cancel</button>
- </form>
-</div>
-<div id="conflict-form" class="popupForm" style="display:none" -->
- <h1>Conflicting changes</h1>
- <p>Someone else has modified this ticket since you loaded the page.
- The differences between your desired version of the ticket and the
- currently saved version of the ticket are shown in the table below.
- </p>
- <br>
- <table>
- <thead>
- <tr>
- <th>Field</th><th>Yours</th><th>Theirs</th>
- </tr>
- </thead>
- <tbody id="conflict-list"></tbody>
- </table>
- <br>
- <p>
- Click one of the buttons below; you will be returned to the editor
- where you can make further changes (or cancel your changes).
- When you next click the save button, your changes will resolve this
- conflict and be applied to the ticket.
- </p>
- <button class='btn' id="conflict-keep">Keep my changes and return to editor</button>
- <button class='btn' id="conflict-take">Take their changes and return to editor</button>
-</div>
-<div id="issue-buttons">
- <div id="issue-content">
- <ul>
- <li><a href="#issue-container" class='active'>Description</a></li>
- <li><a href="#attach">Attachments</a></li>
- <li><a href="#change">Changes</a></li>
- </ul>
- </div>
- <div id="issue-controls">
- <div class='ui-state-error ui-corner-all' id='issue-error'>
- <span class='ui-icon ui-icon-alert'></span>
- <span id="issue-error-text"></span>
- </div>
- <button id="save-issue" class="btn btn-success hide-until-change">Save</button>
- <button id="cancel-issue" class="btn hide-until-change">Cancel</button>
- <button id="comment-issue" class='btn'><i class='icon-comment'></i> Comment</button>
- </div>
-</div>
-<div id="issue-container">
-<div id='commentedit' class='popupForm' style='display:none'></div>
-<div id='tktedit'></div>
-<div id="issue-desc"></div>
- <h2 id="attach">Attachments</h2>
- <div id="issue-attachments"></div>
- <h2 id="change">Changes</h2>
- <div id="issue-changes"></div>
-</div>
-<div id="issue-props"></div>
-
-<script type="text/template" id='attach-template'>
- <a class='attachment' href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><%- filename %></a> (<%- size %>) added by <%- who %>
- <abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr>
- <button class='btn btn-mini'><i class='icon-trash'></i></button>
- <% if (image) {
- var w = parseInt(width);
- var h = parseInt(height);
- var mw = 500;
- if (w > mw) {
- var s = w / mw;
- height = h / s;
- width = mw;
- }
- %>
- <br><a href='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>'><img src='<%= ABSWEB %>attachment.php/<%- object %>/<%- cid %>/<%- filename %>' width='<%- width %>' height='<%- height %>' border='0'></a>
- <% } %>
-</script>
-
-<script type="text/template" id='ticket-edit-template'>
- <h1><% if (status == 'closed') { %><del><% } %>
- <% if (nsident) { %>
- #<%- nsident %>
- <% } else { %>
- [NEW]
- <% } %><span id="tkt-summary-text"></span>
- <% if (status == 'closed') { %></del><% } %>
- </h1>
- <button class='btn' id='edit-description'>Edit Description</button>
-</script>
-
-<script type="text/template" id='ticket-change-template'>
- <div class='ticketevent'>
- <a class='pmark' href='#<%- id %>'>#</a><a name='<%- id %>'>&nbsp;</a><abbr class='timeinterval' title='<%- changedate %>'><%- changedate %></abbr><%- who %>
- <a class='replycomment' href="javascript:mtrack_reply_comment(<%- id %>);">reply</a>
- </div>
- <div class='ticketchangeinfo'>
- <img class='gravatar' src="${ABSWEB}avatar.php?u=<%- who %>&amp;s=48">
- <%
- var comment = null;
-
- _.each(audit, function (ent) {
- if (ent.label == 'Nsident') {
- return;
- }
- if (ent.label == 'Comment') {
- comment = ent.value_html;
- return;
- }
-
- if (ent.action == 'deleted') {
- %><b><%- ent.label %></b><%- ent.action %><%
- } else if (ent.label != 'Description') {
- if (_.isObject(ent.value)) {
- %>
- <b><%- ent.label %></b> &rarr;
- <%
- var cls = ent.label.toLowerCase();
- var url = null;
- if (cls == 'milestone') {
- url = ABSWEB + 'milestone.php/';
- }
- if (cls == 'keyword') {
- url = ABSWEB + 'search.php?q=keyword:';
- }
- if (cls == 'dependencies' || cls == 'blocks' ||
- cls == 'children' || cls == 'parent') {
- cls = 'ticketlink';
- url = ABSWEB + 'ticket.php/';
- }
- for (var id in ent.value) {
- if (url) {
- %><span class="<%- cls %>"><a href="<%- url %><%- ent.value[id] %>"><%- ent.value[id] %></a></span><%
- } else {
- %><span class="<%- cls %>"><%- ent.value[id] %></span><%
- }
- }
- } else {
- %>
- <b><%- ent.label %></b> &rarr; <%- ent.value %>
- <%
- }
- } else {
- %>
- <b><%- ent.label %></b><%- ent.action %><button class="btn toggle-desc" desc-id="desc-<%- ent.cid %>">Toggle</button>
- <p id="desc-<%- ent.cid %>" class="hide-desc"><%- ent.value %></p>
- <%
- }
- %>
-
- <br/>
- <%
- });
- if (comment) { print(comment); }
- %>
- </div>
-</script>
-
-<script type='text/javascript'>
-$(document).ready(function() {
- var TheTicket = null;
- var base_ticket = $TICKET;
- var FIELDSET = $FIELDSET;
- var editable = $editable;
- var editor = null;
- var changes = $CHANGES;
- var attachments = $ATTACH;
- var comment_editor = null;
-
- function reset_editor() {
- if (!TheTicket) {
- TheTicket = new MTrackTicket(base_ticket);
- TheTicket.mtrack_edit_count = 0;
- TheTicket.getChanges().reset(changes);
- TheTicket.getAttachments().reset(attachments);
- } else {
- TheTicket.set(base_ticket);
- TheTicket.changed = false;
- }
- }
- reset_editor();
-
- editor = new MTrackMainTicketEditorView({
- model: TheTicket,
- readonly: !editable,
- fieldset: FIELDSET,
- el: '#tktedit'
- });
-
- var change_view = new MTrackTicketChangesView({
- model: TheTicket,
- collection: TheTicket.getChanges(),
- el: '#issue-changes'
- });
-
- var attach_view = new MTrackTicketAttachmentsView({
- model: TheTicket,
- editable: editable,
- collection: TheTicket.getAttachments(),
- el: '#issue-attachments'
- });
-
- TheTicket.bind('change', function () {
- var id = TheTicket.get('nsident');
- if (id) {
- $('html head title').text('#' + id + ' ' + TheTicket.get('summary'));
- }
-
- if (TheTicket.mtrack_fetching) return;
- if (TheTicket.hasChanged()) {
- TheTicket.changed = true;
- }
- if (TheTicket.changed) {
- $('#issue-buttons button.hide-until-change').fadeIn('fast');
- }
- });
-
- window.onbeforeunload = function() {
- if (TheTicket.changed) {
- return "You haven't saved your changes!";
- }
- };
-
- TheTicket.bind('error', function (model, err) {
- $('#issue-error-text').text(err);
- $('#issue-error').fadeIn('fast')
- });
-
- comment_editor = new MTrackWikiTextAreaView({
- model: TheTicket,
- wikiContext: "ticket:",
- use_overlay: true,
- Caption: "Edit Comment text",
- OKLabel: "Add Comment",
- CancelLabel: "Abandon changes to comment",
- readonly: !editable,
- srcattr: "comment",
- el: "#commentedit"
- });
-
- function refresh_lists() {
- var Changes = TheTicket.getChanges();
- var recent = Changes.at(0);
- if (!recent) {
- return;
- }
- Changes.fetch({
- success: function (c, r) {
- if (r.length) {
- TheTicket.getAttachments().fetch({
- success: function () {
- attachments = TheTicket.getAttachments().models;
- }
- });
- if (!TheTicket.changed && TheTicket.mtrack_edit_count == 0) {
- TheTicket.mtrack_fetching = true;
- TheTicket.fetch({
- error: function() {
- TheTicket.mtrack_fetching = false;
- },
- success: function (model, resp) {
- TheTicket.mtrack_fetching = false;
- if (model.get('updated').cid != base_ticket.updated.cid) {
- /* it changed */
- base_ticket = TheTicket.toJSON();
- TheTicket.unset('comment', {silent: true});
- editor.render();
- }
- }
- });
- }
- }
- changes = Changes.models;
- },
- data: {
- last: recent.id
- },
- add: true
- });
- }
-
- function check_for_changes() {
- refresh_lists();
- }
- if (!TheTicket.isNew()) {
- setInterval(check_for_changes, 60000);
- }
-
- $('#issue-error').click(function () {
- $(this).fadeOut('fast');
- });
-
- $('#save-issue').click(function () {
- $('#issue-error').fadeOut('fast');
- var overlay = $('<div class="overlay"/>');
- overlay.appendTo('body').fadeIn('fast', function () {
- TheTicket.save(TheTicket.toJSON(), {
- success: function(model, response) {
- $('#issue-buttons button.hide-until-change').fadeOut('fast');
- overlay.fadeOut('fast', function () {
- if (base_ticket.nsident == null) {
- /* we just saved the initial version; revise the URL
- * to reflect our new status */
- var url = ABSWEB + 'ticket.php/' + TheTicket.get('nsident');
- window.onbeforeunload = null;
- window.location.href = url;
- return;
- }
- base_ticket = TheTicket.toJSON();
- TheTicket.unset('comment', {silent: true});
- editor.render();
- refresh_lists();
- overlay.remove();
- TheTicket.changed = false;
- });
- },
- error: function(model, response) {
- var err;
- var conflict = null;
- if (!_.isObject(response)) {
- err = response;
- } else {
- err = response.statusText;
- try {
- var r = JSON.parse(response.responseText);
- err = r.message;
- if (r.code == 409) {
- conflict = r.extra;
- }
- } catch (e) {
- err = response.statusText;
- }
- }
- refresh_lists();
-
- if (conflict) {
- var tbl = $('#conflict-list');
- tbl.empty();
- TheTicket.set({updated: conflict.updated});
- delete conflict.updated;
- for (var k in conflict) {
- if (k == 'description_html') {
- continue;
- }
- var item = conflict[k];
- var o = {
- field: k,
- yours: item[0],
- theirs: item[1]
- };
- $(_.template(
- "<tr><td><%- field %></td><td><%- yours %></td><td><%- theirs %></td></tr>", o)).
- appendTo(tbl);
- }
- $('#conflict-form').fadeIn('fast');
- $('#conflict-keep').click(function () {
- $('#conflict-form').fadeOut('fast');
- overlay.fadeOut('fast', function () {
- overlay.remove();
- });
- return false;
- });
- $('#conflict-take').click(function () {
- $('#conflict-form').fadeOut('fast');
- var o = {};
- for (var k in conflict) {
- var item = conflict[k];
- o[k] = item[1];
- }
- TheTicket.set(o);
-
- overlay.fadeOut('fast', function () {
- overlay.remove();
- });
- return false;
- });
- } else {
- $('#issue-error-text').text(err);
- $('#issue-error').fadeIn('fast')
- overlay.fadeOut('fast', function () {
- overlay.remove();
- });
- }
- }
- });
- });
- });
-
- $('#cancel-issue').click(function () {
- $('#issue-error').fadeOut('fast');
- reset_editor();
- $('#issue-buttons button.hide-until-change').fadeOut('fast');
- });
-
- var in_reply = false;
- var orig_comment = null;
-
- if (editable) {
- comment_editor.bind('canceledit', function () {
- console.log("cancel");
- if (in_reply) {
- in_reply = false;
- TheTicket.set({comment: orig_comment},{silent:true});
- }
- });
- TheTicket.bind('change:comment', function () {
- in_reply = false;
- orig_comment = null;
- });
- $('#comment-issue').click(function () {
- comment_editor.edit();
- return false;
- });
- } else {
- $('#comment-issue').hide();
- }
-
- function reply_comment(cid) {
- var c = TheTicket.getChanges().get(cid);
- orig_comment = TheTicket.get("comment");
- var comment = orig_comment || '';
- if (comment.length) {
- comment = comment + "\\n\\n";
- }
- var reason = c.get('reason');
- // cite it
- reason = reason.replace(/^(\s*)/mg, "> \$1");
- comment = comment + "Replying to [comment:" + cid + " a comment by " +
- c.get('who') + "]\\n" + reason + "\\n";
- in_reply = true;
- TheTicket.set({'comment': comment}, {silent: true});
- comment_editor.edit();
- }
- window.mtrack_reply_comment = reply_comment;
-
-
- function calc_soff() {
- var b = $('#issue-buttons');
- var d = b.position().top - $(window).scrollTop() + b.height() + 10;
- return d;
- }
-
- var clicking = false;
- /* color the "tabs" based on the scroll position */
- function highlight_tab() {
- var soff = calc_soff();
- var y = $(window).scrollTop();
- var active = null;
- $('#issue-content a').each(function () {
- var what = $(this).attr('href');
- var target = $(what);
-
- var pos = $(target).position()['top'] - soff;
- $(this).removeClass('active');
- if (y >= pos) {
- active = $(this);
- }
- });
- if (active) {
- active.addClass('active');
- }
- }
- $(window).scroll(function() {
- if (!clicking) {
- highlight_tab();
- }
- });
-
- $('#issue-content a').click(function () {
- var what = $(this).attr('href');
- var target = $(what);
- var d = calc_soff();
- var t = target.offset().top - d;
- clicking = true;
- $('#issue-content a').removeClass('active');
- $(this).addClass('active');
- $('html, body').animate(
- {scrollTop: t},
- 350,
- 'easeOutQuint',
- function () {
- clicking = false;
- }
- );
- return false;
- });
-});
-</script>
-HTML;
-
-mtrack_foot();
-

Repository URL: https://bitbucket.org/wez/mtrack/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
Reply all
Reply to author
Forward
0 new messages