--- /dev/null
+<!DOCTYPE html>
+
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title><!--~$title show {~-->Accounts<!--~}~--></title>
+ <link rel="stylesheet" href="style.css">
+</head>
+
+<body>
+<!--~$body show {~-->
+
+ <!--~form {~-->
+ <h2><!--~id unset {~-->Add a new account<!--~}~--><!--~id {~-->Edit account "~name html~"<!--~}~--></h2>
+
+ <form action="admin_users" method="post"><!--~id {~--><div style="display: none"><input type="hidden" name="edit_id" value="~id attr~"></div><!--~}~-->
+
+ <div class="caption">Role</div>
+ <div class="field"><select name="role"><!--~role options~--></select></div>
+
+ <div class="caption">Name (optional)</div>
+ <div class="field"><input type="text" name="name" value="~name attr~"></div>
+
+ <div class="caption">Username</div>
+ <div class="field_notes">This is used to log in. It is not case sensitive, and symbols/spaces/etc are ignored.</div>
+ <div class="field~username_bad {~ field_error~}~"><input type="text" name="username" value="~username attr~"></div>
+
+ <div class="caption">Password</div>
+ <!--~editing unset {~--><div class="field_notes">If this is left blank, the user will be unable to log in.</div><!--~}~-->
+ <!--~editing {~--><div class="field_notes">Leave this blank to leave teh password unchanged.</div><!--~}~-->
+ <div class="field_notes">Password suggestions: ~password_suggestions {~<code class="password_suggestion">~password_suggestions html~</code>~ sep {~ ~}~~}~</div>
+ <div class="field~password_bad {~ field_error~}~">
+ <input type="password" name="pass1" value=""><br>
+ <input type="password" name="pass2" value="">
+ </div>
+
+ <div class="caption"></div>
+ <div class="field"><input type="submit" name="save" value="Save"></div>
+
+ </form>
+
+ <div class="caption"> </div>
+ <div class="field"><a href="admin_users~id {~?id=~id~~}~">Cancel</a></div>
+ <!--~}~-->
+
+ <!--~listings {~-->
+ <h2>Accounts Listing</h2>
+
+ <p>On this page you can manage who can log into this site, and what sort of things they have permission to do once logged in.</p>
+
+ <!--~rows once_if {~-->
+ <p><a href="admin_users?new=1">[Add a new account]</a></p>
+
+ <table cellspacing="0" cellpadding="4" border="0" summary="" class="evenodd">
+ <tr>
+ <th><a href="?sort=~sorting-by-role~role">Role</a></th>
+ <th><a href="?sort=~sorting-by-name~name">Name</a></th>
+ <th><a href="?sort=~sorting-by-username~username">Username</a></th>
+ <th><a href="?sort=~sorting-by-last_login~last_login">Last Login</a></th>
+ <th><a href="?sort=~sorting-by-last_active~last_active">Last Active</a></th>
+ <th> </th>
+ </tr><!--~rows {~-->
+ <tr>
+ <td class="listing"><a href="admin_users?edit_id=~id~">~role html~<!--~role empty {~--><em>(blank)</em><!--~}~--></a></td>
+ <td class="listing"><a href="admin_users?edit_id=~id~">~name html~<!--~name empty {~--><em>(blank)</em><!--~}~--></a></td>
+ <td class="listing"><a href="admin_users?edit_id=~id~">~username html~<!--~username empty {~--><em>(blank)</em><!--~}~--></a></td>
+ <td class="listing"><a href="admin_users?edit_id=~id~" class="unix_date">~last_login html~</a></td>
+ <td class="listing"><a href="admin_users?edit_id=~id~" class="unix_time">~last_active html~</a></td>
+ <td><a href="admin_users?admin_users_delete_id=~id~" onclick="return confirm('Permanently delete?')">[delete this account]</a></td>
+ </tr><!--~}~-->
+
+ </table>
+ <p><a href="?download_csv=1">Download as CSV file</a></p>
+ <!--~}~-->
+ <!--~rows once_else {~-->
+ <p>No accounts in database.</p>
+ <!--~}~-->
+
+ <p><a href="admin_users?new=1">[Add a new account]</a></p>
+ <!--~}~-->
+
+<!--~}~-->
+</body>
+</html>
--- /dev/null
+<?php
+
+# This form requires wfpl. See: http://sametwice.com/wfpl
+
+# This form was initially auto-generated. If you would like to alter the
+# parameters and generate a new one try this URL:
+#
+# http://metaform.localhost.jasonwoof.com/?file_name=admin_users&table_name=users&singular=account&plural=accounts&opt_email=No&opt_db=Yes&opt_listing=Yes&opt_display=No&opt_pass=No&opt_public_form=No&opt_public_display=No&fields=name%0D%0Ausername%0D%0Atextbox+password%0D%0Arole%0D%0Aint+last_active%0D%0Aint+last_login&edit=yes
+
+
+# SETUP
+
+# To save results to a database, you'll need to create the users table.
+# The file admin_users.sql should help with this
+#
+# if you rename any of the database fields, you'll need to update this:
+define('ADMIN_USERS_DB_FIELDS', 'role,name,username,last_login,last_active');
+
+
+require_once(__DIR__.'/'.'inc/wfpl/format.php');
+
+$GLOBALS['admin_users_field_to_caption'] = array(
+ 'name' => 'Name',
+ 'role' => 'Role',
+ 'username' => 'Username',
+ 'password' => 'Password',
+ 'last_login' => 'Last Login',
+ 'last_active' => 'Last Active'
+);
+
+function admin_users_get_fields() {
+ $data = array();
+
+ $data['role'] = format_options(_REQUEST_cut('role'), 'role');
+ $data['name'] = format_oneline(trim(_REQUEST_cut('name')));
+ $data['username'] = format_oneline(trim(_REQUEST_cut('username')));
+ $data['pass1'] = format_oneline(trim(_REQUEST_cut('pass1')));
+ $data['pass2'] = format_oneline(trim(_REQUEST_cut('pass2')));
+
+ return $data;
+}
+
+
+function admin_users_main() {
+ session_auth_must('admin_users');
+
+ $id = _REQUEST_cut('edit_id');
+ if ($id) {
+ return admin_users_main_form($id);
+ }
+
+ $id = _REQUEST_cut('admin_users_delete_id');
+ if ($id) {
+ return admin_users_main_delete($id);
+ }
+
+ if (_REQUEST_cut('new')) {
+ return admin_users_main_form();
+ }
+
+ if (_REQUEST_cut('list')) {
+ return admin_users_main_listing();
+ }
+
+ if (_REQUEST_cut('download_csv')) {
+ return admin_users_csv_download();
+ }
+
+ if (isset($_POST['name'])) {
+ return admin_users_main_form();
+ }
+
+ # default action:
+ return admin_users_main_listing();
+}
+
+function admin_users_main_delete($id) {
+ db_delete('users', 'where id=%i', $id);
+ message('Account deleted.');
+ return './admin_users';
+}
+
+function admin_users_csv_download() {
+ require_once(__DIR__.'/'.'inc/wfpl/csv.php');
+ $rows = db_get_rows('users', 'id,'.ADMIN_USERS_DB_FIELDS, 'order by id');
+ $fields = explode(',', 'id,'.ADMIN_USERS_DB_FIELDS);
+ $header = array();
+ foreach ($fields as $field) {
+ if (isset($GLOBALS['admin_users_field_to_caption'][$field])) {
+ $header[] = $GLOBALS['admin_users_field_to_caption'][$field];
+ } else {
+ $header[] = $field;
+ }
+ }
+ array_unshift($rows, $header);
+ array2d_to_csv_download($rows, 'admin_users.csv');
+}
+
+function admin_users_main_listing() {
+ $data = array();
+ $desc = '';
+ $sort = _REQUEST_cut('sort');
+ if ($sort && substr($sort, 0, 1) === '-') {
+ $sort = substr($sort, 1);
+ $desc = ' DESC ';
+ } else {
+ $data["sorting-by-$sort"] = '-';
+ }
+ $legal_sorts = explode(',', ADMIN_USERS_DB_FIELDS);
+ if (!$sort || !in_array($sort, $legal_sorts)) {
+ $sort = 'role, name';
+ }
+
+ $data['rows'] = db_get_assocs('users', 'id,role,name,username,last_login,last_active', "order by $sort $desc limit 1000");
+ tem_set('listings', $data);
+ render_timestamps();
+}
+
+function admin_users_suggested_password() {
+ $character_set = "ABCDEFHJKLMNPQRTUWXY34789"; # removed all similar-looking characters
+ $code = " ";
+
+ # PHP 4.2.0 and up seed the random number generator for you.
+ # Lets hope that it seeds with something harder to guess than the clock.
+ for($i = 0; $i < 10; ++$i) {
+ $code{$i} = $character_set{mt_rand(0, 24)}; # inclusive
+ }
+
+ return $code;
+}
+
+function admin_users_main_form($id = false) {
+ if ($id) {
+ tem_set('id', $id);
+ }
+
+ pulldown('role', [
+ ['admin', 'Site Administrator'],
+ ['disabled', 'Account Disabled']
+ ]);
+
+ if (isset($_POST['name'])) {
+ $data = admin_users_get_fields();
+
+ if (strlen($data['username']) < 1) {
+ message("Oop, Username is required");
+ $data['username_bad'] = true;
+ } elseif ($data['pass1'] !== $data['pass2']) {
+ message("Oop, passwords didn't match. Please enter your desired password carefully (twice).");
+ $data['password_bad'] = true;
+ } else {
+ # password hash is slow, so only do it if we're really doing a db write
+ if (isset($data['pass1']) && strlen($data['pass1']) > 0) {
+ # hash password for db storage
+ if (!function_exists('password_hash')) {
+ require_once(DOCROOT . 'inc/password_funcs_backported.php');
+ }
+ $data['password'] = password_hash($data['pass1'], PASSWORD_DEFAULT);
+ }
+ unset($data['pass1']);
+ unset($data['pass2']);
+ if ($id) {
+ db_update_assoc('users', $data, 'where id=%i', $id);
+ message('Account updated.');
+ } else {
+ db_insert_assoc('users', $data);
+ message('Account saved.');
+ }
+ return './admin_users';
+ }
+ # else fall through to display the form again. Field values are in $data
+ } elseif ($id) {
+ # we've recieved an edit id, but no data. So we grab the values to be edited from the database
+ $data = db_get_assoc('users', ADMIN_USERS_DB_FIELDS, 'where id=%i', $id);
+ } else {
+ # form not submitted, you can set default values like so:
+ #$data = array('name' => 'Yes');
+ $data = array();
+ }
+
+ tem_set('password_suggestions', [
+ admin_users_suggested_password(),
+ admin_users_suggested_password(),
+ admin_users_suggested_password(),
+ admin_users_suggested_password(),
+ admin_users_suggested_password()
+ ]);
+ tem_set('form', $data);
+}
--- /dev/null
+drop table if exists users;
+create table users (
+ id int unique auto_increment,
+ name varchar(200) binary not null default "",
+ username varchar(200) binary not null default "",
+ password varchar(255) binary not null default "",
+ role varchar(200) binary not null default "",
+ last_active int(11) not null default 0,
+ last_login int(11) not null default 0
+) CHARSET=utf8;
require_once(DOCROOT . 'inc/wfpl/session_messages.php');
require_once(DOCROOT . 'inc/session_auth.php');
require_once(DOCROOT . 'inc/cms.php');
+require_once(DOCROOT . 'inc/misc.php');
# Connect to the database
db_connect(WFPL_DB, WFPL_DB_USER, WFPL_DB_PASS);
--- /dev/null
+<?php
+
+# call this when you have class="unix_time" or class="unix_date"
+function render_timestamps() {
+ $GLOBALS['wfpl_main_template']->set('$render_timestamps');
+}
--- /dev/null
+<?php
+/**
+ * A Compatibility library with PHP 5.5's simplified password hashing API.
+ *
+ * @author Anthony Ferrara <ircmaxell@php.net>
+ * @license http://www.opensource.org/licenses/mit-license.html MIT License
+ * @copyright 2012 The Authors
+ */
+
+namespace {
+
+ if (!defined('PASSWORD_BCRYPT')) {
+ /**
+ * PHPUnit Process isolation caches constants, but not function declarations.
+ * So we need to check if the constants are defined separately from
+ * the functions to enable supporting process isolation in userland
+ * code.
+ */
+ define('PASSWORD_BCRYPT', 1);
+ define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+ define('PASSWORD_BCRYPT_DEFAULT_COST', 10);
+ }
+
+ if (!function_exists('password_hash')) {
+
+ /**
+ * Hash the password using the specified algorithm
+ *
+ * @param string $password The password to hash
+ * @param int $algo The algorithm to use (Defined by PASSWORD_* constants)
+ * @param array $options The options for the algorithm to use
+ *
+ * @return string|false The hashed password, or false on error.
+ */
+ function password_hash($password, $algo, array $options = array()) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
+ return null;
+ }
+ if (is_null($password) || is_int($password)) {
+ $password = (string) $password;
+ }
+ if (!is_string($password)) {
+ trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
+ return null;
+ }
+ if (!is_int($algo)) {
+ trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
+ return null;
+ }
+ $resultLength = 0;
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ $cost = PASSWORD_BCRYPT_DEFAULT_COST;
+ if (isset($options['cost'])) {
+ $cost = $options['cost'];
+ if ($cost < 4 || $cost > 31) {
+ trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), E_USER_WARNING);
+ return null;
+ }
+ }
+ // The length of salt to generate
+ $raw_salt_len = 16;
+ // The length required in the final serialization
+ $required_salt_len = 22;
+ $hash_format = sprintf("$2y$%02d$", $cost);
+ // The expected length of the final crypt() output
+ $resultLength = 60;
+ break;
+ default:
+ trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
+ return null;
+ }
+ $salt_req_encoding = false;
+ if (isset($options['salt'])) {
+ switch (gettype($options['salt'])) {
+ case 'NULL':
+ case 'boolean':
+ case 'integer':
+ case 'double':
+ case 'string':
+ $salt = (string) $options['salt'];
+ break;
+ case 'object':
+ if (method_exists($options['salt'], '__tostring')) {
+ $salt = (string) $options['salt'];
+ break;
+ }
+ case 'array':
+ case 'resource':
+ default:
+ trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
+ return null;
+ }
+ if (PasswordCompat\binary\_strlen($salt) < $required_salt_len) {
+ trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", PasswordCompat\binary\_strlen($salt), $required_salt_len), E_USER_WARNING);
+ return null;
+ } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
+ $salt_req_encoding = true;
+ }
+ } else {
+ $buffer = '';
+ $buffer_valid = false;
+ if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
+ $buffer = mcrypt_create_iv($raw_salt_len, MCRYPT_DEV_URANDOM);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
+ $buffer = openssl_random_pseudo_bytes($raw_salt_len);
+ if ($buffer) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid && @is_readable('/dev/urandom')) {
+ $file = fopen('/dev/urandom', 'r');
+ $read = PasswordCompat\binary\_strlen($buffer);
+ while ($read < $raw_salt_len) {
+ $buffer .= fread($file, $raw_salt_len - $read);
+ $read = PasswordCompat\binary\_strlen($buffer);
+ }
+ fclose($file);
+ if ($read >= $raw_salt_len) {
+ $buffer_valid = true;
+ }
+ }
+ if (!$buffer_valid || PasswordCompat\binary\_strlen($buffer) < $raw_salt_len) {
+ $buffer_length = PasswordCompat\binary\_strlen($buffer);
+ for ($i = 0; $i < $raw_salt_len; $i++) {
+ if ($i < $buffer_length) {
+ $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
+ } else {
+ $buffer .= chr(mt_rand(0, 255));
+ }
+ }
+ }
+ $salt = $buffer;
+ $salt_req_encoding = true;
+ }
+ if ($salt_req_encoding) {
+ // encode string with the Base64 variant used by crypt
+ $base64_digits =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ $bcrypt64_digits =
+ './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+ $base64_string = base64_encode($salt);
+ $salt = strtr(rtrim($base64_string, '='), $base64_digits, $bcrypt64_digits);
+ }
+ $salt = PasswordCompat\binary\_substr($salt, 0, $required_salt_len);
+
+ $hash = $hash_format . $salt;
+
+ $ret = crypt($password, $hash);
+
+ if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != $resultLength) {
+ return false;
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Get information about the password hash. Returns an array of the information
+ * that was used to generate the password hash.
+ *
+ * array(
+ * 'algo' => 1,
+ * 'algoName' => 'bcrypt',
+ * 'options' => array(
+ * 'cost' => PASSWORD_BCRYPT_DEFAULT_COST,
+ * ),
+ * )
+ *
+ * @param string $hash The password hash to extract info from
+ *
+ * @return array The array of information about the hash.
+ */
+ function password_get_info($hash) {
+ $return = array(
+ 'algo' => 0,
+ 'algoName' => 'unknown',
+ 'options' => array(),
+ );
+ if (PasswordCompat\binary\_substr($hash, 0, 4) == '$2y$' && PasswordCompat\binary\_strlen($hash) == 60) {
+ $return['algo'] = PASSWORD_BCRYPT;
+ $return['algoName'] = 'bcrypt';
+ list($cost) = sscanf($hash, "$2y$%d$");
+ $return['options']['cost'] = $cost;
+ }
+ return $return;
+ }
+
+ /**
+ * Determine if the password hash needs to be rehashed according to the options provided
+ *
+ * If the answer is true, after validating the password using password_verify, rehash it.
+ *
+ * @param string $hash The hash to test
+ * @param int $algo The algorithm used for new password hashes
+ * @param array $options The options array passed to password_hash
+ *
+ * @return boolean True if the password needs to be rehashed.
+ */
+ function password_needs_rehash($hash, $algo, array $options = array()) {
+ $info = password_get_info($hash);
+ if ($info['algo'] != $algo) {
+ return true;
+ }
+ switch ($algo) {
+ case PASSWORD_BCRYPT:
+ $cost = isset($options['cost']) ? $options['cost'] : PASSWORD_BCRYPT_DEFAULT_COST;
+ if ($cost != $info['options']['cost']) {
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Verify a password against a hash using a timing attack resistant approach
+ *
+ * @param string $password The password to verify
+ * @param string $hash The hash to verify against
+ *
+ * @return boolean If the password matches the hash
+ */
+ function password_verify($password, $hash) {
+ if (!function_exists('crypt')) {
+ trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
+ return false;
+ }
+ $ret = crypt($password, $hash);
+ if (!is_string($ret) || PasswordCompat\binary\_strlen($ret) != PasswordCompat\binary\_strlen($hash) || PasswordCompat\binary\_strlen($ret) <= 13) {
+ return false;
+ }
+
+ $status = 0;
+ for ($i = 0; $i < PasswordCompat\binary\_strlen($ret); $i++) {
+ $status |= (ord($ret[$i]) ^ ord($hash[$i]));
+ }
+
+ return $status === 0;
+ }
+ }
+
+}
+
+namespace PasswordCompat\binary {
+
+ if (!function_exists('PasswordCompat\\binary\\_strlen')) {
+
+ /**
+ * Count the number of bytes in a string
+ *
+ * We cannot simply use strlen() for this, because it might be overwritten by the mbstring extension.
+ * In this case, strlen() will count the number of *characters* based on the internal encoding. A
+ * sequence of bytes might be regarded as a single multibyte character.
+ *
+ * @param string $binary_string The input string
+ *
+ * @internal
+ * @return int The number of bytes
+ */
+ function _strlen($binary_string) {
+ if (function_exists('mb_strlen')) {
+ return mb_strlen($binary_string, '8bit');
+ }
+ return strlen($binary_string);
+ }
+
+ /**
+ * Get a substring based on byte limits
+ *
+ * @see _strlen()
+ *
+ * @param string $binary_string The input string
+ * @param int $start
+ * @param int $length
+ *
+ * @internal
+ * @return string The substring
+ */
+ function _substr($binary_string, $start, $length) {
+ if (function_exists('mb_substr')) {
+ return mb_substr($binary_string, $start, $length, '8bit');
+ }
+ return substr($binary_string, $start, $length);
+ }
+
+ /**
+ * Check if current PHP version is compatible with the library
+ *
+ * @return boolean the check result
+ */
+ function check() {
+ static $pass = NULL;
+
+ if (is_null($pass)) {
+ if (function_exists('crypt')) {
+ $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
+ $test = crypt("password", $hash);
+ $pass = $test == $hash;
+ } else {
+ $pass = false;
+ }
+ }
+ return $pass;
+ }
+
+ }
+}
-@require 'inc/wfpl/stylus-helpers.styl'
+@require 'inc/wfpl/stylus_helpers.styl'
@require '.sha1sums.styl'
// dimensions
nav#site-nav
padding-bottom: 20px
ul
- li-reset()
- space-evenly()
+ li_reset()
+ space_evenly()
footer
clear: both
padding-top: 40px
ul
- li-reset()
+ li_reset()
li
- li-reset()
+ li_reset()
display: inline-block
margin-right: 10px
margin-bottom: 0px
table.evenodd
+ td, th
+ padding: 6px 12px
+ text-align: left
> thead, > tbody, &
> tr:nth-child(2n+1)
> td, > th
&:hover
> td, > th
background: rgba(0,0,0,0.09)
+
+.field_error
+ input
+ border: 1px solid red
+
+.password_suggestion
+ & + &
+ margin-left: 10px
footer text here
</footer>
</div>
+ <!--~$render_timestamps {~-->
+ <script>
+ (function() {
+ var i02 = function (i) { return i > 9 ? i : '0' + i; };
+ var date_to_html = function (d, include_time) {
+ var hours = d.getHours()
+ var ret = '<span class="date_time">' +
+ i02(d.getMonth() + 1) + '/' +
+ i02(d.getDate()) + '/' +
+ d.getFullYear();
+ if (include_time) {
+ ret += ' ' + ((hours + 11) % 12 + 1) + ':' +
+ i02(d.getMinutes()) +
+ (hours < 12 ? 'am' : 'pm')
+ }
+ ret += '</span>';
+ return ret;
+ }
+ window.render_timestamps = function() {
+ var els = document.getElementsByClassName('unix_time');
+ var i, d, hours;
+ for (i in els) {
+ d = new Date(1000 * parseInt(els[i].innerHTML));
+ els[i].innerHTML = date_to_html(d, true);
+ }
+ els = document.getElementsByClassName('unix_date');
+ for (i in els) {
+ d = new Date(1000 * parseInt(els[i].innerHTML));
+ els[i].innerHTML = date_to_html(d, false);
+ }
+ }
+ }).call();
+ render_timestamps();
+ </script>
+ <!--~}~-->
</body>
</html>