diff --git a/build/index.php b/build/index.php index fe5d4e4..cecc814 100644 --- a/build/index.php +++ b/build/index.php @@ -61,6 +61,7 @@ $guiConfig = <<<'GUICONFIG' "password_cost_time": {"type": "number", "description": "The desired number of milliseconds to delay by when hashing passwords. Pepperminty Wiki will automatically update the value of password_cost to take the length of time specified here. If you're using PASSWORD_ARGON2I, then the auto-update will be disabled.", "default": 100}, "password_cost_time_interval": {"type": "number", "description": "The interval, in seconds, at which the password cost should be recalculated. Set to -1 to disable. Default: 1 week", "default": 604800}, "password_cost_time_lastcheck": {"type": "number", "description": "Pseudo-setting used to keep track of the last recalculation of password_cost. Is updated with the current unix timestamp every time password_cost is recalculated.", "default": 0}, + "new_password_length": {"type": "number", "description": "The length of newly-generated passwords. This is currently used in the user table when creating new accounts.", "default": 32}, "require_login_view": {"type": "checkbox", "description": "Whether to require that users login before they do anything else. Best used with the data_storage_dir option.", "default": false}, "data_storage_dir": {"type": "text", "description": "The directory in which to store all files, except the main index.php.", "default": "."}, "delayed_indexing_time": {"type": "number", "description": "The amount of time, in seconds, that pages should be blocked from being indexed by search engines after their last edit. Aka delayed indexing.", "default": 0}, @@ -204,7 +205,7 @@ if(!file_exists($settingsFilename)) foreach ($guiConfig as $key => $value) $settings->$key = $value->default; // Generate a random secret - $settings->secret = bin2hex(openssl_random_pseudo_bytes(16)); + $settings->secret = bin2hex(random_bytes(16)); file_put_contents("peppermint.json", json_encode($settings, JSON_PRETTY_PRINT)); } else @@ -395,7 +396,7 @@ if($settings->sessionprefix == "auto") ///////////////////////////////////////////////////////////////////////////// /** The version of Pepperminty Wiki currently running. */ $version = "v0.17-dev"; -$commit = "62dff18b4d1785b1ff8544b0e554af0f8ce6ab92"; +$commit = "e11766bbe1276b1515b608827fd8a49ae700ce09"; /// Environment /// /** Holds information about the current request environment. */ $env = new stdClass(); @@ -5846,6 +5847,133 @@ register_module([ + +register_module([ + "name" => "User Organiser", + "version" => "0.1", + "author" => "Starbeamrainbowlabs", + "description" => "Adds a organiser page that lets moderators (or better) control the reegistered user accounts, and perform adminstrative actions such as password resets, and adding / removing accounts.", + "id" => "feature-user-table", + "code" => function() { + global $settings, $env; + + /** + * @api {get} ?action=user-table Get the user table + * @apiName UserTable + * @apiGroup Settings + * @apiPermission Moderator + */ + + /* + * ██ ██ ███████ ███████ ██████ + * ██ ██ ██ ██ ██ ██ + * ██ ██ ███████ █████ ██████ █████ + * ██ ██ ██ ██ ██ ██ + * ██████ ███████ ███████ ██ ██ + * + * ████████ █████ ██████ ██ ███████ + * ██ ██ ██ ██ ██ ██ ██ + * ██ ███████ ██████ ██ █████ + * ██ ██ ██ ██ ██ ██ ██ + * ██ ██ ██ ██████ ███████ ███████ + */ + add_action("user-table", function() { + global $settings, $env; + + if(!$env->is_logged_in || !$env->is_admin) { + http_response_code(401); + exit(page_renderer::render_main("Unauthorised - User Table - $settings->sitename", "

Only moderators (or better) may access the user table. You could try logging out and then logging in again as a moderator, or alternatively visit the user list instead, if that's what you're after.

")); + } + + $content = "

User Table

+

(Warning! Deleting a user will wipe all their user data! It won't delete any pages they've created, their user page, or their avatar though, as those are part of the wiki itself.)

+ + \n"; + + foreach($settings->users as $username => $user_data) { + $content .= ""; + $content .= ""; + if(!empty($user_data->email)) + $content .= "\n"; + else + $content .= "\n"; + $content .= ""; + } + + $content .= "
UsernameEmail Address
" . page_renderer::render_username($username) . "" . htmlentities($user_data->email) . "(None provided)"; + if(module_exists("feature-user-preferences")) + $content .= "
+ + + +
| "; + $content .= "Delete User"; + $content .= "
\n"; + + $content .= "

