Permalink
Browse files

Access control (WIP)

Work in progress of the access control feature:
* You can lock forums and limit posting / access to certain users:
(Members can be specified in a 'members.txt' file)
- 'threads':	Only moderators / members can start threads, but anybody can reply
- 'posts':	Only moderators / members can start threads or reply
- 'private':	Only moderators / members can access and participate in the forum (hidden to the public)
* Moderators can sign-in to do moderator actions
* Moderators can now reply to and append/delete in locked threads
* Moderators can now fully remove previously deleted (blanked-out) comments
  • Loading branch information...
1 parent bc03dcd commit 0bee588af09a31b60e2d1fd7f8dff2b908a96e58 Kroc Camen committed Dec 1, 2011
View
@@ -1,5 +1,7 @@
*.rss
*.xml
sticky.txt
+locked.txt
mods.txt
+members.txt
config.php
View
@@ -1,3 +1,14 @@
+v8
+* Access control: Major new feature! You can lock forums and limit posting / access to certain users:
+ (Members can be specified in a 'members.txt' file)
+ - 'threads': Only moderators / members can start threads, but anybody can reply
+ - 'posts': Only moderators / members can start threads or reply
+ - 'private': Only moderators / members can access and participate in the forum (hidden to the public)
+* Moderators can sign-in to do moderator actions
+* Moderators can now reply to and append/delete in locked threads
+* Moderators can now fully remove previously deleted (blanked-out) comments
+* Config option to disable new user registrations site-wide (`FORUM_NEWBIES`)
+
v7 05.NOV.11
* NNF can now be run from a folder, with thanks to Richard van Velzen
(this requires theme changes: URLs must be prepended with `FORUM_PATH`)
View
@@ -32,15 +32,22 @@
/* has the un/pw been submitted to authenticate the append?
-------------------------------------------------------------------------------------------------------------- */
- if (
- NAME && PASS && AUTH
- //only a moderator, or the post originator can append to a post
- && (isMod (NAME) || NAME == (string) $post->author)
- //cannot append to a deleted post
- && !(bool) $post->xpath ("category[text()='deleted']")
- //cannot append to a locked thread
- && !$xml->channel->xpath ("category[text()='locked']")
- ) {
+ if (AUTH && TEXT && FORUM_ENABLED && (
+ //- if the thread is unlocked and the forum is either unlocked or thread-locked (anybody can reply)
+ (!(bool) $xml->channel->xpath ("category[text()='locked']") && (!FORUM_LOCK || FORUM_LOCK == 'threads')) ||
+ //- if the thread is locked, but you are a moderator (signed in)
+ ((bool) $xml->channel->xpath ("category[text()='locked']") && CAN_MOD) ||
+ //- if the forum is post-locked, but you are a moderator (signed in) or member
+ (FORUM_LOCK == 'posts' && (CAN_MOD || isMember (NAME)))
+ ) && (
+ //a moderator can always append
+ isMod (NAME) ||
+ //the owner of a post can append
+ (strtolower (NAME) == strtolower ($post->author) && (
+ //if the forum is post-locked, they must be a member to append to their own posts
+ (!FORUM_LOCK || FORUM_LOCK == 'threads') || isMember (NAME)
+ ))
+ )) {
$now = time ();
$post->description .= "\n".template_tags (TEMPLATE_APPEND, array (
'AUTHOR' => safeHTML (NAME),
@@ -129,17 +136,29 @@
/* has the un/pw been submitted to authenticate the delete?
-------------------------------------------------------------------------------------------------------------- */
- if (
- NAME && PASS && AUTH
- //only a moderator, or the post originator can delete a post/thread
- && (isMod (NAME) || NAME == (string) $post->author)
- //cannot delete a locked thread
- && !$xml->channel->xpath ("category[text()='locked']")
-
+ if (AUTH && FORUM_ENABLED && (
+ //- if the thread is unlocked and the forum is either unlocked or thread-locked (anybody can reply)
+ (!(bool) $xml->channel->xpath ("category[text()='locked']") && (!FORUM_LOCK || FORUM_LOCK == 'threads')) ||
+ //- if the thread is locked, but you are a moderator (signed in)
+ ((bool) $xml->channel->xpath ("category[text()='locked']") && CAN_MOD) ||
+ //- if the forum is post-locked, but you are a moderator (signed in) or member
+ (FORUM_LOCK == 'posts' && (CAN_MOD || isMember (NAME)))
+ ) && (
+ //a moderator can always delete
+ isMod (NAME) ||
+ //the owner of a post can delete
+ (strtolower (NAME) == strtolower ($post->author) && (
+ //if the forum is post-locked, they must be a member to delete their own posts
+ (!FORUM_LOCK || FORUM_LOCK == 'threads') || isMember (NAME)
+ ))
//deleting a post?
- ) if ($ID) {
+ )) if ($ID) {
//full delete? (option ticked, is moderator, and post is on the last page)
- if (isset ($_POST['remove']) && isMod (NAME) && $i <= (count ($xml->channel->item)-2) % FORUM_POSTS) {
+ if (
+ (isMod (NAME) && $i <= (count ($xml->channel->item)-2) % FORUM_POSTS) &&
+ //if the post has already been blanked, delete it fully
+ (isset ($_POST['remove']) || $post->xpath ("category[text()='deleted']"))
+ ) {
//remove the post from the thread entirely
unset ($xml->channel->item[$i]);
@@ -225,15 +244,15 @@
$FILE = (preg_match ('/^[^.\/]+$/', @$_GET['file']) ? $_GET['file'] : '') or die ('Malformed request');
//get a write lock on the file so that between now and saving, no other posts could slip in
- $f = fopen ("$FILE.rss", 'c'); flock ($f, LOCK_EX);
+ $f = fopen ("$FILE.rss", 'c'); flock ($f, LOCK_EX);
$xml = simplexml_load_file ("$FILE.rss") or die ('Invalid file');
//what’s the current status?
$LOCKED = (bool) $xml->channel->xpath ("category[text()='locked']");
/* has the un/pw been submitted to authenticate the un/lock? (only a moderator can un/lock a thread)
-------------------------------------------------------------------------------------------------------------- */
- if (NAME && PASS && AUTH && isMod (NAME)) {
+ if (CAN_MOD && AUTH) {
if ($LOCKED) {
//if there’s a "locked" category, remove it
//note: for simplicity this removes *all* channel categories as NNF only uses one atm,
View
@@ -8,6 +8,9 @@
/* --- copy this file as 'config.php' and customise to your liking --- */
+//uncomment this if you want to show PHP errors in the browser
+#error_reporting (-1);
+
//forum’s title. used in theme, and in RSS feeds
//WARNING: changing this won’t update the index RSS feed containing this name; delete 'index.xml' and then post/delete
// a thread to regenerate the 'index.xml' file so as to see the change
View
@@ -18,7 +18,7 @@
/* ====================================================================================================================== */
//has the user submitted a new thread? (and is the info valid?)
-if (FORUM_ENABLED && NAME && PASS && AUTH && TITLE && TEXT && @$_POST['email'] == 'example@abc.com') {
+if (CAN_POST && AUTH && TITLE && TEXT && @$_POST['email'] == 'example@abc.com') {
//the file on disk is a simplified version of the title:
$translit = preg_replace (
//replace non alphanumerics with underscores and don’t use more than 2 in a row
@@ -79,7 +79,7 @@
/* ====================================================================================================================== */
/* sub-forums
---------------------------------------------------------------------------------------------------------------------- */
-//don’t all sub-sub-forums
+//don’t allow sub-sub-forums (yet)
if (!PATH) foreach (array_filter (
//get a list of folders:
//include only directories, but ignore directories starting with ‘.’ and the users / themes folders
@@ -161,7 +161,7 @@
/* new thread form
---------------------------------------------------------------------------------------------------------------------- */
//(exclude if posting has been disabled)
-if (FORUM_ENABLED) $FORM = array (
+if (CAN_POST) $FORM = array (
'NAME' => safeString (NAME),
'PASS' => safeString (PASS),
'TITLE' => safeString (TITLE),
View
@@ -5,22 +5,22 @@
you may do whatever you want to this code as long as you give credit to Kroc Camen, <camendesign.com>
*/
-//let me know when I’m being stupid
-error_reporting (-1);
-
//default UTF-8 throughout
mb_internal_encoding ('UTF-8');
mb_regex_encoding ('UTF-8');
/* constants: some stuff we don’t expect to change
---------------------------------------------------------------------------------------------------------------------- */
-define ('START', microtime (true)); //record how long the page takes to generate
define ('FORUM_ROOT', dirname (__FILE__)); //full server-path for absolute references
define ('FORUM_PATH', //relative from webroot--if running in a folder
str_replace ('//', '/', dirname ($_SERVER['SCRIPT_NAME']).'/') //(always starts with a slash and ends in one)
);
define ('FORUM_URL', 'http://'.$_SERVER['HTTP_HOST']); //todo: https support
+//for HTTP authentication (private forums)
+define ('HTTP_AUTH_UN', @$_SERVER['PHP_AUTH_USER']); //username if using HTTP authentication
+define ('HTTP_AUTH_PW', @$_SERVER['PHP_AUTH_PW']); //password if using HTTP authentication
+
//these are just some enums for templates to react to
define ('ERROR_NONE', 0);
define ('ERROR_NAME', 1); //name entered is invalid / blank
@@ -59,77 +59,115 @@
date_default_timezone_set (FORUM_TIMEZONE);
-/* get input
+/* common input
====================================================================================================================== */
-//all pages can accept a name / password when committing actions (new thread / post &c.)
-define ('NAME', safeGet (@$_POST['username'], SIZE_NAME));
-define ('PASS', safeGet (@$_POST['password'], SIZE_PASS, false));
+//all our pages use path (often optional) so this is done here
+define ('PATH', preg_match ('/[^.\/&]+/', @$_GET['path']) ? $_GET['path'] : '');
+//these two get used an awful lot
+define ('PATH_URL', !PATH ? FORUM_PATH : safeURL (FORUM_PATH.PATH.'/', false)); //when outputting as part of a URL
+define ('PATH_DIR', !PATH ? '/' : '/'.PATH.'/'); //serverside, like `chdir` / `unlink`
+
+//we have to change directory for `is_dir` to work, see <uk3.php.net/manual/en/function.is-dir.php#70005>
+//being in the right directory is also assumed for reading 'mods.txt' and when generating the RSS (`indexRSS`)
+//(oddly with `chdir` the path must end in a slash)
+@chdir (FORUM_ROOT.PATH_DIR) or die ("Invalid path");
+
-//if name & password are provided, validate them
-if (
+/* access control
+ ====================================================================================================================== */
+/* name / password authorisation:
+ ---------------------------------------------------------------------------------------------------------------------- */
+//all pages can accept a name / password when committing actions (new thread / post &c.)
+//in the case of HTTP authentication (sign in / private forums), these are provided in the request header
+define ('NAME', HTTP_AUTH_UN ? HTTP_AUTH_UN : safeGet (@$_POST['username'], SIZE_NAME));
+define ('PASS', HTTP_AUTH_PW ? HTTP_AUTH_PW : safeGet (@$_POST['password'], SIZE_PASS, false));
+
+if ((
+ //if any HTTP authentication is given, we don’t need to validate form fields
+ HTTP_AUTH_UN && HTTP_AUTH_PW
+) || (
+ //if an input form was submitted:
NAME && PASS &&
//the email check is a fake hidden field in the form to try and fool spam bots
isset ($_POST['email']) && @$_POST['email'] == 'example@abc.com' &&
//I wonder what this does...?
((isset ($_POST['x']) && isset ($_POST['y'])) || (isset ($_POST['submit_x']) && isset ($_POST['submit_y'])))
-) {
+)) {
//users are stored as text files based on the hash of the given name
$name = hash ('sha512', strtolower (NAME));
$user = FORUM_ROOT."/users/$name.txt";
- //create the user, if new (if registrations are allowed)
- if (FORUM_NEWBIES && !file_exists ($user)) file_put_contents ($user, hash ('sha512', $name.PASS));
+
+ //create the user, if new:
+ //- if registrations are allowed (`FORUM_NEWBIES`)
+ //- you can’t create new users with the HTTP_AUTH sign in
+ if (FORUM_NEWBIES && !HTTP_AUTH_UN && !file_exists ($user)) file_put_contents ($user, hash ('sha512', $name.PASS));
+
//does password match?
define ('AUTH', @file_get_contents ($user) == hash ('sha512', $name.PASS));
+
+ //if signed in with HTTP_AUTH, confirm that it’s okay to use
+ //(e.g. the user could still have given the wrong password with HTTP_AUTH)
+ define ('HTTP_AUTH', HTTP_AUTH_UN ? AUTH : false);
} else {
- define ('AUTH', false);
+ define ('AUTH', false);
+ define ('HTTP_AUTH', false);
}
-//all our pages use path (often optional) so this is done here
-define ('PATH', preg_match ('/[^.\/&]+/', @$_GET['path']) ? $_GET['path'] : '');
-//these two get used an awful lot
-define ('PATH_URL', !PATH ? FORUM_PATH : safeURL (FORUM_PATH.PATH.'/', false)); //when outputting as part of a URL
-define ('PATH_DIR', !PATH ? '/' : '/'.PATH.'/'); //serverside, like `chdir` / `unlink`
+//get the lock status of the current forum we’re in:
+//"threads" - only users in "mods.txt" / "members.txt" can start threads, but anybody can reply
+//"posts" - only users in "mods.txt" / "members.txt" can start threads or reply
+//"private" - only users in "mods.txt" / "members.txt" can enter and use the forum, it is hidden from everybody else
+define ('FORUM_LOCK', trim (@file_get_contents ('locked.txt')));
-//we have to change directory for `is_dir` to work, see <uk3.php.net/manual/en/function.is-dir.php#70005>
-//being in the right directory is also assumed for reading 'mods.txt' and when generating the RSS (`indexRSS`)
-//(oddly with `chdir` the path must end in a slash)
-@chdir (FORUM_ROOT.PATH_DIR) or die ("Invalid path");
+//if the sign-in link was clicked, invoke a HTTP_AUTH request in the browser
+if (!HTTP_AUTH && isset ($_GET['login'])) {
+ header ('WWW-Authenticate: Basic');
+ header ('HTTP/1.0 401 Unauthorized');
+}
+/* access rights
+ ---------------------------------------------------------------------------------------------------------------------- */
//get the list of moderators:
$MODS = array (
- //mods.txt on root for mods on all sub-forums
+ //'mods.txt' on root for mods on all sub-forums
'GLOBAL'=> file_exists (FORUM_ROOT.'/mods.txt')
? file (FORUM_ROOT.'/mods.txt', FILE_IGNORE_NEW_LINES + FILE_SKIP_EMPTY_LINES)
: array (),
- //if in a sub-forum, the local mods.txt
+ //if in a sub-forum, the local 'mods.txt'
'LOCAL' => PATH && file_exists ('mods.txt')
? file ('mods.txt', FILE_IGNORE_NEW_LINES + FILE_SKIP_EMPTY_LINES)
: array ()
);
+//get the list (if any) of users allowed to access this current forum
+$MEMBERS = file_exists ('members.txt') ? file ('members.txt', FILE_IGNORE_NEW_LINES + FILE_SKIP_EMPTY_LINES) : array ();
+
+//can the current user moderate in this forum?
+define ('CAN_MOD', HTTP_AUTH ? isMod (NAME) : false);
+
+//can the current user post new threads in the current forum?
+//(posting replies is dependent on the the thread -- if locked -- so tested in 'thread.php')
+define ('CAN_POST', FORUM_ENABLED && (
+ //- if the user is a moderator or member of the current forum, they can post
+ CAN_MOD || isMember (NAME) ||
+ //- if the forum is unlocked (mods will have to log in to see the form)
+ !FORUM_LOCK
+));
+
+//if the forum is private, check the current user and issue an auth request if not signed in or allowed
+if (FORUM_LOCK == 'private' && !(CAN_MOD || isMember (NAME))) {
+ header ('WWW-Authenticate: Basic');
+ header ('HTTP/1.0 401 Unauthorized');
+ //todo: a proper error page, if I make a splash/login screen for a private root-forum
+ die ("Authorisation required.");
+}
/* ---------------------------------------------------------------------------------------------------------------------- */
//stop browsers caching, so you don’t have to refresh every time to see changes
header ('Cache-Control: no-cache', true);
header ('Expires: 0', true);
-
-/* ====================================================================================================================== */
-
-//<stackoverflow.com/questions/2092012/simplexml-how-to-prepend-a-child-in-a-node/2093059#2093059>
-//we could of course do all the XML manipulation in DOM proper to save doing this…
-class allow_prepend extends SimpleXMLElement {
- public function prependChild ($name, $value=null) {
- $dom = dom_import_simplexml ($this);
- $new = $dom->insertBefore (
- $dom->ownerDocument->createElement ($name, $value),
- $dom->firstChild
- );
- return simplexml_import_dom ($new, get_class ($this));
- }
-}
-
/* ====================================================================================================================== */
//sanitise input:
@@ -165,7 +203,7 @@ function template_tags ($template, $values) {
return $template;
}
-//produces a truncated list of pages around the current page
+//produces a truncated list of page numbers around the current page
function pageList ($current, $total) {
//always include the first page
$PAGES[] = 1;
@@ -273,12 +311,17 @@ function formatText ($text) {
/* ====================================================================================================================== */
-//check to see if a name is a known moderator in mods.txt
+//check to see if a name is a known moderator in 'mods.txt'
function isMod ($name) {
global $MODS;
return in_array (strtolower ($name), array_map ('strtolower', $MODS['GLOBAL'] + $MODS['LOCAL']));
}
+function isMember ($name) {
+ global $MEMBERS;
+ return in_array (strtolower ($name), array_map ('strtolower', $MEMBERS));
+}
+
/* ====================================================================================================================== */
//regenerate a folder's RSS file (all changes happening in a folder)
@@ -330,11 +373,11 @@ function indexRSS () {
/* sitemap
-------------------------------------------------------------------------------------------------------------- */
+ //we’re going to use the RSS files as sitemaps
chdir (FORUM_ROOT);
- //we’re going to use the RSS files as sitemaps
- $folders = array ('');
//get list of sub-forums
+ $folders = array ('');
foreach (array_filter (
//include only directories, but ignore directories starting with ‘.’ and the users / themes folders
preg_grep ('/^(\.|users$|themes$)/', scandir (FORUM_ROOT.'/'), PREG_GREP_INVERT), 'is_dir'
@@ -370,4 +413,19 @@ function indexRSS () {
clearstatcache ();
}
-?>
+/* ====================================================================================================================== */
+
+//<stackoverflow.com/questions/2092012/simplexml-how-to-prepend-a-child-in-a-node/2093059#2093059>
+//we could of course do all the XML manipulation in DOM proper to save doing this…
+class allow_prepend extends SimpleXMLElement {
+ public function prependChild ($name, $value=null) {
+ $dom = dom_import_simplexml ($this);
+ $new = $dom->insertBefore (
+ $dom->ownerDocument->createElement ($name, $value),
+ $dom->firstChild
+ );
+ return simplexml_import_dom ($new, get_class ($this));
+ }
+}
+
+?>
Oops, something went wrong. Retry.

0 comments on commit 0bee588

Please sign in to comment.