diff --git a/include/functions.inc b/include/functions.inc index c9c3046..e057f69 100644 --- a/include/functions.inc +++ b/include/functions.inc @@ -36,7 +36,6 @@ function head($title="", $config = []) { ["href" => "/manage/event.php", "text" => "Events"], ["href" => "/manage/users.php", "text" => "Users"], ["href" => "/manage/user-notes.php", "text" => "Notes"], - ["href" => "/manage/github.php", "text" => "Github"], ]; $CSS = ["/styles/master.css"]; $SEARCH = []; @@ -258,6 +257,11 @@ function find_group_address_from_notes_for($id) { define("MT_USER_APPROVE_MAIL", "group@php.net"); define("MT_USER_REMOVE_MAIL", "group@php.net"); function user_approve($id) { + if (!is_admin($_SESSION["username"])) { + warn("you're not allowed to take actions on users."); + exit; + } + $res = db_query_safe("UPDATE users SET cvsaccess=1, enable=1 WHERE userid=?", [$id]); if ($res && mysql_affected_rows()) { $cc = find_group_address_from_notes_for($id); @@ -284,6 +288,10 @@ function user_approve($id) { } function user_remove($id) { + if (!is_admin($_SESSION["username"])) { + warn("you're not allowed to take actions on users."); + exit; + } $userinfo = fetch_user($id); $res = db_query_safe("DELETE FROM users WHERE userid=?", [$id]); if ($res && mysql_affected_rows()) { @@ -313,6 +321,21 @@ function user_remove($id) { } } +function user_unlink_github($id) { + $db = DB::connect(); + + if(!can_modify($_SESSION['username'], $id)) { + warn("you're not allowed to take actions on users."); + exit; + } + + $query = $db->prepare('UPDATE users SET github = ? WHERE userid = ?'); + $query->execute([null, (int)$id]); + + warn("record $id updated"); + exit; +} + function is_admin($user) { $admins = [ "jimw", @@ -428,6 +451,7 @@ function validateAction($k) { switch($k) { case "approve": case "remove": + case "github_unlink": return $k; default: warn("that action ('" . hsc($k) . "') is not understood."); diff --git a/public/manage/github.php b/public/manage/github.php index d340be2..c7bd692 100644 --- a/public/manage/github.php +++ b/public/manage/github.php @@ -1,176 +1,65 @@ - $options]); - - $url = 'https://api.github.com'.$endpoint; - $s = @file_get_contents($url, false, $ctxt); - if ($s === false) { - die('Request to GitHub failed. Endpoint: '.$endpoint); - } - - return json_decode($s); +$oauth = new OAuthClient(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET); +if (!isset($_GET['code'])) { + header('Location: ' . $oauth->getRequestCodeUrl()); + exit; } -function github_current_user($access_token = false) -{ - if (!$access_token) { - $access_token = $_SESSION['github']['access_token']; - } +head("github administration"); - if (empty($_SESSION['github']['current_user'])) { - $user = github_api('/user?access_token='.urlencode($access_token)); - if (!$user->login) { - die('Failed to get current user'); - } +try { + if (isset($_GET['code'])) { + $response = $oauth->requestAccessToken($_GET['code']); + if (!isset($response['access_token'])) { + throw new RuntimeException('Can not receive the access token'); + } - $_SESSION['github']['current_user'] = $user; - } + $client = new Client($response['access_token']); + $user = $client->me(); - return $_SESSION['github']['current_user']; -} + if (!isset($user['login'])) { + throw new RuntimeException('Can not get the user GitHub login'); + } -function github_require_valid_user() -{ - if (isset($_SESSION['github']['access_token'])) { - return true; - } + $username = $_SESSION['credentials'][0]; - if (isset($_GET['code'])) { - $data = [ - 'client_id' => GITHUB_CLIENT_ID, - 'client_secret' => GITHUB_CLIENT_SECRET, - 'code' => $_GET['code'] - ]; - $data_encoded = http_build_query($data); - $opts = [ - 'method' => 'POST', - 'user_agent' => GITHUB_USER_AGENT, - 'header' => 'Content-type: application/x-www-form-urlencoded', - 'content' => $data_encoded, - ]; - $ctxt = stream_context_create(['http' => $opts]); - $s = @file_get_contents('https://github.com/login/oauth/access_token', false, $ctxt); - if (!$s) { - die('Failed while checking with GitHub,either you are trying to hack us or our configuration is wrong (GITHUB_CLIENT_SECRET outdated?)'); - } - $gh = []; - parse_str($s, $gh); - if (empty($gh['access_token'])) { - die("GitHub responded but didn't send an access_token"); - } + $db = \App\DB::connect(); + $query = $db->prepare('SELECT userid FROM users WHERE username = ?'); + $query->execute([$username]); + if (!$query->rowCount()) { + throw new RuntimeException('was not able to find user matching ' . $username); + } - $user = github_current_user($gh['access_token']); + $account = $query->fetch(); + $query = $db->prepare('SELECT userid FROM users WHERE github = ? AND userid != ?'); + $query->execute([$user['login'], $account['userid']]); + if ($query->rowCount() > 0) { + throw new RuntimeException('GitHub account ' . $user['login'] . ' is already linked'); + } - $endpoint = '/teams/'.urlencode((string)GITHUB_PHP_OWNER_TEAM_ID).'/members/'.urlencode($user->login); - $opts = ['user_agent' => GITHUB_USER_AGENT]; - $ctxt = stream_context_create(['http' => $opts]); - $is_member = file_get_contents('https://api.github.com'.$endpoint.'?access_token='.urlencode($gh['access_token']), false, $ctxt); + $query = $db->prepare('UPDATE users SET github = ? WHERE userid = ?'); + $query->execute([$user['login'], $account['userid']]); - if ($is_member === false) { - head("github administration"); - echo '

You (Authenticated GitHub user: '.htmlentities($user->login). ') are no member of the php organization on github.

'. - '

Please contact an existing member if you see need.

'; - foot(); - exit; + echo '

We linked your GitHub account with your profile.

' . + '

Back to profile

'; } - // SUCCESS - $_SESSION['github']['access_token'] = $gh['access_token']; - header('Location: github.php'); - exit; - } - - // Start oauth - header('Location: https://github.com/login/oauth/authorize?scope=repo&client_id='.urlencode(GITHUB_CLIENT_ID)); - exit; -} - -if (isset($_POST['description']) && isset($_SESSION['github']['access_token'])) { - action_create_repo(); -} elseif (isset($_GET['login']) || isset($_GET['code']) || isset($_SESSION['github']['access_token'])) { - action_form(); -} else { - action_default(); -} - -function action_default() -{ - head("github administration"); - echo '

This tool is for administrating PHP repos on GitHub. Currently it is used for adding repos only.

'; - echo '

NOTE: Only members of the PHP organisation on GitHub can use this tool. We try to keep the number of members limited.

'; - echo '

In case you are a member you can login using GitHub.

'; - foot(); -} - -function action_form() -{ - github_require_valid_user(); - $user = $_SESSION['github']['current_user']; - head("github administration"); -?> -

GitHub user: login); ?>

-

Creating a GitHub repo using this form ensures the proper configuration. This -includes disabling the GitHub wiki and issue tracker as well as enabling the -php-pulls user to push changes made on git.php.net.

-

The name, description and homepage should follow other existing repositories.

-
-Github repo name: https://github.com/php/ (i.e. pecl-category-foobar)
-Description: (i.e. PECL foobar extension)
-Homepage: (i.e. http://pecl.php.net/package/foobar)
- - -getMessage()); } -function action_create_repo() -{ - github_require_valid_user(); - - $data = [ - 'name' => $_POST['name'], - 'description' => $_POST['description'], - - 'homepage' => $_POST['homepage'], - 'private' => false, - 'has_issues' => false, - 'has_wiki' => false, - 'has_downloads' => false, - 'team_id' => GITHUB_REPO_TEAM_ID, - ]; - $data_j = json_encode($data); - $opts = [ - 'content' => $data_j, - ]; - $res = github_api('/orgs/php/repos?access_token='.urlencode($_SESSION['github']['access_token']), 'POST', $opts); - - head("github administration"); - if (isset($res->html_url)) { - echo '

Repo created!

Check on GitHub.

'; - } else { - echo "Error while creating repo."; - } - foot(); -} -?> +foot(); diff --git a/public/manage/users.php b/public/manage/users.php index c426061..a21afef 100644 --- a/public/manage/users.php +++ b/public/manage/users.php @@ -84,11 +84,6 @@ function csrf_validate(&$mydata, $name) { $action = filter_input(INPUT_POST, "action", FILTER_CALLBACK, ["options" => "validateAction"]); if ($id && $action) { csrf_validate($_SESSION, $action); - if (!is_admin($_SESSION["username"])) { - warn("you're not allowed to take actions on users."); - exit; - } - switch ($action) { case 'approve': user_approve((int)$id); @@ -98,6 +93,10 @@ function csrf_validate(&$mydata, $name) { user_remove((int)$id); break; + case 'github_unlink': + user_unlink_github((int)$id); + break; + default: warn("that action ('$action') is not understood."); } @@ -199,6 +198,24 @@ function csrf_validate(&$mydata, $name) { + + GitHub account: + + + + + + + +
+ + + + Link GitHub account + + — + + Leave password fields blank to leave password unchanged. @@ -321,7 +338,7 @@ function csrf_validate(&$mydata, $name) { $search = filter_input(INPUT_GET, "search", FILTER_UNSAFE_RAW) ?: ""; $order = filter_input(INPUT_GET, "order", FILTER_UNSAFE_RAW) ?: ""; -$query = new Query("SELECT DISTINCT SQL_CALC_FOUND_ROWS users.userid,cvsaccess,username,name,email,GROUP_CONCAT(note) note FROM users "); +$query = new Query("SELECT DISTINCT SQL_CALC_FOUND_ROWS users.userid,cvsaccess,username,name,email,github,GROUP_CONCAT(note) note FROM users "); $query->add(" LEFT JOIN users_note ON users_note.userid = users.userid "); if ($search) { @@ -338,7 +355,7 @@ function csrf_validate(&$mydata, $name) { $query->add(" GROUP BY users.userid "); if ($order) { - if (!in_array($order, ["username", "name", "email", "note"], true)) { + if (!in_array($order, ["username", "name", "email", "note", "github"], true)) { die("Invalid order!"); } if ($forward) { @@ -380,7 +397,8 @@ function csrf_validate(&$mydata, $name) { "username"]);?>">username "name"]);?>">name - "email"]);?>">email + "email"]);?>">email + "github"]);?>">github "email"]);?>">email "note"]);?>">note @@ -395,7 +413,8 @@ function csrf_validate(&$mydata, $name) { - + + diff --git a/schema.sql b/schema.sql index 778c148..46ea58d 100644 --- a/schema.sql +++ b/schema.sql @@ -197,6 +197,7 @@ CREATE TABLE `users` ( `name` varchar(255) NOT NULL DEFAULT '', `email` varchar(255) NOT NULL DEFAULT '', `username` varchar(16) DEFAULT NULL, + `github` varchar(39) DEFAULT NULL, `cvsaccess` int(1) NOT NULL DEFAULT 0, `spamprotect` int(1) NOT NULL DEFAULT 1, `forgot` varchar(32) DEFAULT NULL, diff --git a/src/GitHub/Client.php b/src/GitHub/Client.php new file mode 100644 index 0000000..b40bb92 --- /dev/null +++ b/src/GitHub/Client.php @@ -0,0 +1,49 @@ +token = $token; + } + + public function me() + { + return $this->query('/user'); + } + + protected function query($endpoint, $method = 'GET', $headers = []) + { + $options = [ + 'method' => $method, + 'user_agent' => self::GITHUB_USER_AGENT + ]; + + if ($token = $this->token) { + $headers[] = 'Authorization: token ' . urlencode($token); + } + + $options['header'] = implode(PHP_EOL, $headers); + + $context = stream_context_create(['http' => $options]); + $url = 'https://api.github.com' . $endpoint; + $response = file_get_contents($url, false, $context); + + if (!$response) { + throw new \RuntimeException('Failed GitHub request: ' . $endpoint); + } + + $result = json_decode($response, true, 512, JSON_THROW_ON_ERROR); + if (isset($result['error'])) { + throw new \RuntimeException($result['error_description']); + } + + return $result; + } +} \ No newline at end of file diff --git a/src/GitHub/OAuthClient.php b/src/GitHub/OAuthClient.php new file mode 100644 index 0000000..e2d5c82 --- /dev/null +++ b/src/GitHub/OAuthClient.php @@ -0,0 +1,59 @@ +clientId = $clientId; + $this->clientSecret = $clientSecret; + } + + public function getRequestCodeUrl() + { + return 'https://github.com/login/oauth/authorize?client_id=' . urlencode($this->clientId); + } + + public function requestAccessToken($code) + { + $headers = []; + $headers[] = 'Content-type: application/x-www-form-urlencoded'; + $headers[] = 'Accept: application/json'; + + $data = http_build_query( + [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'code' => $code + ] + ); + + $options['method'] = 'POST'; + $options['user_agent'] = self::GITHUB_USER_AGENT; + $options['header'] = implode(PHP_EOL, $headers); + $options['content'] = $data; + + $context = stream_context_create(['http' => $options]); + $response = file_get_contents('https://github.com/login/oauth/access_token', false, $context); + + if (!$response) { + throw new RuntimeException('Failed GitHub request access token'); + } + + $result = json_decode($response, true, 512, JSON_THROW_ON_ERROR); + if (isset($result['error'])) { + throw new RuntimeException($result['error_description']); + } + + return $result; + } +} \ No newline at end of file