Add User

+
+ + + +
"; + + exit(page_renderer::render_main("User Table - $settings->sitename", $content)); + }); + + add_action("user-add", function() { + global $settings, $env; + + if(!$env->is_admin) { + http_response_code(401); + exit(page_renderer::render_main("Error - Unauthorised - $settings->sitename", "

Only moderators (or better) may create users. You could try logging out and then logging in again as a moderator, or alternatively visit the user list instead, if that's what you're after.

")); + } + + if(!isset($_POST["user"])) { + http_response_code(400); + header("content-type: text/plain"); + exit("Error: No username specified in the 'user' post parameter."); + } + + $new_username = $_POST["user"]; + $new_email = $_POST["email"] ?? null; + + // TODO: Validate & sanitize username / email + + $new_password = generate_password($settings->new_password_length); + + $user_data = new stdClass(); + $user_data->password = hash_password($new_password); + if(!empty($new_email)) + $user_data->email = $new_email; + + $settings->users->$new_username = $user_data; + + // TODO: Save new user's data, display the password to the admin, and send email if we're able to + + }); + + if($env->is_admin) add_help_section("949-user-table", "Managing User Accounts", "

As a moderator on $settings->sitename, you can use the User Table to adminstrate the user accounts on $settings->sitename. It allows you to perform actions such as adding and removing accounts, and resetting passwords.

"); + } +]); +/** + * Generates a new (cryptographically secure) random password that's also readable (i.e. consonant-vowel-consonant). + * This implementation may be changed in the future to use random dictionary words instead - ref https://xkcd.com/936/ + * @param string $length The length of password to generate. + * @return string The generated random password. + */ +function generate_password($length) { + $vowels = "aeiou"; + $consonants = "bcdfghjklmnpqrstvwxyz"; + $result = ""; + for($i = 0; $i < $length; $i++) { + if($i % 2 == 0) + $result .= $consonants[random_int(0, strlen($consonants) - 1)]; + else + $result .= $vowels[random_int(0, strlen($vowels) - 1)]; + } + return $result; +} + register_module([ "name" => "Credits", @@ -6198,7 +6326,7 @@ register_module([ register_module([ "name" => "Page editor", - "version" => "0.17.2", + "version" => "0.17.3", "author" => "Starbeamrainbowlabs", "description" => "Allows you to edit pages by adding the edit and save actions. You should probably include this one.", "id" => "page-edit", @@ -6728,7 +6856,9 @@ DIFFSCRIPT;

Editing is simple. The edit page has a sizeable box that contains a page's current contents. Once you are done altering it, add or change the comma separated list of tags in the field below the editor and then click save page.

A reference to the syntax that $settings->sitename supports can be found below.

"); - add_help_section("17-user-pages", "User Pages", "

If you are logged in, $settings->sitename allocates you your own user page that only you can edit. On $settings->sitename, user pages are sub-pages of the " . htmlentities($settings->user_page_prefix) . " page, and each user page can have a nested structure of pages underneath it, just like a normal page. Your user page is located at " . htmlentities(get_user_pagename($env->user)) . ".

"); + add_help_section("17-user-pages", "User Pages", "

If you are logged in, $settings->sitename allocates you your own user page that only you can edit. On $settings->sitename, user pages are sub-pages of the " . htmlentities($settings->user_page_prefix) . " page, and each user page can have a nested structure of pages underneath it, just like a normal page. Your user page is located at " . htmlentities(get_user_pagename($env->user)) . ". " . + (module_exists("page-user-list") ? "You can see a list of all the users on $settings->sitename and visit their user pages on the user list." : "") + . "

