Skip to content

Commit

Permalink
Access control (WIP)
Browse files Browse the repository at this point in the history
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
Kroc Camen committed Dec 1, 2011
1 parent bc03dcd commit 0bee588
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 175 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -1,5 +1,7 @@
*.rss
*.xml
sticky.txt
locked.txt
mods.txt
members.txt
config.php
11 changes: 11 additions & 0 deletions HISTORY.txt
@@ -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`)
Expand Down
59 changes: 39 additions & 20 deletions action.php
Expand Up @@ -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),
Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions config.example.php
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions index.php
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
150 changes: 104 additions & 46 deletions shared.php
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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));
}
}

?>

0 comments on commit 0bee588

Please sign in to comment.