"); } ]); diff --git a/module_index.json b/module_index.json index d9dac97..50b86d0 100755 --- a/module_index.json +++ b/module_index.json @@ -134,6 +134,15 @@ "lastupdate": 1526035213, "optional": false }, + { + "name": "User Organiser", + "version": "0.1", + "author": "Starbeamrainbowlabs", + "description": "Adds a organiser page that lets moderators (or better) control the reegistered user accounts, and perform adminstrative actions such as password resets, and adding \/ removing accounts.", + "id": "feature-user-table", + "lastupdate": 1526077752, + "optional": false + }, { "name": "Credits", "version": "0.7.7", @@ -163,11 +172,11 @@ }, { "name": "Page editor", - "version": "0.17.2", + "version": "0.17.3", "author": "Starbeamrainbowlabs", "description": "Allows you to edit pages by adding the edit and save actions. You should probably include this one.", "id": "page-edit", - "lastupdate": 1524416238, + "lastupdate": 1526037910, "optional": false }, { @@ -203,7 +212,7 @@ "author": "Starbeamrainbowlabs", "description": "Adds a pair of actions (login and checklogin) that allow users to login. You need this one if you want your users to be able to login.", "id": "page-login", - "lastupdate": 1526034977, + "lastupdate": 1526038530, "optional": false }, { diff --git a/modules/feature-user-table.php b/modules/feature-user-table.php new file mode 100644 index 0000000..daef63a --- /dev/null +++ b/modules/feature-user-table.php @@ -0,0 +1,126 @@ + "User Organiser", + "version" => "0.1", + "author" => "Starbeamrainbowlabs", + "description" => "Adds a organiser page that lets moderators (or better) control the reegistered user accounts, and perform adminstrative actions such as password resets, and adding / removing accounts.", + "id" => "feature-user-table", + "code" => function() { + global $settings, $env; + + /** + * @api {get} ?action=user-table Get the user table + * @apiName UserTable + * @apiGroup Settings + * @apiPermission Moderator + */ + + /* + * ██ ██ ███████ ███████ ██████ + * ██ ██ ██ ██ ██ ██ + * ██ ██ ███████ █████ ██████ █████ + * ██ ██ ██ ██ ██ ██ + * ██████ ███████ ███████ ██ ██ + * + * ████████ █████ ██████ ██ ███████ + * ██ ██ ██ ██ ██ ██ ██ + * ██ ███████ ██████ ██ █████ + * ██ ██ ██ ██ ██ ██ ██ + * ██ ██ ██ ██████ ███████ ███████ + */ + add_action("user-table", function() { + global $settings, $env; + + if(!$env->is_logged_in || !$env->is_admin) { + http_response_code(401); + exit(page_renderer::render_main("Unauthorised - User Table - $settings->sitename", "

Only moderators (or better) may access the user table. You could try logging out and then logging in again as a moderator, or alternatively visit the user list instead, if that's what you're after.

")); + } + + $content = "

User Table

+

(Warning! Deleting a user will wipe all their user data! It won't delete any pages they've created, their user page, or their avatar though, as those are part of the wiki itself.)

+ + \n"; + + foreach($settings->users as $username => $user_data) { + $content .= ""; + $content .= ""; + if(!empty($user_data->email)) + $content .= "\n"; + else + $content .= "\n"; + $content .= ""; + } + + $content .= "
UsernameEmail Address
" . page_renderer::render_username($username) . "" . htmlentities($user_data->email) . "(None provided)"; + if(module_exists("feature-user-preferences")) + $content .= "
+ + + +
| "; + $content .= "Delete User"; + $content .= "
\n"; + + $content .= "

Add User

+
+ + + +
"; + + exit(page_renderer::render_main("User Table - $settings->sitename", $content)); + }); + + add_action("user-add", function() { + global $settings, $env; + + if(!$env->is_admin) { + http_response_code(401); + exit(page_renderer::render_main("Error - Unauthorised - $settings->sitename", "

Only moderators (or better) may create users. You could try logging out and then logging in again as a moderator, or alternatively visit the user list instead, if that's what you're after.

")); + } + + if(!isset($_POST["user"])) { + http_response_code(400); + header("content-type: text/plain"); + exit("Error: No username specified in the 'user' post parameter."); + } + + $new_username = $_POST["user"]; + $new_email = $_POST["email"] ?? null; + + // TODO: Validate & sanitize username / email + + $new_password = generate_password($settings->new_password_length); + + $user_data = new stdClass(); + $user_data->password = hash_password($new_password); + if(!empty($new_email)) + $user_data->email = $new_email; + + $settings->users->$new_username = $user_data; + + // TODO: Save new user's data, display the password to the admin, and send email if we're able to + + }); + + if($env->is_admin) add_help_section("949-user-table", "Managing User Accounts", "

As a moderator on $settings->sitename, you can use the User Table to adminstrate the user accounts on $settings->sitename. It allows you to perform actions such as adding and removing accounts, and resetting passwords.

"); + } +]); +/** + * Generates a new (cryptographically secure) random password that's also readable (i.e. consonant-vowel-consonant). + * This implementation may be changed in the future to use random dictionary words instead - ref https://xkcd.com/936/ + * @param string $length The length of password to generate. + * @return string The generated random password. + */ +function generate_password($length) { + $vowels = "aeiou"; + $consonants = "bcdfghjklmnpqrstvwxyz"; + $result = ""; + for($i = 0; $i < $length; $i++) { + if($i % 2 == 0) + $result .= $consonants[random_int(0, strlen($consonants) - 1)]; + else + $result .= $vowels[random_int(0, strlen($vowels) - 1)]; + } + return $result; +} diff --git a/modules/page-edit.php b/modules/page-edit.php index 31e2929..2b65da6 100644 --- a/modules/page-edit.php +++ b/modules/page-edit.php @@ -1,7 +1,7 @@ "Page editor", - "version" => "0.17.2", + "version" => "0.17.3", "author" => "Starbeamrainbowlabs", "description" => "Allows you to edit pages by adding the edit and save actions. You should probably include this one.", "id" => "page-edit", @@ -531,7 +531,9 @@ DIFFSCRIPT;

Editing is simple. The edit page has a sizeable box that contains a page's current contents. Once you are done altering it, add or change the comma separated list of tags in the field below the editor and then click save page.

A reference to the syntax that $settings->sitename supports can be found below.

"); - add_help_section("17-user-pages", "User Pages", "

If you are logged in, $settings->sitename allocates you your own user page that only you can edit. On $settings->sitename, user pages are sub-pages of the " . htmlentities($settings->user_page_prefix) . " page, and each user page can have a nested structure of pages underneath it, just like a normal page. Your user page is located at " . htmlentities(get_user_pagename($env->user)) . ".

"); + add_help_section("17-user-pages", "User Pages", "

If you are logged in, $settings->sitename allocates you your own user page that only you can edit. On $settings->sitename, user pages are sub-pages of the " . htmlentities($settings->user_page_prefix) . " page, and each user page can have a nested structure of pages underneath it, just like a normal page. Your user page is located at " . htmlentities(get_user_pagename($env->user)) . ". " . + (module_exists("page-user-list") ? "You can see a list of all the users on $settings->sitename and visit their user pages on the user list." : "") + . "

"); } ]); diff --git a/peppermint.guiconfig.json b/peppermint.guiconfig.json index 02cde94..9b58572 100644 --- a/peppermint.guiconfig.json +++ b/peppermint.guiconfig.json @@ -38,6 +38,7 @@ "password_cost_time": {"type": "number", "description": "The desired number of milliseconds to delay by when hashing passwords. Pepperminty Wiki will automatically update the value of password_cost to take the length of time specified here. If you're using PASSWORD_ARGON2I, then the auto-update will be disabled.", "default": 100}, "password_cost_time_interval": {"type": "number", "description": "The interval, in seconds, at which the password cost should be recalculated. Set to -1 to disable. Default: 1 week", "default": 604800}, "password_cost_time_lastcheck": {"type": "number", "description": "Pseudo-setting used to keep track of the last recalculation of password_cost. Is updated with the current unix timestamp every time password_cost is recalculated.", "default": 0}, + "new_password_length": {"type": "number", "description": "The length of newly-generated passwords. This is currently used in the user table when creating new accounts.", "default": 32}, "require_login_view": {"type": "checkbox", "description": "Whether to require that users login before they do anything else. Best used with the data_storage_dir option.", "default": false}, "data_storage_dir": {"type": "text", "description": "The directory in which to store all files, except the main index.php.", "default": "."}, "delayed_indexing_time": {"type": "number", "description": "The amount of time, in seconds, that pages should be blocked from being indexed by search engines after their last edit. Aka delayed indexing.", "default": 0}, diff --git a/settings.fragment.php b/settings.fragment.php index c580b9a..d5027d0 100644 --- a/settings.fragment.php +++ b/settings.fragment.php @@ -35,7 +35,6 @@ if(!file_exists($settingsFilename)) foreach ($guiConfig as $key => $value) $settings->$key = $value->default; // Generate a random secret - // Updated to use random_bytes - ref https://paragonie.com/blog/2015/07/how-safely-generate-random-strings-and-integers-in-php $settings->secret = bin2hex(random_bytes(16)); file_put_contents("peppermint.json", json_encode($settings, JSON_PRETTY_PRINT)); }