diff options
59 files changed, 5607 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cee3653 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +msg/* +*.bak +*~ +conf.php diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..ce311c9 --- /dev/null +++ b/.htaccess @@ -0,0 +1,9 @@ +<IfModule mod_rewrite.c> + RewriteEngine On + RewriteBase /1/frc1024mail/ + + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule (.*) index.php?p=$1 [L,QSA] +</IfModule> + diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..5d674af --- /dev/null +++ b/README.txt @@ -0,0 +1,115 @@ +MessageManager: README +====================== + +MessageManager is a mailing list program, much like GNU Mailman, but +with sub-lists, SMS messages, and easier configuration. Also, social +networking. + +MVC/ICM/PAC +----------- + +So there's a bit of a controversy on what MVC actually is (namely, the +C). `Controller' is an ambiguous word, it means something different +in each of the above acronyms. + +A common (mis)interpretation of MVC is actually more correctly +described as ICM. In this (mis)interpretation `controller' is the +logic glue between the model and the view, where the original/correct +interpretation of MVC has the controller being the user's feedback, +which is considered part of of the view in this (mis)intrepretation. +This (mis)interpretation is ICM (view=interface). Because of this, in +several places (here, and in code), I refer to interfaces as views. So +sue me. + +MessageManager sort of has a interface-controller-model. + +But it also has a bit of a God Class going on. This might be +considered the controller in a PAC architecture. But that would +require me to rework this section, and I want to get back to coding. + +The relationship between ICM and PAC is: + ICM + P AC + +We've got our main, "God" class, `MessageManager'. It basically does +three things: + 1. abstract away all database access + 2. handle authentication (which is just #1 with password hashing) + 3. serve as a factory for all the other resources we may need + +There are 4 objects that are models: + - User + - Group + - Message + - Plugin +These have a little logic in them, but are mostly just wrappers around +the various database getX and setX methods in MessageManager. The +coolest thing that they do is handle permissions on whether the +currently logged in user can read or write to them. + +The interface is in the directory `src/views' (the directory name +comes from the incorrect interpretation of MVC). The Template class +provides a pretty low-level wrapper for (X)HTML, that should make +converting fairly painless, and makes it easier to generate pretty, +valid markup. It also handles a few common cases (mostly form stuff). +These do a lot of what could be considered belonging to a controller, +but that is because most of what they do is directly operate on the +models, and any controller behavior is just validating/parsing data +from the view, and is view-specific. There were too many `and's in +that last sentence. + +The controllers are basically made up of MessageHandler and the +plugins (which plugins to MessageHandler). They are in charge of +parsing incoming messages, storing them into our message store, and +sending them out to recipients. + +RESTful +------- + +MessageManager is RESTful in design. + +Here is a table(?) of all URIs in this application, and what HTTP verbs +they can each be expected to handle. + +- index GET the homepage + |- auth GET the current auth state + | PUT user credentials + |- messages GET a list of all messages + | | POST a new message + | `- <msgid> GET a representation of <msgid> + |- plugins GET the current plugin configuration + | PUT an updated plugin configuration + `- users GET a list of all users + | POST a new user + | PUT an updated user index + |- new GET the form for a new user + `- <user> GET <user>'s info + PUT updated user info + +Now, there is only one URI that is expected to handle both POST and +PUT (`users'), let's ignore it for a moment. No URI that is expected +to handle both POST and PUT. I haven't done this intentionally, but +it works out well, because it means that I can treat them +interchangeably. This is nice because current web technologies make +it a pain in the butt to send/handle PUT requests, so I can just do +POST requests, even if it's the wrong thing to use. So it doesn't +follow HTTP's original design, it's still RESTful, it just uses +different semantics to decide which verb to use. + +Ok, now, that tricky `users' URI. I've handled that it must handle +PUT and POST by noting that it is accessable at two URIs, `users' and +`users/index', and assigning one to each. `users' handles POSTing new +users, and `users/index' handles PUTing an updated user index. + +BUGS/TODO +--------- + +When creating a new user, if something goes wrong (illegal/existing +name, password missmatch), it isn't "reported", and the user will be +sitting at "users/new" without any feedback about what went wrong. + +The End +------- + +Happy Hacking! +~ Luke Shumaker diff --git a/index.php b/index.php new file mode 100644 index 0000000..c8f72a6 --- /dev/null +++ b/index.php @@ -0,0 +1,44 @@ +<?php +// What directory are we in on the server. +define('BASEPATH', dirname(__FILE__)); + +// Decide where to look for things +define('LIBPATH', BASEPATH.'/src/lib'.PATH_SEPARATOR.BASEPATH.'/src/ext'); +define('MODELPATH', BASEPATH.'/src/models'); +define('VIEWPATH', BASEPATH.'/src/views');// views are not objects +define('CONTROLLERPATH', BASEPATH.'/src/controllers'); + +// Modify our include path to catch our class files. +set_include_path(get_include_path() + .PATH_SEPARATOR.LIBPATH + .PATH_SEPARATOR.MODELPATH + .PATH_SEPARATOR.CONTROLLERPATH + ); + +// Figure what page is trying to be loaded. Don't worry if we're +// looking for a real file, if the requested page exists as a real +// file, .htaccess won't even let us load this file. +@$PAGE_RAW = $_GET['p']; +preg_match('@(.*)(\.([^./]))?@', $PAGE_RAW, $matches); +@$PAGE = $matches[1]; +@$EXT = $matches[3]; +@$ACCEPT = $_SERVER['HTTP_ACCEPT']; +if ($PAGE=='') + $PAGE = 'index'; +if ($EXT!='') + $ACCEPT = "$EXT, $ACCEPT"; +define('PAGE', $PAGE); unset($PAGE); +define('ACCEPT', $ACCEPT); unset($ACCEPT); + +// Get ready +require_once('Model.class.php'); +require_once('Controller.class.php'); +require_once('Router.class.php'); + +global $mm; +require_once('MessageManager.class.php'); +$mm = new MessageManager(BASEPATH.'/conf.php'); + +// Actually do stuff +$router = new Router(CONTROLLERPATH); +$router->route(PAGE); diff --git a/installer/include.php b/installer/include.php new file mode 100644 index 0000000..7300e90 --- /dev/null +++ b/installer/include.php @@ -0,0 +1,81 @@ +<?php + +function mm_getParam($name, $default='') { + if (isset($_POST[$name])) { + return $_POST[$name]; + } else { + return $default; + } +} + +function mm_configStr($param) { + return "\$db_config['$param'] = \"".$_POST["db_$param"]."\";\n"; +} + +function mm_isSqlConfigured($conf_file) { + if (file_exists($conf_file)) { + global $db_config; + require($conf_file); + if (isset($db_config)) { + unset($db_config); + return true; + } + } + return false; +} + +function mm_mysql_create_db($mysql, $db_name, &$r) { + global $t; + if ($mysql) { + $db_list = mysql_list_dbs($mysql); + $db_array = Array(); + while ($row = mysql_fetch_object($db_list)) { + $db_array[] = $row->Database . ''; + } + $r.=$t->inputP("Existing databases: ".implode(', ',$db_array)); + + if (!in_array($db_name, $db_array)) { + $str.=$t->inputP("Creating database <q>$db_name</q>..."); + $db = mysql_query("CREATE DATABASE $db_name;", $mysql); + if ($db===FALSE) { + $str.=$t->inputP("Database <q>$db_name</q> ". + "could not be created: ". + mysql_error($mysql), true); + return false; + } + } + $r.=$t->inputP("Selecting database <q>$db_name</q>..."); + $db = mysql_select_db($db_name, $mysql); + if (!$db) { + $r.=$t->inputP('Could not select database: ', + mysql_error($mysql), true); + return false; + } + return true; + } else { + return false; + } +} + +function mm_mysql_count_rows_in_table($mysql, $table_name) { + $table=mysql_real_escape_string($table_name); + $query = + "SELECT COUNT(*)\n". + "FROM $table;"; + $total = mysql_query($query, $mysql); + $total = mysql_fetch_array($total); + $total = $total[0]; + return $total; +} + +function mm_mysql_table_exists($mysql, $table_name) { + $table=mysql_real_escape_string($table_name); + $query = + "SELECT COUNT(*)\n". + "FROM information_schema.tables\n". + "WHERE table_name = '$table';"; + $total = mysql_query($query, $mysql); + $total = mysql_fetch_array($total); + $total = $total[0]; + return $total>0; +} diff --git a/installer/index.php b/installer/index.php new file mode 100644 index 0000000..5ea418b --- /dev/null +++ b/installer/index.php @@ -0,0 +1,235 @@ +<?php + +$BASE = dirname(dirname(__FILE__)); +set_include_path(get_include_path() + .PATH_SEPARATOR. "$BASE/src/lib" + .PATH_SEPARATOR. "$BASE/src/ext" + ); +$uri = $_SERVER['REQUEST_URI']; + +require_once('include.php'); + +$conf_file = "$BASE/conf.php"; +if (!mm_isSqlConfigured($conf_file)) { + require_once('Template.class.php'); + $t = new Template($BASE); + + $t->header('Message Manager: Installer'); + + $t->paragraph("First we need to set up the SQL configuration, ". + "then we will set up the first user."); + + $t->openTag('form', array('method'=>'post','action'=>$uri)); + $t->tag('input', array('type'=>'hidden', 'name'=>'try', 'value'=>'t')); + $try = isset($_POST['try']); + + $mysql = false; + if ($try) { + @$mysql = mysql_connect($_POST['db_host'], + $_POST['db_user'], + $_POST['db_password']); + } + + //////////////////////////////////////////////////////////////////////// + + $t->openFieldset("MySQL Authentication", $mysql); + $t->inputText('db_host', 'Hostname', '', + getParam('db_host','localhost'), $mysql); + $t->inputText('db_user', 'Username', '', + getParam('db_user'), $mysql); + $t->inputPassword('db_password', 'Password', '', + getParam('db_password'), $mysql); + if ($try && !$mysql) { + $t->inputP("Could not authenticate: ".mysql_error(), true); + } + $t->closeFieldset(); + + //////////////////////////////////////////////////////////////////////// + + $charset = false; + if ($mysql) { + $charset = mysql_set_charset($_POST['db_charset'], $mysql); + if (!$charset) { + $charset_error = mysql_error($mysql); + } + } + + $db = false; + $db_message = ''; + if ($charset) { + $t->setRet(true); + $db = mm_mysql_db($mysql, $_POST['db_name'], $db_message); + $t->setRet(false); + } + + $db_prefix = $_POST['db_prefix']; + + $table = false; + if ($db) { + $table_exists = mm_mysql_table_exists($mysql,$db_prefix.'auth'); + if (!$table_exists) { + $query = + 'CREATE TABLE '.$db_prefix."auth (\n". + " uid INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,\n". + " name VARCHAR(255),\n". + " hash CHAR(60)\n". + " status INT\n" + ");"; + $table = mysql_query($query); + if (!$table) { + $table_error = mysql_error($mysql); + } + } else { + $table = true; + } + } + + //////////////////////////////////////////////////////////////////////// + + $t->openFieldset("MySQL Settings", $table); + + $t->inputText('db_charset', 'Charset', + "I've heard that you may need to change this if ". + "you use an old version of MySQL. 'utf8' is ". + "generally a good option, though.", + getParam('db_charset','utf8'), $table); + if ($mysql) { + $str = $_POST['db_charset']; + if ($charset) { + $t->inputP("Set charset to <q>$str</q>."); + } else { + $t->inputP("Could not set charset to ". + "<q>$str</q>: ".$charset_error, true); + } + } + + $t->inputText('db_name', 'Database name', '', + getParam('db_name', 'messagemanager'), $table); + echo $db_message; + + $t->inputText('db_prefix', 'Table prefix', + 'Just use simple characters, like [A-Za-z0-9_], '. + 'and keep it short.', + getParam('db_prefix','mm_'), $table); + + if ($db) { + $db_name = '<q>'.$db_prefix.'auth</q>'; + if ($table) { + if ($table_exists) { + $msg="Table $db_name already exists."; + } else { + $msg="Created table $db_name."; + } + } else { + $msg="Could not create table $db_name: ".$table_error; + } + $t->inputP($msg, !$table); + } + + $t->closeFieldset(); + + //////////////////////////////////////////////////////////////////////// + + $fh = false; + if ($table) { + $fh = fopen('conf.php', 'w'); + if ($fh === FALSE) { + $msg="Could not open file <q>conf.php</q> for writing."; + $template->paragraph($msg, true); + } else { + fwrite($fh, '<?php global $db_config;'."\n"); + fwrite($fh, configStr('host')); + fwrite($fh, configStr('user')); + fwrite($fh, configStr('password')); + fwrite($fh, "\n"); + fwrite($fh, configStr('charset')); + fwrite($fh, configStr('name')); + fwrite($fh, configStr('prefix')); + fclose($fh); + } + } + if ($fh) { + $t->closeTag('form'); + $t->openTag('form', array('action'=>$uri)); + $t->tag('input', array('type'=>'submit', + 'value'=>'Cool beans, go to step 2!')); + } else { + $t->tag('input', array('type'=>'submit', 'value'=>'Submit')); + } + $t->closeTag('form'); + $t->footer(); + //////////////////////////////////////////////////////////////////////// +} else { + require_once('MessageManager.class.php'); + $m = new MessageManager($conf_file); + $t = $m->template(); + + $t->header('Message Manager: Installer'); + + $user_count = $m->countUsers(); + + if ($user_count<1) { + $t->openTag('form', array('method'=>'post', 'action'=>$uri)); + $t->tag('input', array('type'=>'hidden', + 'id'=>'try', + 'name'=>'try', + 'value'=>'t')); + $try = isset($_POST['try']); + + $pw = false; + if ($try) { + $pw = ( $_POST['mm_password'] === + $_POST['mm_password_verify'] ); + } + + $admin = false; + if ($pw) { + $user = $_POST['mm_user']; + $password = $_POST['mm_password']; + + $uid = $m->addUser($user, $password); + $admin = $m->setStatus($uid, 2); + if (!$admin) { + $admin_error = mysql_error($mysql); + } + } + + //////////////////////////////////////////////////////////////// + + $t->openFieldset("First Account (administrator)",$admin); + $t->inputText('mm_user', 'Username', + "Must be <= 255 characters.", + getParam('mm_user','root'), $admin); + $t->inputNewPassword('mm_password', 'Password', + ($pw?getParam('mm_password'):''), + $admin); + if ($try && !$pw) { + $msg="Passwords don't match."; + $template->inputP($msg, true); + } + if ($pw) { + $user = "<q>".$_POST['mm_user'].'</q>'; + if ($admin) { + $msg="Created user $user."; + } else { + $msg="Could not create user $user: ". + $admin_error; + } + $t->inputP($msg, !$admin); + } + $t->closeFieldset(); + + //////////////////////////////////////////////////////////////// + + if (!$admin) { + $t->tag('input', array('type'=>'submit', + 'value'=>'Submit')); + } + $t->closeTag('form'); + } else { + $t->paragraph("File conf.php already exists, and there ". + "is at least one user. Return to the ". + "<a href='index.php'>main page</a>."); + } + $t->footer(); +} diff --git a/screen.css b/screen.css new file mode 100644 index 0000000..3705244 --- /dev/null +++ b/screen.css @@ -0,0 +1,50 @@ +/* If the indentation looks funny, it's using SASS indentation style. + * See <http://sass-lang.com>. SASS is pretty cool, but I'm too lame actually + * use SASS, because that's one more thing I have to install and run. I already + * have enough trouble saving before I switch to my browser to check it, let + * alone having to run another utility. + * + * To handle the introduction of curly braces (SASS doesn't have any), I've + * handled them as one handles parens in LISP. + * + * On a related note, I actually preffer SCSS to SASS, but I find SASS + * indentation to be more readable when using plain CSS. + */ +body { + background-color: #FFFF00; } + div.infobar, div.main { + margin: 0 auto; + width: 70%; + min-width: 30em; } + div.infobar { + margin-top: .5em; + + background-color: #0000FF; + border: solid #000099; + border-width: 1px 1px 0 1px; + border-top-left-radius: 0.7em; + border-top-right-radius: 0.7em; + box-shadow: 0 1px 0 #AAAAFF inset; } + div.infobar input[type="submit"]:hover, + div.infobar input[type="submit"]:active, + div.infobar input[type="submit"]:focus { + text-decoration: underline; } + div.main { + margin-bottom: 5em; + + background-color: #FFFFFF; + border: solid #AAAA00; + border-width: 0 1px 1px 1px; + padding-top: 1px;/* we don't want this, but it can't be 0 */ } + div.main_sub { + /* 'main_sub' is basically just to add padding to 'main' w/o it + * extending outside 'all'. */ + margin: 2em; } + +a { + color: #555555; } +a:hover, +a:active, +a:focus { + text-decoration: underline; +} diff --git a/src/controllers/Auth.class.php b/src/controllers/Auth.class.php new file mode 100644 index 0000000..86bd83f --- /dev/null +++ b/src/controllers/Auth.class.php @@ -0,0 +1,57 @@ +<?php + +Router::register('auth', 'Auth'); + +class Auth extends Controller { + public function index($routed, $remainder) { + // So if $_POST['action'] isn't set, it will trip on '', which + // is great, so we don't have to handle GET and PUT separately. + @$action = $_POST['action']; + switch ($action) { + case 'login' : $this->login(); break; + case 'logout': $this->logout(); break; + case '' : $this->maybe_login(); break; + default : $this->badrequest(); break; + } + } + private function login() { + $username = ''; + $password = ''; + + $login = -1; + if ( isset($_POST['username']) && isset($_POST['password'])) { + $username = $_POST['username']; + $password = $_POST['password']; + $login = $mm->login($username, $password); + } + + $vars = array(); + $vars['login_code'] = $login; + $vars['username'] = $username; + $vars['password'] = $password; + if (isset($_POST['url'])) { + $vars['url'] = $_POST['url']; + } + + $this->showView('auth/login', $vars); + } + private function logout() { + global $mm; + $mm->logout(); + $this->showView('auth/logout'); + } + private function maybe_login() { + global $mm; + $uid = $mm->isLoggedIn(); + if ($uid===false) { + $this->login(); + } else { + $username = $mm->getUsername($uid); + $this->showView('auth/index', + array('username'=>$username)); + } + } + private function badrequest() { + $this->showView('auth/badrequest'); + } +} diff --git a/src/controllers/Http404.class.php b/src/controllers/Http404.class.php new file mode 100644 index 0000000..322feaa --- /dev/null +++ b/src/controllers/Http404.class.php @@ -0,0 +1,7 @@ +<?php + +class Http404 extends Controller { + public function index($routed, $remainder) { + $this->http404($routed, $remainder); + } +} diff --git a/src/controllers/Users.class.php b/src/controllers/Users.class.php new file mode 100644 index 0000000..617c57a --- /dev/null +++ b/src/controllers/Users.class.php @@ -0,0 +1,291 @@ +<?php + +Router::register('users/new' , 'Users', 'new_user'); +Router::register('users/index', 'Users', 'index_file'); +Router::register('users' , 'Users', 'index_dir'); +Router::register('users/*' , 'Users', 'individual'); + +class Users extends Controller { + public static $illegal_names = array('', 'new', 'index'); + + /** + * Handle GETing the new user form. + * + * I would have named this `new', but that's a keyword. + */ + public function new_user($routed, $vars) { + // since there will never be a remainder to `users/new', we can + // use that parameter to pass in some data. + $this->showView('users/new', $vars); + } + + public function index($routed, $remainder) { + return $this->index_dir($routed, $remainder); + } + + /** + * Handle POSTing a new user, or GETing the index. + */ + public function index_dir($routed, $remainder) { + $method = $_SERVER['REQUEST_METHOD']; + switch ($method) { + case 'POST': + // We're POSTing a new user. + $this->create_user(); + break; + case 'HEAD': // fall-through to GET + case 'GET': + // We're GETing the index. + $this->show_index($routed, $remainder); + break; + } + } + + /** + * Handle PUTing an updated user index, or GETing the index. + */ + public function index_file($routed, $remainder) { + $method = $_SERVER['REQUEST_METHOD']; + switch ($method) { + case 'PUT': $_POST = $_PUT; + case 'POST': + // We're PUTing an updated user index. + $this->update_users(); + break; + } + $this->show_index($routed, $remainder); + } + + + public function individual($routed, $remainder) { + $username = implode('/', $remainder); + + global $mm; + $uid = $mm->getUID($username); + if ($mm->getStatus($uid)===3) $uid = false; // ignore groups. + + if ($uid===false) { + $this->http404($routed, $remainder); + } else { + $user = $mm->getAuthObj($uid); + if (!$user->canRead()) { + $this->http401($routed, $remainder); + exit(); + } + + $vars = array(); + $method = $_SERVER['REQUEST_METHOD']; + switch ($method) { + case 'PUT': $_POST = $_PUT; + case 'POST': + // We're PUTing updated user info. + if ($user->canEdit()) { + $vars = $this->update_user($user); + } + break; + } + $vars['user'] = $user; + $vars['groups'] = $mm->listGroupNames(); + $this->showView('users/individual', $vars); + } + } + + public function http404($routed, $remainder) { + $username = implode('/', $remainder); + $this->showView('users/404', + array('username'=>$username)); + } + + public function http401($routed, $remainder) { + global $mm; + $this->showView('users/401', array('uid'=>$mm->isLoggedIn())); + } + + /** + * This will parse POST data to create a new user. + * If successfull it will show a message saying so. + * If not successfull, it will re-show the new-user form with errors + * explained. + */ + private function create_user() { + $vars = array(); + @$vars['username' ] = $_POST['auth_name']; + @$vars['password1'] = $_POST['auth_password' ]; + @$vars['password2'] = $_POST['auth_password_verify']; + + global $mm; + $vars['errors'] = array(); + if ($mm->getUID($vars['username'])!==false) + $vars['errors'][] = 'user exists'; + if (in_array($vars['username'], $this->illegal_names)) + $vars['errors'] = 'illegal name'; + $matches = ($vars['password1'] == $vars['password2']); + if (!$matches) + $vars['errors'] = 'pw mixmatch'; + if ($matches && $password2 == '') + $vars['errors'] = 'no pw'; + + if (count($vars['errors']) > 0) { + $this->new_user($routed, $vars); + } else { + $username = $vars['username']; + $passowrd = $vars['password1']; + $uid = $mm->addUser($username, $password); + if ($uid===false) { + $this->showView('users/500'); + } else { + $mm->login($username, $password); + $this->showView('users/created', + array('username'=>$username)); + } + } + } + + /** + * This will parse POST (really, PUT) data to update a single user + */ + private function update_user($user) { + $vars = array(); + + $username = $user->getName(); + // Change the username ///////////////////////////////////////// + if (isset($_POST['auth_name'])) { + $new_name = $_POST['auth_name']; + if ($new_name != $username) { + if (!in_array($new_name, $this->illegal_names)) { + $changed_name = $user->setName($new_name); + $username = $user->getName(); + $vars['changed name'] = $changed_name; + } + } + } + + // Change the password ///////////////////////////////////////// + @$password1 = $_POST['auth_password' ]; + @$password2 = $_POST['auth_password'.'_verify']; + + // Check the verify box, not main box, so that we don't get + // tripped by browsers annoyingly autocompleting the password. + $is_set = ($password2 != ''); + + if ($is_set) { + $matches = ( $password1 == $password2 ); + if ($matches) { + $user->setPassword($password1); + $vars['pw updated'] = true; + } else { + $vars['pw mixmatch'] = true; + } + } + + // Change information ////////////////////////////////////////// + $this->confText($user, 'firstname'); + $this->confText($user, 'lastname'); + $this->confText($user, 'hsclass'); + + // Change contact info ///////////////////////////////////////// + global $CONTACT_METHODS; + foreach ($CONTACT_METHODS as $method) { + $this->confText($user, $method->addr_slug); + } + $this->confArray($user, 'use'); + + // Change groups /////////////////////////////////////////////// + $this->confArray($user, 'groups'); + + return $vars; + } + + private function confArray($user, $key) { + if (isset($_POST[$key]) && is_array($_POST[$key])) { + $user->setConfArray($key, $_POST[$key]); + } + } + + private function confText($user, $name) { + if (isset($_POST["user_$name"])) { + $user->setConf($name, $_POST["user_$name"]); + } + } + + + /** + * This will parse POST (really, PUT) data to update multiple users. + */ + private function update_users() { + // TODO + } + + /** + * This will show the user index. + */ + private function show_index($routed, $remainder) { + global $mm; + + $logged_in_user = $mm->getAuthObj($mm->isLoggedIn()); + if (!$logged_in_user->isUser()) { + $this->http401($routed, $remainder); + exit(); + } + + $vars = array(); + $vars['attribs'] = $this->getIndexAttribs(); + $vars['users'] = array(); + $uids = $mm->listUsers(); + foreach ($uids as $uid) { + $user = $mm->getAuthObj($uid); + $vars['users'][$uid] = array(); + foreach ($vars['attribs'] as $attrib) { + $key = $attrib['key']; + $props = $this->getConf($user, $key); + $vars['users'][$uid][$key] = $props; + } + } + $this->showView('users/index', $vars); + } + + private function getConf($user, $key) { + global $mm; + $logged_in_user = $mm->getAuthObj($mm->isLoggedIn()); + $uid = $user->getUID(); + $post_key = $key."[$uid]"; + @$value = $_POST[$post_key]; + $editable = $user->canEdit(); + + switch ($key) { + case 'auth_name': + $value = $user->getName(); + break; + case 'auth_user': + $editable = $editable && $logged_in_user->isAdmin(); + $value = $user->isUser(); + break; + case 'auth_admin': + $editable = $editable && $logged_in_user->isAdmin(); + $value = $user->isAdmin(); + break; + default: + $value = $user->getConf($key); + break; + } + + return array('value'=>$value, + 'post_key'=>$post_key, + 'editable'=>$editable); + } + + function attrib($key, $name) { + return array('key'=>$key, 'name'=>$name); + } + private function getIndexAttribs() { + $attribs = array($this->attrib('auth_user', 'Active'), + $this->attrib('lastname','Last'), + $this->attrib('firstname','First'), + $this->attrib('hsclass','Class of'), + $this->attrib('phone','Phone number'), + $this->attrib('email','Email'), + $this->attrib('auth_name', 'Username'), + ); + return $attrib; + } +} diff --git a/src/ext/GoogleVoice.class.php b/src/ext/GoogleVoice.class.php new file mode 100644 index 0000000..9638416 --- /dev/null +++ b/src/ext/GoogleVoice.class.php @@ -0,0 +1,84 @@ +<?PHP
+/*
+Version 0.2
+License This code is released under the MIT Open Source License. Feel free to do whatever you want with it.
+Author lostleon@gmail.com, http://www.lostleon.com/
+LastUpdate 05/28/2010
+*/
+class GoogleVoice
+{
+ public $username;
+ public $password;
+ public $status;
+ private $lastURL;
+ private $login_auth;
+ private $inboxURL = 'https://www.google.com/voice/m/';
+ private $loginURL = 'https://www.google.com/accounts/ClientLogin';
+ private $smsURL = 'https://www.google.com/voice/m/sendsms';
+
+ public function __construct($username, $password)
+ {
+ $this->username = $username;
+ $this->password = $password;
+ }
+
+ public function getLoginAuth()
+ {
+ $login_param = "accountType=GOOGLE&Email={$this->username}&Passwd={$this->password}&service=grandcentral&source=com.lostleon.GoogleVoiceTool";
+ $ch = curl_init($this->loginURL);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_2_1 like Mac OS X; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11 Safari/525.20");
+ curl_setopt($ch, CURLOPT_REFERER, $this->lastURL);
+ curl_setopt($ch, CURLOPT_POST, "application/x-www-form-urlencoded");
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $login_param);
+ $html = curl_exec($ch);
+ $this->lastURL = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
+ curl_close($ch);
+ $this->login_auth = $this->match('/Auth=([A-z0-9_-]+)/', $html, 1);
+ return $this->login_auth;
+ }
+
+ public function get_rnr_se()
+ {
+ $this->getLoginAuth();
+ $ch = curl_init($this->inboxURL);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ $headers = array("Authorization: GoogleLogin auth=".$this->login_auth, 'User-Agent: Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_2_1 like Mac OS X; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11 Safari/525.20');
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+ $html = curl_exec($ch);
+ $this->lastURL = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
+ curl_close($ch);
+ $_rnr_se = $this->match('!<input.*?name="_rnr_se".*?value="(.*?)"!ms', $html, 1);
+ return $_rnr_se;
+ }
+
+ public function sms($to_phonenumber, $smstxt)
+ {
+ $_rnr_se = $this->get_rnr_se();
+ $sms_param = "id=&c=&number=".urlencode($to_phonenumber)."&smstext=".urlencode($smstxt)."&_rnr_se=".urlencode($_rnr_se);
+ $ch = curl_init($this->smsURL);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ $headers = array("Authorization: GoogleLogin auth=".$this->login_auth, 'User-Agent: Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_2_1 like Mac OS X; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11 Safari/525.20');
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt($ch, CURLOPT_REFERER, $this->lastURL);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $sms_param);
+ $this->status = curl_exec($ch);
+ $this->lastURL = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
+ curl_close($ch);
+ return $this->status;
+ }
+
+ private function match($regex, $str, $out_ary = 0)
+ {
+ return preg_match($regex, $str, $match) == 1 ? $match[$out_ary] : false;
+ }
+}
+?>
diff --git a/src/ext/HTTP_Accept.class.php b/src/ext/HTTP_Accept.class.php new file mode 100644 index 0000000..5efaa5d --- /dev/null +++ b/src/ext/HTTP_Accept.class.php @@ -0,0 +1,659 @@ +<?php + +/** + * HTTP_Accept class for dealing with the HTTP 'Accept' header + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to the MIT License. + * The full text of this license is available at the following URL: + * http://www.opensource.org/licenses/mit-license.php + * + * @category HTTP + * @package HTTP_Accept + * @author Kevin Locke <kwl7@cornell.edu> + * @copyright 2007 Kevin Locke + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version SVN: $Id: HTTP_Accept.php 22 2007-10-06 18:46:45Z kevin $ + * @link http://pear.php.net/package/HTTP_Accept + */ + +/** + * HTTP_Accept class for dealing with the HTTP 'Accept' header + * + * This class is intended to be used to parse the HTTP Accept header into + * usable information and provide a simple API for dealing with that + * information. + * + * The parsing of this class is designed to follow RFC 2616 to the letter, + * any deviations from that standard are bugs and should be reported to the + * maintainer. + * + * Often the class will be used very simply as + * <code> + * <?php + * $accept = new HTTP_Accept($_SERVER['HTTP_ACCEPT']); + * if ($accept->getQuality("image/png") > $accept->getQuality("image/jpeg")) + * // Send PNG image + * else + * // Send JPEG image + * ?> + * </code> + * + * However, for browsers which do not accurately describe their preferences, + * it may be necessary to check if a MIME Type is explicitly listed in their + * Accept header, in addition to being preferred to another type + * + * <code> + * <?php + * if ($accept->isMatchExact("application/xhtml+xml")) + * // Client specifically asked for this type at some quality level + * ?> + * </code> + * + * + * @category HTTP + * @package HTTP_Accept + * @access public + * @link http://pear.php.net/package/HTTP_Accept + */ +class HTTP_Accept +{ + /** + * Array of types and their associated parameters, extensions, and quality + * factors, as represented in the Accept: header. + * Indexed by [type][subtype][index], + * and contains 'PARAMS', 'QUALITY', and 'EXTENSIONS' keys for the + * parameter set, quality factor, and extensions set respectively. + * Note: Since type, subtype, and parameters are case-insensitive + * (RFC 2045 5.1) they are stored as lower-case. + * + * @var array + * @access private + */ + var $acceptedtypes = array(); + + /** + * Regular expression to match a token, as defined in RFC 2045 + * + * @var string + * @access private + */ + var $_matchtoken = '(?:[^[:cntrl:]()<>@,;:\\\\"\/\[\]?={} \t]+)'; + + /** + * Regular expression to match a quoted string, as defined in RFC 2045 + * + * @var string + * @access private + */ + var $_matchqstring = '(?:"[^\\\\"]*(?:\\\\.[^\\\\"]*)*")'; + + /** + * Constructs a new HTTP_Accept object + * + * Initializes the HTTP_Accept class with a given accept string + * or creates a new (empty) HTTP_Accept object if no string is given + * + * Note: The behavior is a little strange here to accomodate + * missing headers (to be interpreted as accept all) as well as + * new empty objects which should accept nothing. This means that + * HTTP_Accept("") will be different than HTTP_Accept() + * + * @access public + * @return object HTTP_Accept + * @param string $acceptstring The value of an Accept: header + * Will often be $_SERVER['HTTP_ACCEPT'] + * Note: If get_magic_quotes_gpc is on, + * run stripslashes() on the string first + */ + function HTTP_Accept() + { + if (func_num_args() == 0) { + // User wishes to create empty HTTP_Accept object + $this->acceptedtypes = array( + '*' => array( + '*' => array ( + 0 => array( + 'PARAMS' => array(), + 'QUALITY' => 0, + 'EXTENSIONS' => array() + ) + ) + ) + ); + return; + } + + $acceptstring = trim(func_get_arg(0)); + if (empty($acceptstring)) { + // Accept header empty or not sent, interpret as "*/*" + $this->acceptedtypes = array( + '*' => array( + '*' => array ( + 0 => array( + 'PARAMS' => array(), + 'QUALITY' => 1, + 'EXTENSIONS' => array() + ) + ) + ) + ); + return; + } + + $matches = preg_match_all( + '/\s*('.$this->_matchtoken.')\/' . // typegroup/ + '('.$this->_matchtoken.')' . // subtype + '((?:\s*;\s*'.$this->_matchtoken.'\s*' . // parameter + '(?:=\s*' . // optional =value + '(?:'.$this->_matchqstring.'|'.$this->_matchtoken.'))?)*)/', // value + $acceptstring, $acceptedtypes, + PREG_SET_ORDER); + + if ($matches == 0) { + // Malformed Accept header + $this->acceptedtypes = array( + '*' => array( + '*' => array ( + 0 => array( + 'PARAMS' => array(), + 'QUALITY' => 1, + 'EXTENSIONS' => array() + ) + ) + ) + ); + return; + } + + foreach ($acceptedtypes as $accepted) { + $typefamily = strtolower($accepted[1]); + $subtype = strtolower($accepted[2]); + + // */subtype is invalid according to grammar in section 14.1 + // so we ignore it + if ($typefamily == '*' && $subtype != '*') + continue; + + // Parse all arguments of the form "key=value" + $matches = preg_match_all('/;\s*('.$this->_matchtoken.')\s*' . + '(?:=\s*' . + '('.$this->_matchqstring.'|'. + $this->_matchtoken.'))?/', + $accepted[3], $args, + PREG_SET_ORDER); + + $params = array(); + $quality = -1; + $extensions = array(); + foreach ($args as $arg) { + array_shift($arg); + if (!empty($arg[1])) { + // Strip quotes (Note: Can't use trim() in case "text\"") + $len = strlen($arg[1]); + if ($arg[1][0] == '"' && $arg[1][$len-1] == '"' + && $len > 1) { + $arg[1] = substr($arg[1], 1, $len-2); + $arg[1] = stripslashes($arg[1]); + } + } else if (!isset($arg[1])) { + $arg[1] = null; + } + + // Everything before q=# is a parameter, after is an extension + if ($quality >= 0) { + $extensions[$arg[0]] = $arg[1]; + } else if ($arg[0] == 'q') { + $quality = (float)$arg[1]; + + if ($quality < 0) + $quality = 0; + else if ($quality > 1) + $quality = 1; + } else { + $arg[0] = strtolower($arg[0]); + // Values required for parameters, + // assume empty-string for missing values + if (isset($arg[1])) + $params[$arg[0]] = $arg[1]; + else + $params[$arg[0]] = ""; + } + } + + if ($quality < 0) + $quality = 1; + else if ($quality == 0) + continue; + + if (!isset($this->acceptedtypes[$typefamily])) + $this->acceptedtypes[$typefamily] = array(); + if (!isset($this->acceptedtypes[$typefamily][$subtype])) + $this->acceptedtypes[$typefamily][$subtype] = array(); + + $this->acceptedtypes[$typefamily][$subtype][] = + array('PARAMS' => $params, + 'QUALITY' => $quality, + 'EXTENSIONS' => $extensions); + } + + if (!isset($this->acceptedtypes['*'])) + $this->acceptedtypes['*'] = array(); + if (!isset($this->acceptedtypes['*']['*'])) + $this->acceptedtypes['*']['*'] = array( + 0 => array( + 'PARAMS' => array(), + 'QUALITY' => 0, + 'EXTENSIONS' => array() + ) + ); + } + + /** + * Gets the accepted quality factor for a given MIME Type + * + * Note: If there are multiple best matches + * (e.g. "text/html;level=4;charset=utf-8" matching both "text/html;level=4" + * and "text/html;charset=utf-8"), it returns the lowest quality factor as + * a conservative estimate. Further, if the ambiguity is between parameters + * and extensions (e.g. "text/html;level=4;q=1;ext=foo" matching both + * "text/html;level=4" and "text/html;q=1;ext=foo") the parameters take + * precidence. + * + * Usage Note: If the quality factor for all supported media types is 0, + * RFC 2616 specifies that applications SHOULD send an HTTP 406 (not + * acceptable) response. + * + * @access public + * @return double the quality value for the given MIME Type + * Quality values are in the range [0,1] where 0 means + * "not accepted" and 1 is "perfect quality". + * @param string $mimetype The MIME Type to query ("text/html") + * @param array $params Parameters of Type to query ([level => 4]) + * @param array $extensions Extension parameters to query + */ + function getQuality($mimetype, $params = array(), $extensions = array()) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if ($params == null) + $params = array(); + if ($extensions == null) + $extensions = array(); + + if (empty($this->acceptedtypes[$supertype])) { + if ($supertype == '*') + return 0; + else + return $this->getQuality("*/*", $params, $extensions); + } + + if (empty($this->acceptedtypes[$supertype][$subtype])) { + if ($subtype == '*') + return $this->getQuality("*/*", $params, $extensions); + else + return $this->getQuality("$supertype/*", $params, $extensions); + } + + $params = array_change_key_case($params, CASE_LOWER); + + $matches = $this->_findBestMatchIndices($supertype, $subtype, + $params, $extensions); + + if (count($matches) == 0) { + if ($subtype != '*') + return $this->getQuality("$supertype/*", $params, $extensions); + else if ($supertype != '*') + return $this->getQuality("*/*", $params, $extensions); + else + return 0; + } + + $minquality = 1; + foreach ($matches as $match) + if ($this->acceptedtypes[$supertype][$subtype][$match]['QUALITY'] < $minquality) + $minquality = $this->acceptedtypes[$supertype][$subtype][$match]['QUALITY']; + + return $minquality; + } + + /** + * Determines if there is an exact match for the specified MIME Type + * + * @access public + * @return boolean true if there is an exact match to the given + * values, false otherwise. + * @param string $mimetype The MIME Type to query (e.g. "text/html") + * @param array $params Parameters of Type to query (e.g. [level => 4]) + * @param array $extensions Extension parameters to query + */ + function isMatchExact($mimetype, $params = array(), $extensions = array()) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if ($params == null) + $params = array(); + if ($extensions == null) + $extensions = array(); + + return $this->_findExactMatchIndex($supertype, $subtype, + $params, $extensions) >= 0; + } + + /** + * Gets a list of all MIME Types explicitly accepted, sorted by quality + * + * @access public + * @return array list of MIME Types explicitly accepted, sorted + * in decreasing order of quality factor + */ + function getTypes() + { + $qvalues = array(); + $types = array(); + foreach ($this->acceptedtypes as $typefamily => $subtypes) { + if ($typefamily == '*') + continue; + + foreach ($subtypes as $subtype => $variants) { + if ($subtype == '*') + continue; + + $maxquality = 0; + foreach ($variants as $variant) + if ($variant['QUALITY'] > $maxquality) + $maxquality = $variant['QUALITY']; + + if ($maxquality > 0) { + $qvalues[] = $maxquality; + $types[] = $typefamily.'/'.$subtype; + } + } + } + + array_multisort($qvalues, SORT_DESC, SORT_NUMERIC, + $types, SORT_DESC, SORT_STRING); + + return $types; + } + + /** + * Gets the parameter sets for a given mime type, sorted by quality. + * Only parameter sets where the extensions set is empty will be returned. + * + * @access public + * @return array list of sets of name=>value parameter pairs + * in decreasing order of quality factor + * @param string $mimetype The MIME Type to query ("text/html") + */ + function getParameterSets($mimetype) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if (!isset($this->acceptedtypes[$supertype]) + || !isset($this->acceptedtypes[$supertype][$subtype])) + return array(); + + $qvalues = array(); + $paramsets = array(); + foreach ($this->acceptedtypes[$supertype][$subtype] as $acceptedtype) { + if (count($acceptedtype['EXTENSIONS']) == 0) { + $qvalues[] = $acceptedtype['QUALITY']; + $paramsets[] = $acceptedtype['PARAMS']; + } + } + + array_multisort($qvalues, SORT_DESC, SORT_NUMERIC, + $paramsets, SORT_DESC, SORT_STRING); + + return $paramsets; + } + + /** + * Gets the extension sets for a given mime type, sorted by quality. + * Only extension sets where the parameter set is empty will be returned. + * + * @access public + * @return array list of sets of name=>value extension pairs + * in decreasing order of quality factor + * @param string $mimetype The MIME Type to query ("text/html") + */ + function getExtensionSets($mimetype) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if (!isset($this->acceptedtypes[$supertype]) + || !isset($this->acceptedtypes[$supertype][$subtype])) + return array(); + + $qvalues = array(); + $extensionsets = array(); + foreach ($this->acceptedtypes[$supertype][$subtype] as $acceptedtype) { + if (count($acceptedtype['PARAMS']) == 0) { + $qvalues[] = $acceptedtype['QUALITY']; + $extensionsets[] = $acceptedtype['EXTENSIONS']; + } + } + + array_multisort($qvalues, SORT_DESC, SORT_NUMERIC, + $extensionsets, SORT_DESC, SORT_STRING); + + return $extensionsets; + } + + /** + * Adds a type to the set of accepted types + * + * @access public + * @param string $mimetype The MIME Type to add (e.g. "text/html") + * @param double $quality The quality value for the given MIME Type + * Quality values are in the range [0,1] where + * 0 means "not accepted" and 1 is + * "perfect quality". + * @param array $params Parameters of the type to add (e.g. [level => 4]) + * @param array $extensions Extension parameters of the type to add + */ + function addType($mimetype, $quality = 1, + $params = array(), $extensions = array()) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if ($params == null) + $params = array(); + if ($extensions == null) + $extensions = array(); + + $index = $this->_findExactMatchIndex($supertype, $subtype, $params, $extensions); + + if ($index >= 0) { + $this->acceptedtypes[$supertype][$subtype][$index]['QUALITY'] = $quality; + } else { + if (!isset($this->acceptedtypes[$supertype])) + $this->acceptedtypes[$supertype] = array(); + if (!isset($this->acceptedtypes[$supertype][$subtype])) + $this->acceptedtypes[$supertype][$subtype] = array(); + + $this->acceptedtypes[$supertype][$subtype][] = + array('PARAMS' => $params, + 'QUALITY' => $quality, + 'EXTENSIONS' => $extensions); + } + } + + /** + * Removes a type from the set of accepted types + * + * @access public + * @param string $mimetype The MIME Type to remove (e.g. "text/html") + * @param array $params Parameters of the type to remove (e.g. [level => 4]) + * @param array $extensions Extension parameters of the type to remove + */ + function removeType($mimetype, $params = array(), $extensions = array()) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if ($params == null) + $params = array(); + if ($extensions == null) + $extensions = array(); + + $index = $this->_findExactMatchIndex($supertype, $subtype, $params, $extensions); + + if ($index >= 0) { + $this->acceptedtypes[$supertype][$subtype] = + array_merge(array_slice($this->acceptedtypes[$supertype][$subtype], + 0, $index), + array_slice($this->acceptedtypes[$supertype][$subtype], + $index+1)); + } + } + + /** + * Gets a string representation suitable for use in an HTTP Accept header + * + * @access public + * @return string a string representation of this object + */ + function __toString() + { + $accepted = array(); + $qvalues = array(); + foreach ($this->acceptedtypes as $supertype => $subtypes) { + foreach ($subtypes as $subtype => $entries) { + foreach ($entries as $entry) { + $accepted[] = array('TYPE' => "$supertype/$subtype", + 'QUALITY' => $entry['QUALITY'], + 'PARAMS' => $entry['PARAMS'], + 'EXTENSIONS' => $entry['EXTENSIONS']); + $qvalues[] = $entry['QUALITY']; + } + } + } + + array_multisort($qvalues, SORT_DESC, SORT_NUMERIC, + $accepted); + + $str = ""; + foreach ($accepted as $accept) { + // Skip the catchall value if it is 0, since this is implied + if ($accept['TYPE'] == '*/*' && + $accept['QUALITY'] == 0 && + count($accept['PARAMS']) == 0 && + count($accept['EXTENSIONS'] == 0)) + continue; + + $str = $str.$accept['TYPE'].';'; + + foreach ($accept['PARAMS'] as $param => $value) { + if (preg_match('/^'.$this->_matchtoken.'$/', $value)) + $str = $str.$param.'='.$value.';'; + else + $str = $str.$param.'="'.addcslashes($value,'"\\').'";'; + } + + if ($accept['QUALITY'] < 1 || !empty($accept['EXTENSIONS'])) + $str = $str.'q='.$accept['QUALITY'].';'; + + foreach ($accept['EXTENSIONS'] as $extension => $value) { + if (preg_match('/^'.$this->_matchtoken.'$/', $value)) + $str = $str.$extension.'='.$value.';'; + else + $str = $str.$extension.'="'.addcslashes($value,'"\\').'";'; + } + + $str[strlen($str)-1] = ','; + } + + return rtrim($str, ','); + } + + /** + * Finds the index of an exact match for the specified MIME Type + * + * @access private + * @return int the index of an exact match if found, + * -1 otherwise + * @param string $supertype The general MIME Type to find (e.g. "text") + * @param string $subtype The MIME subtype to find (e.g. "html") + * @param array $params Parameters of Type to find ([level => 4]) + * @param array $extensions Extension parameters to find + */ + function _findExactMatchIndex($supertype, $subtype, $params, $extensions) + { + if (empty($this->acceptedtypes[$supertype]) + || empty($this->acceptedtypes[$supertype][$subtype])) + return -1; + + $params = array_change_key_case($params, CASE_LOWER); + + $parammatches = array(); + foreach ($this->acceptedtypes[$supertype][$subtype] as $index => $typematch) + if ($typematch['PARAMS'] == $params + && $typematch['EXTENSIONS'] == $extensions) + return $index; + + return -1; + } + + /** + * Finds the indices of the best matches for the specified MIME Type + * + * A "match" in this context is an exact type match and no extraneous + * matches for parameters or extensions (so the best match for + * "text/html;level=4" may be "text/html" but not the other way around). + * + * "Best" is interpreted as the entries that match the most + * parameters and extensions (the sum of the number of matches) + * + * @access private + * @return array an array of the indices of the best matches + * (empty if no matches) + * @param string $supertype The general MIME Type to find (e.g. "text") + * @param string $subtype The MIME subtype to find (e.g. "html") + * @param array $params Parameters of Type to find ([level => 4]) + * @param array $extensions Extension parameters to find + */ + function _findBestMatchIndices($supertype, $subtype, $params, $extensions) + { + $bestmatches = array(); + $bestlength = 0; + + if (empty($this->acceptedtypes[$supertype]) + || empty($this->acceptedtypes[$supertype][$subtype])) + return $bestmatches; + + foreach ($this->acceptedtypes[$supertype][$subtype] as $index => $typematch) { + if (count(array_diff_assoc($typematch['PARAMS'], $params)) == 0 + && count(array_diff_assoc($typematch['EXTENSIONS'], + $extensions)) == 0) { + $length = count($typematch['PARAMS']) + + count($typematch['EXTENSIONS']); + + if ($length > $bestlength) { + $bestmatches = array($index); + $bestlength = $length; + } else if ($length == $bestlength) { + $bestmatches[] = $index; + } + } + } + + return $bestmatches; + } +} + +// vim: set ts=4 sts=4 sw=4 et: +?> diff --git a/src/ext/Identica.class.php b/src/ext/Identica.class.php new file mode 100644 index 0000000..a3e62a9 --- /dev/null +++ b/src/ext/Identica.class.php @@ -0,0 +1,491 @@ +<?php +/** + * Copyright (c) <2009> Gianluca Urgese <g.urgese@jasone.it> + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + + +/** +* The main identica-php class. Create an object to use your Identi.ca account from php. +*/ +class Identica { + /** Username:password format string */ + private $credentials; + + /** Contains the last HTTP status code returned */ + private $http_status; + + /** Contains the last API call */ + private $last_api_call; + + /** Contains the application calling the API */ + private $application_source; + + /** + * Identi.ca class constructor. + * @param username is a alphanumeric string to perform login on Identi.ca. + * @param password is a alphanumeric string to perform login on Identi.ca. + * @param source is the name of your application. + * @return An Identica object to use to perform all the operation. + */ + function Identica($username, $password, $source=false) { + $this->credentials = sprintf("%s:%s", $username, $password); + $this->application_source = $source; + } + + /** + * Returns the 20 most recent statuses from non-protected users who have set a custom user icon. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param since_id returns only statuses with an ID greater than (that is, more recent than) the specified ID. + * @return the public timeline in the specified format. + */ + function getPublicTimeline($format, $since_id = 0) { + $api_call = sprintf("http://identi.ca/api/statuses/public_timeline.%s", $format); + if ($since_id > 0) { + $api_call .= sprintf("?since_id=%d", $since_id); + } + return $this->APICall($api_call); + } + + /** + * Returns the 20 most recent statuses posted by the authenticating user and that user's friends. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param id returns only statuses from specified ID. + * @param since returns only statuses with an ID greater than (that is, more recent than) the specified ID. + * @return the friends timeline in the specified format. + */ + function getFriendsTimeline($format, $id = NULL, $since = NULL) { + if ($id != NULL) { + $api_call = sprintf("http://identi.ca/api/statuses/friends_timeline/%s.%s", $id, $format); + } + else { + $api_call = sprintf("http://identi.ca/api/statuses/friends_timeline.%s", $format); + } + if ($since != NULL) { + $api_call .= sprintf("?since=%s", urlencode($since)); + } + return $this->APICall($api_call, true); + } + + /** + * Returns the 20 most recent statuses posted from the authenticating user. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param id get only statuses from specified ID. + * @param count specifies the number of statuses to retrieve. May not be greater than 200. + * @param since get only statuses with an ID greater than (that is, more recent than) the specified ID. + * @return the 20 most recent statuses posted from the authenticating user. + */ + function getUserTimeline($format, $id = NULL, $count = 20, $since = NULL) { + if ($id != NULL) { + $api_call = sprintf("http://identi.ca/api/statuses/user_timeline/%s.%s", $id, $format); + } + else { + $api_call = sprintf("http://identi.ca/api/statuses/user_timeline.%s", $format); + } + if ($count != 20) { + $api_call .= sprintf("?count=%d", $count); + } + if ($since != NULL) { + $api_call .= sprintf("%ssince=%s", (strpos($api_call, "?count=") === false) ? "?" : "&", urlencode($since)); + } + return $this->APICall($api_call, true); + } + + /** + * Returns a single status, specified by the id parameter below. The status's author will be returned inline. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param id get only statuses from specified ID. + * @return a single status, specified by the id parameter. + */ + function showStatus($format, $id) { + $api_call = sprintf("http://identi.ca/api/statuses/show/%d.%s", $id, $format); + return $this->APICall($api_call); + } + + /** + * Updates the authenticating user's status. Request must be a POST. Statuses over 140 characters will be forceably truncated. + * @param status is the text of your status update. + * @return the current update from authenticating user. + */ + function updateStatus($status) { + $status = urlencode(stripslashes(urldecode($status))); + $api_call = sprintf("http://identi.ca/api/statuses/update.xml?status=%s", $status); + return $this->APICall($api_call, true, true); + } + + /** + * Returns a list of replies from authenticating user. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param page specifies the page of results to retrieve. + * @return list of replies from authenticating user. + */ + function getReplies($format, $page = 0) { + $api_call = sprintf("http://identi.ca/api/statuses/replies.%s", $format); + if ($page) { + $api_call .= sprintf("?page=%d", $page); + } + return $this->APICall($api_call, true); + } + + /** + * Destroys the status specified by the required ID parameter. The authenticating user must be the author of the specified status. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param id the ID of the status to destroy. + * @return a destroyed status specified by the id parameter. + */ + function destroyStatus($format, $id) { + $api_call = sprintf("http://identi.ca/api/statuses/destroy/%d.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Returns a user's friends, each with current status inline. They are ordered by the order in which + * they were added as friends, 100 at a time. Use the page option to access older friends. With no + * user specified, request defaults to the authenticated user's friends. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param id is the ID of the user for whom to request a list of friends. + * @return a user's friends. + */ + function getFriends($format, $id = NULL) { + if ($id != NULL) { + $api_call = sprintf("http://identi.ca/api/statuses/friends/%s.%s", $id, $format); + } + else { + $api_call = sprintf("http://identi.ca/api/statuses/friends.%s", $format); + } + return $this->APICall($api_call, true); + } + + /** + * Returns a user's followers. They are ordered by the order in which they joined Identi.ca, 100 at a time. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param lite specified if status must be show. + * @return a user's followers. + */ + function getFollowers($format, $lite = NULL) { + $api_call = sprintf("http://identi.ca/api/statuses/followers.%s%s", $format, ($lite) ? "?lite=true" : NULL); + return $this->APICall($api_call, true); + } + + /** + * Returns extended information of a given user, specified by ID or email as per the required id parameter. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @param email is the email of specified user. + * @return extended information of a given user. + */ + function showUser($format, $id, $email = NULL) { + if ($email == NULL) { + $api_call = sprintf("http://identi.ca/api/users/show/%s.%s", $id, $format); + } + else { + $api_call = sprintf("http://identi.ca/api/users/show.xml?email=%s", $email); + } + return $this->APICall($api_call, true); + } + + /** + * Returns a list of the 20 most recent direct messages sent to the authenticating user. The XML + * and JSON versions include detailed information about the sending and recipient users. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param since get only messages from an ID greater than (that is, more recent than) the specified ID. + * @param since_id returns only statuses from the specified ID. + * @param page Specifies the page of direct messages to retrieve. + * @return a list of the 20 most recent direct messages. + */ + function getMessages($format, $since = NULL, $since_id = 0, $page = 1) { + $api_call = sprintf("http://identi.ca/api/direct_messages.%s", $format); + if ($since != NULL) { + $api_call .= sprintf("?since=%s", urlencode($since)); + } + if ($since_id > 0) { + $api_call .= sprintf("%ssince_id=%d", (strpos($api_call, "?since") === false) ? "?" : "&", $since_id); + } + if ($page > 1) { + $api_call .= sprintf("%spage=%d", (strpos($api_call, "?since") === false) ? "?" : "&", $page); + } + return $this->APICall($api_call, true); + } + + /** + * Returns a list of the 20 most recent direct messages sent by the authenticating user. The XML + * and JSON versions include detailed information about the sending and recipient users. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param since get only messages from an ID greater than (that is, more recent than) the specified ID. + * @param since_id returns only statuses from the specified ID. + * @param page specifies the page of direct messages to retrieve. + * @return a list of the 20 most recent sent direct messages. + */ + function getSentMessages($format, $since = NULL, $since_id = 0, $page = 1) { + $api_call = sprintf("http://identi.ca/api/direct_messages/sent.%s", $format); + if ($since != NULL) { + $api_call .= sprintf("?since=%s", urlencode($since)); + } + if ($since_id > 0) { + $api_call .= sprintf("%ssince_id=%d", (strpos($api_call, "?since") === false) ? "?" : "&", $since_id); + } + if ($page > 1) { + $api_call .= sprintf("%spage=%d", (strpos($api_call, "?since") === false) ? "?" : "&", $page); + } + return $this->APICall($api_call, true); + } + + /** + * Sends a new direct message to the specified user from the authenticating user. Request must be a POST. + * @param format is the extension for the result file (xml, json). + * @param user is the ID of specified user to send the message. + * @param text is the text of your direct message. + * @return the sent message in the requested format when successful. + */ + function newMessage($format, $user, $text) { + $text = urlencode(stripslashes(urldecode($text))); + $api_call = sprintf("http://identi.ca/api/direct_messages/new.%s?user=%s&text=%s", $format, $user, $text); + return $this->APICall($api_call, true, true); + } + + /** + * Destroys the direct message specified in the required ID parameter. The authenticating user + * must be the recipient of the specified direct message. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified direct message. + * @return the message destroyed. + */ + function destroyMessage($format, $id) { + $api_call = sprintf("http://identi.ca/api/direct_messages/destroy/%s.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Allows the authenticating users to follow the user specified in the ID parameter. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @return the befriended user in the requested format when successful. Returns a string describing the + * failure condition when unsuccessful. If you are already friends with the user an HTTP 403 will be returned. + */ + function createFriendship($format, $id) { + $api_call = sprintf("http://identi.ca/api/friendships/create/%s.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Allows the authenticating users to unfollow the user specified in the ID parameter. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @return the unfollowed user in the requested format when successful. Returns a string + * describing the failure condition when unsuccessful. + */ + function destroyFriendship($format, $id) { + $api_call = sprintf("http://identi.ca/api/friendships/destroy/%s.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Tests for the existence of friendship between two users. + * @param format is the extension for the result file (xml, json). + * @param user_a is the ID of the first specified user. + * @param user_b is the ID of the second specified user. + * @return true if user_a follows user_b, otherwise will return false. + */ + function friendshipExists($format, $user_a, $user_b) { + $api_call = sprintf("http://identi.ca/api/friendships/exists.%s?user_a=%s&user_b=%s", $format, $user_a, $user_b); + return $this->APICall($api_call, true); + } + + /** + * Tests if supplied user credentials are valid. + * @param format is the extension for the result file (xml, json). + * @return an HTTP 200 OK response code and a representation of the requesting user if authentication + * was successful; returns a 401 status code and an error message if not. + */ + function verifyCredentials($format = NULL) { + $api_call = sprintf("http://identi.ca/api/account/verify_credentials%s", ($format != NULL) ? sprintf(".%s", $format) : NULL); + return $this->APICall($api_call, true); + } + + /** + * Ends the session of the authenticating user, returning a null cookie. + * @return NULL + */ + function endSession() { + $api_call = "http://identi.ca/api/account/end_session"; + return $this->APICall($api_call, true); + } + + /** + * Update user's location in the profile. + * @param location is the user's location . + * @return NULL. + */ + function updateLocation($format, $location) { + $api_call = sprintf("http://identi.ca/api/account/update_location.%s?location=%s", $format, $location); + return $this->APICall($api_call, true, true); + } + + /** + * Sets which device Identi.ca delivers updates to for the authenticating user. + * @param format is the extension for the result file (xml, json). + * @param device must be one of: sms, im, none. + * @return user's profile details in a selected format. + */ + function updateDeliveryDevice($format, $device) { + $api_call = sprintf("http://identi.ca/api/account/update_delivery_device.%s?device=%s", $format, $device); + return $this->APICall($api_call, true, true); + } + + /** + * Returns the remaining number of API requests available to the requesting user before the API + * limit is reached for the current hour. Calls to rateLimitStatus() do not count against the rate limit. + * @param format is the extension for the result file (xml, json). + * @return remaining number of API requests. + */ + function rateLimitStatus($format) { + $api_call = sprintf("http://identi.ca/api/account/rate_limit_status.%s", $format); + return $this->APICall($api_call, true); + } + + /** + * Returns the 20 most recent favorite statuses for the authenticating user or user + * specified by the ID parameter in the requested format. + * @param format is the extension for the result file (xml, json, rss, atom). + * @param id is the ID of specified user. + * @param page specifies the page of favorites to retrieve. + * @return a list of the 20 most recent favorite statuses. + */ + function getFavorites($format, $id = NULL, $page = 1) { + if ($id == NULL) { + $api_call = sprintf("http://identi.ca/api/favorites.%s", $format); + } + else { + $api_call = sprintf("http://identi.ca/api/favorites/%s.%s", $id, $format); + } + if ($page > 1) { + $api_call .= sprintf("?page=%d", $page); + } + return $this->APICall($api_call, true); + } + + /** + * Favorites the status specified in the ID parameter as the authenticating user. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @return the favorite status when successful. + */ + function createFavorite($format, $id) { + $api_call = sprintf("http://identi.ca/api/favorites/create/%d.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Un-favorites the status specified in the ID parameter as the authenticating user. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @return the un-favorited status in the requested format when successful. + */ + function destroyFavorite($format, $id) { + $api_call = sprintf("http://identi.ca/api/favorites/destroy/%d.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Enables device notifications for updates from the specified user. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @return the specified user when successful. + */ + function follow($format, $id) { + $api_call = sprintf("http://identi.ca/api/notifications/follow/%d.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Disables notifications for updates from the specified user to the authenticating user. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @return the specified user when successful. + */ + function leave($format, $id) { + $api_call = sprintf("http://identi.ca/api/notifications/leave/%d.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @return the blocked user in the requested format when successful. + */ + function createBlock($format, $id) { + $api_call = sprintf("http://identi.ca/api/blocks/create/%d.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Un-blocks the user specified in the ID parameter for the authenticating user. + * @param format is the extension for the result file (xml, json). + * @param id is the ID of specified user. + * @return the un-blocked user in the requested format when successful. + */ + function destroyBlock($format, $id) { + $api_call = sprintf("http://identi.ca/api/blocks/destroy/%d.%s", $id, $format); + return $this->APICall($api_call, true, true); + } + + /** + * Returns true or false in the requested format with a 200 OK HTTP status code. + * @param format is the extension for the result file (xml, json). + * @return test results. + */ + function test($format) { + $api_call = sprintf("http://identi.ca/api/help/test.%s", $format); + return $this->APICall($api_call, true); + } + + private function APICall($api_url, $require_credentials = false, $http_post = false) { + $curl_handle = curl_init(); + if($this->application_source){ + $api_url .= "&source=" . $this->application_source; + } + curl_setopt($curl_handle, CURLOPT_URL, $api_url); + if ($require_credentials) { + curl_setopt($curl_handle, CURLOPT_USERPWD, $this->credentials); + } + if ($http_post) { + curl_setopt($curl_handle, CURLOPT_POST, true); + } + curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, TRUE); + $identica_data = curl_exec($curl_handle); + $this->http_status = curl_getinfo($curl_handle, CURLINFO_HTTP_CODE); + $this->last_api_call = $api_url; + curl_close($curl_handle); + return $identica_data; + } + + function lastStatusCode() { + return $this->http_status; + } + + function lastAPICall() { + return $this->last_api_call; + } +} +?> diff --git a/src/ext/MimeMailParser.class.php b/src/ext/MimeMailParser.class.php new file mode 100644 index 0000000..0080199 --- /dev/null +++ b/src/ext/MimeMailParser.class.php @@ -0,0 +1,447 @@ +<?php
+
+require_once('MimeMailParser_attachment.class.php');
+
+/**
+ * Fast Mime Mail parser Class using PHP's MailParse Extension
+ * @author gabe@fijiwebdesign.com
+ * @url http://www.fijiwebdesign.com/
+ * @license http://creativecommons.org/licenses/by-sa/3.0/us/
+ * @version $Id$
+ */
+class MimeMailParser {
+
+ /**
+ * PHP MimeParser Resource ID
+ */
+ public $resource;
+
+ /**
+ * A file pointer to email
+ */
+ public $stream;
+
+ /**
+ * A text of an email
+ */
+ public $data;
+
+ /**
+ * Stream Resources for Attachments
+ */
+ public $attachment_streams;
+
+ /**
+ * Inialize some stuff
+ * @return
+ */
+ public function __construct() {
+ $this->attachment_streams = array();
+ }
+
+ /**
+ * Free the held resouces
+ * @return void
+ */
+ public function __destruct() {
+ // clear the email file resource
+ if (is_resource($this->stream)) {
+ fclose($this->stream);
+ }
+ // clear the MailParse resource
+ if (is_resource($this->resource)) {
+ mailparse_msg_free($this->resource);
+ }
+ // remove attachment resources
+ foreach($this->attachment_streams as $stream) {
+ fclose($stream);
+ }
+ }
+
+ /**
+ * Set the file path we use to get the email text
+ * @return Object MimeMailParser Instance
+ * @param $mail_path Object
+ */
+ public function setPath($path) {
+ // should parse message incrementally from file
+ $this->resource = mailparse_msg_parse_file($path);
+ $this->stream = fopen($path, 'r');
+ $this->parse();
+ return $this;
+ }
+
+ /**
+ * Set the Stream resource we use to get the email text
+ * @return Object MimeMailParser Instance
+ * @param $stream Resource
+ */
+ public function setStream($stream) {
+
+ // streams have to be cached to file first
+ if (get_resource_type($stream) == 'stream') {
+ $tmp_fp = tmpfile();
+ if ($tmp_fp) {
+ while(!feof($stream)) {
+ fwrite($tmp_fp, fread($stream, 2028));
+ }
+ fseek($tmp_fp, 0);
+ $this->stream =& $tmp_fp;
+ } else {
+ throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.');
+ return false;
+ }
+ fclose($stream);
+ } else {
+ $this->stream = $stream;
+ }
+
+ $this->resource = mailparse_msg_create();
+ // parses the message incrementally low memory usage but slower
+ while(!feof($this->stream)) {
+ mailparse_msg_parse($this->resource, fread($this->stream, 2082));
+ }
+ $this->parse();
+ return $this;
+ }
+
+ /**
+ * Set the email text
+ * @return Object MimeMailParser Instance
+ * @param $data String
+ */
+ public function setText($data) {
+ $this->resource = mailparse_msg_create();
+ // does not parse incrementally, fast memory hog might explode
+ mailparse_msg_parse($this->resource, $data);
+ $this->data = $data;
+ $this->parse();
+ return $this;
+ }
+
+ /**
+ * Parse the Message into parts
+ * @return void
+ * @private
+ */
+ private function parse() {
+ $structure = mailparse_msg_get_structure($this->resource);
+ $this->parts = array();
+ foreach($structure as $part_id) {
+ $part = mailparse_msg_get_part($this->resource, $part_id);
+ $this->parts[$part_id] = mailparse_msg_get_part_data($part);
+ }
+ }
+
+ /**
+ * Retrieve the Email Headers
+ * @return Array
+ */
+ public function getHeaders() {
+ if (isset($this->parts[1])) {
+ return $this->getPartHeaders($this->parts[1]);
+ } else {
+ throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
+ }
+ return false;
+ }
+ /**
+ * Retrieve the raw Email Headers
+ * @return string
+ */
+ public function getHeadersRaw() {
+ if (isset($this->parts[1])) {
+ return $this->getPartHeaderRaw($this->parts[1]);
+ } else {
+ throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve a specific Email Header
+ * @return String
+ * @param $name String Header name
+ */
+ public function getHeader($name) {
+ if (isset($this->parts[1])) {
+ $headers = $this->getPartHeaders($this->parts[1]);
+ if (isset($headers[$name])) {
+ return $headers[$name];
+ }
+ } else {
+ throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.');
+ }
+ return false;
+ }
+
+ /**
+ * Returns the part for the message body in the specified format
+ * @return Part or False if not found
+ * @param $type String[optional]
+ */
+ public function getMessageBodyPart($type = 'text') {
+ $mime_types = array(
+ 'text'=> 'text/plain',
+ 'html'=> 'text/html'
+ );
+ $attachment_dispositions = array("attachment","inline");
+ if (in_array($type, array_keys($mime_types))) {
+ foreach($this->parts as $part) {
+ $disposition = $this->getPartContentDisposition($part);
+ $mime_type = $this->getPartContentType($part);
+ if ( (!in_array($disposition, $attachment_dispositions)) &&
+ ($mime_type == $mime_types[$type]) ) {
+ return $part;
+ }
+ }
+ } else {
+ throw new Exception('Invalid type specified for MimeMailParser::getMessageBodyPart. "type" can either be text or html.');
+ }
+ return false;
+ }
+
+ /**
+ * Returns the email message body in the specified format
+ * @return Mixed String Body or False if not found
+ * @param $type Object[optional]
+ */
+ public function getMessageBody($type = 'text') {
+ $body = false;
+ $part = $this->getMessageBodyPart($type);
+ if ($part!==false) {
+ $headers = $this->getPartHeaders($part);
+ $body = $this->decode($this->getPartBody($part), array_key_exists('content-transfer-encoding', $headers) ? $headers['content-transfer-encoding'] : '');
+ }
+ return $body;
+ }
+
+ /**
+ * get the headers for the message body part.
+ * @return Array
+ * @param $type Object[optional]
+ */
+ public function getMessageBodyHeaders($type = 'text') {
+ $headers = false;
+ $part = $this->getMessageBodyPart($type);
+ if ($part!==false) {
+ $headers = $this->getPartHeaders($part);
+ }
+ return $headers;
+ }
+
+
+ /**
+ * Returns the attachments contents in order of appearance
+ * @return Array
+ * @param $type Object[optional]
+ */
+ public function getAttachments() {
+ $attachments = array();
+ $dispositions = array("attachment","inline");
+ foreach($this->parts as $part) {
+ $disposition = $this->getPartContentDisposition($part);
+ if (in_array($disposition, $dispositions)) {
+ $headers = $this->getPartHeaders($part);
+ $attachments[] = new MimeMailParser_attachment(
+ $part['disposition-filename'],
+ $this->getPartContentType($part),
+ $this->getAttachmentStream($part),
+ $disposition,
+ $headers
+ );
+ }
+ }
+ return $attachments;
+ }
+
+ /**
+ * Return the Headers for a MIME part
+ * @return Array
+ * @param $part Array
+ */
+ private function getPartHeaders($part) {
+ if (isset($part['headers'])) {
+ return $part['headers'];
+ }
+ return false;
+ }
+
+ /**
+ * Return a Specific Header for a MIME part
+ * @return Array
+ * @param $part Array
+ * @param $header String Header Name
+ */
+ private function getPartHeader($part, $header) {
+ if (isset($part['headers'][$header])) {
+ return $part['headers'][$header];
+ }
+ return false;
+ }
+
+ /**
+ * Return the ContentType of the MIME part
+ * @return String
+ * @param $part Array
+ */
+ private function getPartContentType($part) {
+ if (isset($part['content-type'])) {
+ return $part['content-type'];
+ }
+ return false;
+ }
+
+ /**
+ * Return the Content Disposition
+ * @return String
+ * @param $part Array
+ */
+ private function getPartContentDisposition($part) {
+ if (isset($part['content-disposition'])) {
+ return $part['content-disposition'];
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the raw Header of a MIME part
+ * @return String
+ * @param $part Object
+ */
+ private function getPartHeaderRaw(&$part) {
+ $header = '';
+ if ($this->stream) {
+ $header = $this->getPartHeaderFromFile($part);
+ } else if ($this->data) {
+ $header = $this->getPartHeaderFromText($part);
+ } else {
+ throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.');
+ }
+ return $header;
+ }
+ /**
+ * Retrieve the Body of a MIME part
+ * @return String
+ * @param $part Object
+ */
+ private function getPartBody(&$part) {
+ $body = '';
+ if ($this->stream) {
+ $body = $this->getPartBodyFromFile($part);
+ } else if ($this->data) {
+ $body = $this->getPartBodyFromText($part);
+ } else {
+ throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.');
+ }
+ return $body;
+ }
+
+ /**
+ * Retrieve the Header from a MIME part from file
+ * @return String Mime Header Part
+ * @param $part Array
+ */
+ private function getPartHeaderFromFile(&$part) {
+ $start = $part['starting-pos'];
+ $end = $part['starting-pos-body'];
+ fseek($this->stream, $start, SEEK_SET);
+ $header = fread($this->stream, $end-$start);
+ return $header;
+ }
+ /**
+ * Retrieve the Body from a MIME part from file
+ * @return String Mime Body Part
+ * @param $part Array
+ */
+ private function getPartBodyFromFile(&$part) {
+ $start = $part['starting-pos-body'];
+ $end = $part['ending-pos-body'];
+ fseek($this->stream, $start, SEEK_SET);
+ $body = fread($this->stream, $end-$start);
+ return $body;
+ }
+
+ /**
+ * Retrieve the Header from a MIME part from text
+ * @return String Mime Header Part
+ * @param $part Array
+ */
+ private function getPartHeaderFromText(&$part) {
+ $start = $part['starting-pos'];
+ $end = $part['starting-pos-body'];
+ $header = substr($this->data, $start, $end-$start);
+ return $header;
+ }
+ /**
+ * Retrieve the Body from a MIME part from text
+ * @return String Mime Body Part
+ * @param $part Array
+ */
+ private function getPartBodyFromText(&$part) {
+ $start = $part['starting-pos-body'];
+ $end = $part['ending-pos-body'];
+ $body = substr($this->data, $start, $end-$start);
+ return $body;
+ }
+
+ /**
+ * Read the attachment Body and save temporary file resource
+ * @return String Mime Body Part
+ * @param $part Array
+ */
+ private function getAttachmentStream(&$part) {
+ $temp_fp = tmpfile();
+
+ array_key_exists('content-transfer-encoding', $part['headers']) ? $encoding = $part['headers']['content-transfer-encoding'] : $encoding = '';
+
+ if ($temp_fp) {
+ if ($this->stream) {
+ $start = $part['starting-pos-body'];
+ $end = $part['ending-pos-body'];
+ fseek($this->stream, $start, SEEK_SET);
+ $len = $end-$start;
+ $written = 0;
+ $write = 2028;
+ $body = '';
+ while($written < $len) {
+ if (($written+$write < $len )) {
+ $write = $len - $written;
+ }
+ $part = fread($this->stream, $write);
+ fwrite($temp_fp, $this->decode($part, $encoding));
+ $written += $write;
+ }
+ } else if ($this->data) {
+ $attachment = $this->decode($this->getPartBodyFromText($part), $encoding);
+ fwrite($temp_fp, $attachment, strlen($attachment));
+ }
+ fseek($temp_fp, 0, SEEK_SET);
+ } else {
+ throw new Exception('Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.');
+ return false;
+ }
+ return $temp_fp;
+ }
+
+
+ /**
+ * Decode the string depending on encoding type.
+ * @return String the decoded string.
+ * @param $encodedString The string in its original encoded state.
+ * @param $encodingType The encoding type from the Content-Transfer-Encoding header of the part.
+ */
+ private function decode($encodedString, $encodingType) {
+ if (strtolower($encodingType) == 'base64') {
+ return base64_decode($encodedString);
+ } else if (strtolower($encodingType) == 'quoted-printable') {
+ return quoted_printable_decode($encodedString);
+ } else {
+ return $encodedString;
+ }
+ }
+
+}
+
+
+?>
diff --git a/src/ext/MimeMailParser_attachment.class.php b/src/ext/MimeMailParser_attachment.class.php new file mode 100644 index 0000000..6bd327c --- /dev/null +++ b/src/ext/MimeMailParser_attachment.class.php @@ -0,0 +1,136 @@ +<?php
+
+/**
+ * Model of an Attachment
+ */
+class MimeMailParser_attachment {
+
+ /**
+ * @var $filename Filename
+ */
+ public $filename;
+ /**
+ * @var $content_type Mime Type
+ */
+ public $content_type;
+ /**
+ * @var $content File Content
+ */
+ private $content;
+ /**
+ * @var $extension Filename extension
+ */
+ private $extension;
+ /**
+ * @var $content_disposition Content-Disposition (attachment or inline)
+ */
+ public $content_disposition;
+ /**
+ * @var $headers An Array of the attachment headers
+ */
+ public $headers;
+
+ private $stream;
+
+ public function __construct($filename, $content_type, $stream, $content_disposition, $headers) {
+ $this->filename = $filename;
+ $this->content_type = $content_type;
+ $this->stream = $stream;
+ $this->content = null;
+ $this->content_disposition = $content_disposition;
+ $this->headers = $headers;
+ }
+
+ /**
+ * retrieve the attachment filename
+ * @return String
+ */
+ public function getFilename() {
+ return $this->filename;
+ }
+
+ /**
+ * Retrieve the Attachment Content-Type
+ * @return String
+ */
+ public function getContentType() {
+ return $this->content_type;
+ }
+
+ /**
+ * Retrieve the Attachment Content-Disposition
+ * @return String
+ */
+ public function getContentDisposition() {
+ return $this->content_disposition;
+ }
+
+ /**
+ * Retrieve the Attachment Headers
+ * @return String
+ */
+ public function getHeaders() {
+ return $this->headers;
+ }
+
+ /**
+ * Retrieve the file extension
+ * @return String
+ */
+ public function getFileExtension() {
+ if (!$this->extension) {
+ $ext = substr(strrchr($this->filename, '.'), 1);
+ if ($ext == 'gz') {
+ // special case, tar.gz
+ // todo: other special cases?
+ $ext = preg_match("/\.tar\.gz$/i", $ext) ? 'tar.gz' : 'gz';
+ }
+ $this->extension = $ext;
+ }
+ return $this->extension;
+ }
+
+ /**
+ * Read the contents a few bytes at a time until completed
+ * Once read to completion, it always returns false
+ * @return String
+ * @param $bytes Int[optional]
+ */
+ public function read($bytes = 2082) {
+ return feof($this->stream) ? false : fread($this->stream, $bytes);
+ }
+
+ /**
+ * Retrieve the file content in one go
+ * Once you retreive the content you cannot use MimeMailParser_attachment::read()
+ * @return String
+ */
+ public function getContent() {
+ if ($this->content === null) {
+ fseek($this->stream, 0);
+ while(($buf = $this->read()) !== false) {
+ $this->content .= $buf;
+ }
+ }
+ return $this->content;
+ }
+
+ /**
+ * Allow the properties
+ * MimeMailParser_attachment::$name,
+ * MimeMailParser_attachment::$extension
+ * to be retrieved as public properties
+ * @param $name Object
+ */
+ public function __get($name) {
+ if ($name == 'content') {
+ return $this->getContent();
+ } else if ($name == 'extension') {
+ return $this->getFileExtension();
+ }
+ return null;
+ }
+
+}
+
+?>
\ No newline at end of file diff --git a/src/ext/PasswordHash.class.php b/src/ext/PasswordHash.class.php new file mode 100644 index 0000000..12958c7 --- /dev/null +++ b/src/ext/PasswordHash.class.php @@ -0,0 +1,253 @@ +<?php +# +# Portable PHP password hashing framework. +# +# Version 0.3 / genuine. +# +# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in +# the public domain. Revised in subsequent years, still public domain. +# +# There's absolutely no warranty. +# +# The homepage URL for this framework is: +# +# http://www.openwall.com/phpass/ +# +# Please be sure to update the Version line if you edit this file in any way. +# It is suggested that you leave the main version number intact, but indicate +# your project name (after the slash) and add your own revision information. +# +# Please do not change the "private" password hashing method implemented in +# here, thereby making your hashes incompatible. However, if you must, please +# change the hash type identifier (the "$P$") to something different. +# +# Obviously, since this code is in the public domain, the above are not +# requirements (there can be none), but merely suggestions. +# +class PasswordHash { + var $itoa64; + var $iteration_count_log2; + var $portable_hashes; + var $random_state; + + function PasswordHash($iteration_count_log2, $portable_hashes) + { + $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) + $iteration_count_log2 = 8; + $this->iteration_count_log2 = $iteration_count_log2; + + $this->portable_hashes = $portable_hashes; + + $this->random_state = microtime(); + if (function_exists('getmypid')) + $this->random_state .= getmypid(); + } + + function get_random_bytes($count) + { + $output = ''; + if (is_readable('/dev/urandom') && + ($fh = @fopen('/dev/urandom', 'rb'))) { + $output = fread($fh, $count); + fclose($fh); + } + + if (strlen($output) < $count) { + $output = ''; + for ($i = 0; $i < $count; $i += 16) { + $this->random_state = + md5(microtime() . $this->random_state); + $output .= + pack('H*', md5($this->random_state)); + } + $output = substr($output, 0, $count); + } + + return $output; + } + + function encode64($input, $count) + { + $output = ''; + $i = 0; + do { + $value = ord($input[$i++]); + $output .= $this->itoa64[$value & 0x3f]; + if ($i < $count) + $value |= ord($input[$i]) << 8; + $output .= $this->itoa64[($value >> 6) & 0x3f]; + if ($i++ >= $count) + break; + if ($i < $count) + $value |= ord($input[$i]) << 16; + $output .= $this->itoa64[($value >> 12) & 0x3f]; + if ($i++ >= $count) + break; + $output .= $this->itoa64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; + } + + function gensalt_private($input) + { + $output = '$P$'; + $output .= $this->itoa64[min($this->iteration_count_log2 + + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; + $output .= $this->encode64($input, 6); + + return $output; + } + + function crypt_private($password, $setting) + { + $output = '*0'; + if (substr($setting, 0, 2) == $output) + $output = '*1'; + + $id = substr($setting, 0, 3); + # We use "$P$", phpBB3 uses "$H$" for the same thing + if ($id != '$P$' && $id != '$H$') + return $output; + + $count_log2 = strpos($this->itoa64, $setting[3]); + if ($count_log2 < 7 || $count_log2 > 30) + return $output; + + $count = 1 << $count_log2; + + $salt = substr($setting, 4, 8); + if (strlen($salt) != 8) + return $output; + + # We're kind of forced to use MD5 here since it's the only + # cryptographic primitive available in all versions of PHP + # currently in use. To implement our own low-level crypto + # in PHP would result in much worse performance and + # consequently in lower iteration counts and hashes that are + # quicker to crack (by non-PHP code). + if (PHP_VERSION >= '5') { + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + } else { + $hash = pack('H*', md5($salt . $password)); + do { + $hash = pack('H*', md5($hash . $password)); + } while (--$count); + } + + $output = substr($setting, 0, 12); + $output .= $this->encode64($hash, 16); + + return $output; + } + + function gensalt_extended($input) + { + $count_log2 = min($this->iteration_count_log2 + 8, 24); + # This should be odd to not reveal weak DES keys, and the + # maximum valid value is (2**24 - 1) which is odd anyway. + $count = (1 << $count_log2) - 1; + + $output = '_'; + $output .= $this->itoa64[$count & 0x3f]; + $output .= $this->itoa64[($count >> 6) & 0x3f]; + $output .= $this->itoa64[($count >> 12) & 0x3f]; + $output .= $this->itoa64[($count >> 18) & 0x3f]; + + $output .= $this->encode64($input, 3); + + return $output; + } + + function gensalt_blowfish($input) + { + # This one needs to use a different order of characters and a + # different encoding scheme from the one in encode64() above. + # We care because the last character in our encoded string will + # only represent 2 bits. While two known implementations of + # bcrypt will happily accept and correct a salt string which + # has the 4 unused bits set to non-zero, we do not want to take + # chances and we also do not want to waste an additional byte + # of entropy. + $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $output = '$2a$'; + $output .= chr(ord('0') + $this->iteration_count_log2 / 10); + $output .= chr(ord('0') + $this->iteration_count_log2 % 10); + $output .= '$'; + + $i = 0; + do { + $c1 = ord($input[$i++]); + $output .= $itoa64[$c1 >> 2]; + $c1 = ($c1 & 0x03) << 4; + if ($i >= 16) { + $output .= $itoa64[$c1]; + break; + } + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 4; + $output .= $itoa64[$c1]; + $c1 = ($c2 & 0x0f) << 2; + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 6; + $output .= $itoa64[$c1]; + $output .= $itoa64[$c2 & 0x3f]; + } while (1); + + return $output; + } + + function HashPassword($password) + { + $random = ''; + + if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) { + $random = $this->get_random_bytes(16); + $hash = + crypt($password, $this->gensalt_blowfish($random)); + if (strlen($hash) == 60) + return $hash; + } + + if (CRYPT_EXT_DES == 1 && !$this->portable_hashes) { + if (strlen($random) < 3) + $random = $this->get_random_bytes(3); + $hash = + crypt($password, $this->gensalt_extended($random)); + if (strlen($hash) == 20) + return $hash; + } + + if (strlen($random) < 6) + $random = $this->get_random_bytes(6); + $hash = + $this->crypt_private($password, + $this->gensalt_private($random)); + if (strlen($hash) == 34) + return $hash; + + # Returning '*' on error is safe here, but would _not_ be safe + # in a crypt(3)-like function used _both_ for generating new + # hashes and for validating passwords against existing hashes. + return '*'; + } + + function CheckPassword($password, $stored_hash) + { + $hash = $this->crypt_private($password, $stored_hash); + if ($hash[0] == '*') + $hash = crypt($password, $stored_hash); + + return $hash == $stored_hash; + } +} + +?> diff --git a/src/ext/README.txt b/src/ext/README.txt new file mode 100644 index 0000000..df28599 --- /dev/null +++ b/src/ext/README.txt @@ -0,0 +1,16 @@ +These are class files that I've gathered from around the internet. + +I've renamed the files to follow a standard scheme. +(Could we get a standard scheme, please?) + +This is where each file came from: + +My Name : Original Name : From +GoogleVoice.class.php : class.googlevoice.php : https://code.google.com/p/phpgooglevoice/ +Identica.class.php : identica.lib.php : https://code.google.com/p/identica-php/ +MimeMailParser.class.php : MimeMailParser.class.php : https://code.google.com/p/php-mime-mail-parser/ +MimeMailParser_attachment.class.php : attachment.php : https://code.google.com/p/php-mime-mail-parser/ +PasswordHash.class.php : PasswordHash.php : http://www.openwall.com/phpass/ + +~ Luke Shumaker <lukeshu.ath.cx> +Happy Hacking! diff --git a/src/lib/Auth.class.php b/src/lib/Auth.class.php new file mode 100644 index 0000000..4c2a9c6 --- /dev/null +++ b/src/lib/Auth.class.php @@ -0,0 +1,105 @@ +<?php +require_once('MessageManager.class.php'); + +class Auth { + protected $mm = null; + protected $uid = false; + public function __construct($uid) { + global $mm; + $this->mm = $mm; + $this->uid = $uid; + } + public function getUID() { + return $this->uid; + } + + /**********************************************************************\ + * The 'auth' table. * + \**********************************************************************/ + + // Row Type //////////////////////////////////////////////////////////// + /** + * @return 0=unverified 1=user 2=admin 3=group + */ + protected function getType() { + $type = $this->mm->getStatus($this->uid); + return $type; + } + protected function setType($type) { + return $this->mm->setStatus($this->uid, $type); + } + public function isUser() { + $type = $this->getType(); + return ($type===1) || ($type===2); + } + public function isAdmin() { + $type = $this->getType(); + return ($type===2); + } + public function isGroup() { + $type = $this->getType(); + return ($type===3); + } + public function setUser($is_user) { + $is_user = ($is_user?true:false); + if ($this->isUser() != $is_user) { + $this->setType($is_user?1:0); + } + } + public function setAdmin($is_admin) { + $is_admin = ($is_admin?true:false); + $is_user = $this->isUser(); + $this->setType($is_admin?2:($is_user?1:0)); + } + + // Permissions ///////////////////////////////////////////////////////// + public function canRead() { + $logged_in_uid = $this->mm->isLoggedIn(); + $is_me = ($logged_in_uid === $this->uid); + + $logged_in_obj = $this->mm->getAuthObj($logged_in_uid); + $is_user = $logged_in_obj->isUser(); + + return ($is_me || $is_user); + } + public function canEdit() { + $logged_in_uid = $this->mm->isLoggedIn(); + $is_me = ($logged_in_uid === $this->uid); + + $logged_in_obj = $this->mm->getAuthObj($logged_in_uid); + $is_admin = $logged_in_obj->isAdmin(); + + return ($is_me || $is_admin); + } + + // [user|group]name //////////////////////////////////////////////////// + public function getName() { + if (!$this->canRead()) return false; + return $this->mm->getUsername($this->uid); + } + public function setName($new_name) { + if (!$this->canEdit()) return false; + return $this->mm->setUsername($this->uid, $new_name); + } + + /**********************************************************************\ + * The 'users' table. * + \**********************************************************************/ + + public function getConf($setting) { + if (!$this->canRead()) return false; + return $this->mm->getUserConf($this->uid, $setting); + } + public function setConf($setting, $value) { + if (!$this->canEdit()) return false; + return $this->mm->setUserConf($this->uid, $setting, $value); + } + public function getConfArray($setting) { + $string = $this->getConf($setting); + return $this->mm->valueToArray($string); + } + public function setConfArray($setting, $list) { + $string = $this->mm->arrayToValue($list); + return $this->setConf($setting, $string); + } +} diff --git a/src/lib/ContactMethod.class.php b/src/lib/ContactMethod.class.php new file mode 100644 index 0000000..c01374e --- /dev/null +++ b/src/lib/ContactMethod.class.php @@ -0,0 +1,35 @@ +<?php + +global $CONTACT_METHODS; +if (!isset($CONTACT_METHODS)) { + $CONTACT_METHODS = array(); +} + +class ContactMethod { + public $verb_slug = ''; // sms + public $addr_slug = ''; // phone + public $verb_text = ''; // text message + public $addr_text = ''; // phone number + + public $handler = null; + + public function __construct($verb_slug, $addr_slug, + $verb_text, $addr_text) + { + $this->$verb_slug = $verb_slug; + $this->$addr_slug = $addr_slug; + $this->$verb_text = $verb_text; + $this->$addr_Text = $addr_text; + + global $CONTACT_METHODS; + $CONTACT_METHODS[$slug] = $this; + } + public function setHandler($handler) { + $this->$handler = $handler; + } +} + +new ContactMethod('sms' , 'phone' , + 'text message', 'cell number' ); +new ContactMethod('email' , 'email' , + 'email' , 'email address'); diff --git a/src/lib/Controller.class.php b/src/lib/Controller.class.php new file mode 100644 index 0000000..592ea2c --- /dev/null +++ b/src/lib/Controller.class.php @@ -0,0 +1,80 @@ +<?php + +class Controller { + /** + * Find the best view file to include based on file extension and HTTP + * 'Accept' headers. + */ + private function _resolveView($view) { + require_once('Mime.class.php'); + require_once('HTTP_Accept.class.php'); + + // Make a list of candidate views + $glob_string = VIEWPATH.'/pages/'.$view.'.*.php'; + $files = glob($glob_string); + + // Return false if there were no candidate views. + if (count($files) < 1) return false; + + // $prefs is a associative array where the key is the file + // extension, and the value is how much we like that extension. + // Higher numbers are better. + $prefs = array(); + + // $accept will tell us how much we like a given mime type, + // based on the ACCEPT constant. + $accept = new HTTP_Accept(ACCEPT); + + // Loop through the candidate views, and record how much we + // like each. + foreach ($files as $file) { + $ext = preg_replace('@[^.]*\.(.*)\.php$@','$1', $file); + $mimes = Mime::ext2mime($ext); + foreach ($mimes as $mime) { + $quality = $accept->getQuality($mime); + if (isset($final[$ext])) { + $quality = max($final[$ext], $quality); + } + $prefs[$ext] = $quality; + } + } + + // Sort $prefs such that the entry with the highest value will + // appear first. + arsort($prefs); + + // Return the first entry in $prefs. + foreach ($prefs as $ext => $quality) { + return VIEWPATH."/pages/$view.$ext.php"; + } + } + + /** + * Show a $view, in the most appropriate format (according to file + * extension and HTTP Accept header). Pass the array $vars to the view. + */ + protected function showView($view, $vars=null) { + global $VARS, $mm; + if ($vars===null) { $vars = array(); } + $VARS = $vars; + $VARS['template'] = $mm->template(); + include($this->_resolveView($view)); + unset($VARS); + } + + // Here be default handlers //////////////////////////////////////////// + + public function index($routed, $remainder) { + header('Content-type: text/plain'); + echo " == Generic Controller Index == \n\n"; + $routed_str = implode('/', $routed); + $remainder_str = implode('/', $remainder); + echo "Full path: $routed_str/$remainder_str\n"; + echo "Controller path: $routed_str\n"; + echo "Remainder path: $remainder_str\n"; + } + public function http404($routed, $remainder) { + $this->showView('http404', array('routed'=>$routed, + 'remainder'=>$remainder)); + } +} diff --git a/src/lib/Group.class.php b/src/lib/Group.class.php new file mode 100644 index 0000000..96c5e2c --- /dev/null +++ b/src/lib/Group.class.php @@ -0,0 +1,23 @@ +<?php +require_once('Auth.class.php'); + +class User extends Auth { + public function __construct($uid) { + parent::__construct($uid); + } + public function getUID() { + return $this->uid; + } + + /**********************************************************************\ + * The 'auth' table. * + \**********************************************************************/ + + /**********************************************************************\ + * The 'users' table. * + \**********************************************************************/ + + public function getMembers() { + return $this->mm->getUsersInGroup($this->getName()); + } +} diff --git a/src/lib/MessageHandler.class.php b/src/lib/MessageHandler.class.php new file mode 100644 index 0000000..2dce491 --- /dev/null +++ b/src/lib/MessageHandler.class.php @@ -0,0 +1,55 @@ +<?php + + +require_once('send/SenderGVSMS.class.php'); +require_once('send/SenderIdentica.class.php'); + +set_include_path(get_include_path().PATH_SEPARATOR."$BASE/src/plugins"); + +class MessageHandler { + public function __constructor() { + + } + public function loadPlugin($plugin_name) { + global $m; + + require_once("$plugin.class.php"); + $obj = new $plugin; + $params = call_user_func("$plugin::configList"); + foreach ($params as $param => $type) { + $value = $m->getPluginConf($plugin, $param); + if ($value!==false) { + switch ($type) { + case 'text': + case 'password': + $value = "$value"; + break; + case 'int': + $value = (int)$value; + break; + } + configSet($param, $value); + } + } + return $obj; + } + public function main() { + global $BASE; + + $private_senders = array(); + $broadcast_senders = array(); + + $plugin_list = $m->getSysConf('plugins'); + $plugins = explode(',', $plugin_list); + foreach ($plugins as $plugin) { + require_once("$plugin.class.php"); + if (is_subclass_of($plugin, 'SenderPrivate')) { + $private_senders[] = $this->loadPlugin($plugin); + } + if (is_subclass_of($plugin, 'SenderBroadcast')) { + $broadcast_senders[] = $this->loadPlugin($plugin); + } + } + //foreach ($private_senders) + } +}
\ No newline at end of file diff --git a/src/lib/MessageManager.class.php b/src/lib/MessageManager.class.php new file mode 100644 index 0000000..d9d9fbc --- /dev/null +++ b/src/lib/MessageManager.class.php @@ -0,0 +1,489 @@ +<?php + +class MessageManager { + private $conf; + private $mysql; + private $db_prefix; + private $pw_hasher; + private $template; + private $base; + private $users = array(); + + // Low-Level SQL functions ///////////////////////////////////////////// + + private function mysql() { + if (!isset($this->mysql)) { + $this->mysql_init(); + } + return $this->mysql; + } + private function mysql_init() { + global $db_config; + require($this->conf); + $this->mysql = mysql_connect($db_config['host'], + $db_config['user'], + $db_config['password']); + mysql_set_charset($db_config['charset'], $this->mysql); + mysql_select_db($db_config['name'], $this->mysql); + $this->db_prefix = $db_config['prefix']; + unset($db_config); + } + private function mysql_table($table_name) { + $mysql = $this->mysql(); + $prefix = $this->db_prefix; + return $prefix.mysql_real_escape_string($table_name, $mysql); + } + private function mysql_escape($string) { + $mysql = $this->mysql(); + return mysql_real_escape_string($string, $mysql); + } + private function mysql_query($query) { + $mysql = $this->mysql(); + return mysql_query($query, $mysql); + } + public function mysql_error() { + $mysql = $this->mysql(); + return mysql_error($mysql); + } + + // High-Level SQL functions //////////////////////////////////////////// + + // The 'auth' table + + public function getUID($username) { + $t = $this->mysql_table('auth'); + $v = $this->mysql_escape($username); + $query = + "SELECT * \n". + "FROM $t \n". + "WHERE name='$v' ;"; + $q = $this->mysql_query($query); + $user = mysql_fetch_array($q); + if (isset($user['uid'])) { + return (int)$user['uid']; + } else { + return false; + } + } + public function getUsername($uid) { + if (!is_int($uid)) return false; + $t = $this->mysql_table('auth'); + $query = + "SELECT * \n". + "FROM $t \n". + "WHERE uid=$uid ;"; + $q = $this->mysql_query($query); + $user = mysql_fetch_array($q); + if (isset($user['name'])) { + return $user['name']; + } else { + return false; + } + } + public function setUsername($uid, $username) { + if (!is_int($uid)) return false; + if ($this->getUID($username) !== false) { + return false; + } + $table = $this->mysql_table('auth'); + $name = $this->mysql_escape($username); + $query = + "UPDATE $table \n". + "SET name='$name' \n". + "WHERE uid=$uid ;"; + $q = $this->mysql_query($query); + return ($q?true:false); + } + public function getPasswordHash($uid) { + if (!is_int($uid)) return false; + + $table = $this->mysql_table('auth'); + $query = + "SELECT * \n". + "FROM $table \n". + "WHERE uid=$uid ;"; + $q = $this->mysql_query($query); + $user = mysql_fetch_array($q); + if (isset($user['hash'])) { + return $user['hash']; + } else { + return false; + } + } + public function setPassword($uid, $password) { + if (!is_int($uid)) return false; + $table = $this->mysql_table('auth'); + + $hasher = $this->hasher(); + @$hash = $hasher->HashPassword($password); + $query = + "UPDATE $table \n". + "SET hash='$hash' \n". + "WHERE uid=$uid ;"; + $q = $this->mysql_query($query); + return ($q?true:false); + } + public function addUser($username, $password) { + $user_exits = $this->getUID($username); + if ($user_exists) { + return false; + } + + $table = $this->mysql_table('auth'); + $user = $this->mysql_escape($username); + $hasher = $this->hasher(); + @$hash = $hasher->HashPassword($password); + $status = 0; + $query = + "INSERT INTO $table ( name, hash , status) \n". + "VALUES ('$user', '$hash', $status) ;"; + $this->mysql_query($query); + $uid = $this->getUID($username); + return $uid; + } + public function getStatus($uid) { + if (!is_int($uid)) return false; + $table = $this->mysql_table('auth'); + $query = + "SELECT * \n". + "FROM $table \n". + "WHERE uid=$uid ;"; + $q = $this->mysql_query($query); + $user = mysql_fetch_array($q); + if (isset($user['status'])) { + return (int)$user['status']; + } else { + return false; + } + } + public function setStatus($uid, $status) { + if (!is_int($uid)) return false; + $table = $this->mysql_table('auth'); + $s = $this->mysql_escape($status); + $query = + "UPDATE $table * \n". + "SET status=$s \n". + "WHERE uid=$uid ;"; + $q = $this->mysql_query($query); + return ($q?true:false); + } + public function countUsers() { + $table = $this->mysql_table('auth'); + $query = "SELECT COUNT(*) FROM $table;"; + $q = $this->mysql_query($query); + $row = mysql_fetch_array($q); + $count = $row[0]; + return $count; + } + public function listGroups() { + $table = $this->mysql_table('auth'); + $query = + "SELECT uid \n". + "FROM $table \n". + "WHERE status=3 ;"; + $q = $this->mysql_query($query); + $groups = array(); + while (($row = mysql_fetch_array($q)) !==false) { + $groups[] = (int)$row[0]; + } + return $groups; + } + public function listGroupNames() { + $table = $this->mysql_table('auth'); + $query = + "SELECT name \n". + "FROM $table \n". + "WHERE status=3 ;"; + $q = $this->mysql_query($query); + $groups = array(); + while (($row = mysql_fetch_array($q)) !==false) { + $groups[] = $row[0].''; + } + return $groups; + } + public function listUsers() { + $table = $this->mysql_table('auth'); + $query = + "SELECT uid \n". + "FROM $table \n". + "WHERE status < 3 ;"; + $q = $this->mysql_query($query); + $users = array(); + while (($row = mysql_fetch_array($q)) !==false) { + $users[] = (int)$row[0]; + } + return $users; + } + + // The 'users' table + + public function findUser($setting, $value) { + $t = $this->mysql_table('users'); + $k = $this->mysql_escape($setting); + $v = $this->mysql_escape($value); + $query = + "SELECT * \n". + "FROM $t \n". + "WHERE k = '$k' \n". + "AND UPPER(v)=UPPER('$v') ;"; + $q = $this->mysql_query($query); + $user = mysql_fetch_array($q); + if (isset($user['uid'])) { + return $user['uid']; + } else { + return false; + } + } + public function getUserConf($uid, $setting) { + if (!is_int($uid)) return false; + $t = $this->mysql_table('users'); + $k = $this->mysql_escape($setting); + $query = + "SELECT * \n". + "FROM $t \n". + "WHERE k='$k' \n". + "AND uid=$uid ;"; + $q = $this->mysql_query($query); + $row = mysql_fetch_array($q); + if (isset($row['v'])) { + return $row['v']; + } else { + return false; + } + } + public function setUserConf($uid, $setting, $value) { + if (!is_int($uid)) return false; + $isset = ($this->getUserConf($uid, $setting) !== false); + $t = $this->mysql_table('users'); + $k = $this->mysql_escape($setting); + $v = $this->mysql_escape($value); + if ($isset) { + $query = + "UPDATE $t \n". + "SET v = '$v' \n". + "WHERE k = '$k' \n". + "AND uid = $uid ;"; + } else { + $query = + "INSERT INTO $t ( uid, k , v ) \n". + "VALUES ($uid, '$k', '$v') ;"; + } + $q = $this->mysql_query($query); + return ($q?true:false); + } + public function getUsersInGroup($groupname) { + $table = $this->mysql_table('users'); + $group = $this->mysql_escape($groupname); + $query = + "SELECT uid \n". + "FROM $table \n". + "WHERE k='groups' \n". + "AND v LIKE '%,$group,%' ;"; + $q = $this->mysql_query($query); + $users = array(); + while (($row = mysql_fetch_array($q)) !==false) { + $users[] = $row[0]; + } + return $users; + } + + // The 'plugins' table + + public function getPluginConf($plugin, $key) { + $t = $this->mysql_table('plugins'); + $p = $this->mysql_escape($plugin); + $k = $this->mysql_escape($key); + $query = + "SELECT * \n". + "FROM $t \n". + "WHERE k='$k' \n". + "AND plugin='$p' ;"; + $q = $this->mysql_query($query); + $row = mysql_fetch_array($q); + if (isset($row['v'])) { + return $row['v']; + } else { + return false; + } + } + public function setPluginConf($plugin, $key, $value) { + $isset = ($this->getPluginConf($plugin, $key) !== false); + $t = $this->mysql_table('plugins'); + $p = $this->mysql_escape($plugin); + $k = $this->mysql_escape($key); + $v = $this->mysql_escape($value); + if ($isset) { + $query = + "UPDATE $t \n". + "SET v = '$v' \n". + "WHERE k = '$k' \n". + "AND plugin = '$p' ;"; + } else { + $query = + "INSERT INTO $t (plugin, k , v ) \n". + "VALUES ('$p' , '$k', '$v') ;"; + } + $q = $this->mysql_query($query); + return ($q?true:false); + } + + // The 'conf' table + + public function getSysConf($key) { + $t = $this->mysql_table('conf'); + $k = $this->mysql_escape($key); + $query = + "SELECT * \n". + "FROM $t \n". + "WHERE k='$k' ;"; + $q = $this->mysql_query($query); + $row = mysql_fetch_array($q); + if (isset($row['v'])) { + return $row['v']; + } else { + return false; + } + } + public function setSysConf($key, $value) { + $isset = (getSysConf($key) !== false); + $t = $this->mysql_table('conf'); + $k = $this->mysql_escape($key); + $v = $this->mysql_escape($value); + if ($isset) { + $query = + "UPDATE $t \n". + "SET v = '$v' \n". + "WHERE k = '$k' ;"; + } else { + $query = + "INSERT INTO $t ( k , v ) \n". + "VALUES ('$k', '$v') ;"; + } + $q = $this->mysql_query($query); + return ($q?true:false); + } + + // If the remaining code has to deal with SQL, you're doing it wrong. // + + public function baseUrl() { + if (!isset($this->base)) { + $this->base = $this->getSysConf('baseurl'); + } + return $this->base; + } + public function hasher() { + if (!isset($this->pw_hasher)) { + require_once('PasswordHash.class.php'); + $this->pw_hasher = new PasswordHash(8, false); + } + return $this->pw_hasher; + } + + public function template() { + if (!isset($this->template)) { + require_once(VIEWPATH.'/Template.class.php'); + $this->template = new Template($this->baseUrl(), $this); + } + return $this->template; + } + + public function login($username, $password) { + $uid = $this->getUID($username); + if ($uid===false) { + // user does not exist + return 2; + } + $hash = $this->getPasswordHash($uid); + $hasher = $this->hasher(); + if ($hasher->CheckPassword($password, $hash)) { + // success + $_SESSION['uid'] = $uid; + return 0; + } else { + // wrong password + return 1; + } + } + public function isLoggedIn() { + if ( isset($_SESSION['uid']) && ($_SESSION['uid']!='') ) { + return $_SESSION['uid']; + } else { + return false; + } + } + public function logout() { + $_SESSION['uid'] = ''; + } + + public function shortUrl($longUrl) { + $ch = curl_init('http://ur1.ca'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFILEDS, + 'longurl='.urlencode($longUrl)); + $html = curl_exec(); + preg_match('/Your ur1 is: <a href="([^"]*)">/',$html,$matches); + $shortUrl = $matches[1]; + curl_close($ch); + return $shortUrl; + } + + public function __construct($conf_file) { + $this->conf = $conf_file; + if (!file_exists($this->conf)) { + $this->base = $_SERVER['REQUEST_URI']; + $t = $this->template(); + $t->header('Message Manager'); + $t->paragraph( + 'Awe shiz, dude, conf.php doesn\'t exist, you '. + 'need to go through the '. + '<a href="installer">installer</a>.'); + $t->footer(); + exit(); + } + session_start(); + } + + public function getAuthObj($uid) { + if (!isset($this->users[$uid])) { + $is_group = ($this->getStatus($uid)===3); + if ($is_group) { + require_once('Group.class.php'); + $this->users[$uid] = new Group($uid); + } else { + require_once('User.class.php'); + $this->users[$uid] = new User($uid); + } + } + return $this->users[$uid]; + } + /** + * Strip out empty group names and duplicates, sort. + */ + private function sanitizeArray($in) { + $out = array(); + foreach ($in as $item) { + if (($item !== '')||(!in_array($item, $out))) { + $out[] = $item; + } + } + natsort($out); + return $out; + } + /** + * Translate an array into a value suitable to be stored into a + * key-value store in the database. + */ + public function arrayToValue($list) { + $out_list = $this->sanitizeArray($list); + return ','.implode(',', $out_list).','; + } + /** + * Translate a value from arrayToValue() back into an array. + */ + public function valueToArray($value) { + $raw_list = explode(',', $value); + $out_list = $this->sanitizeArray($raw_list); + return $out_list; + } +} diff --git a/src/lib/Mime.class.php b/src/lib/Mime.class.php new file mode 100644 index 0000000..40562b4 --- /dev/null +++ b/src/lib/Mime.class.php @@ -0,0 +1,45 @@ +<?php + +class Mime { + public static $mimes = array( + 'csv' => array('text/x-comma-separated-values', + 'text/comma-separated-values', + 'application/octet-stream', + 'application/vnd.ms-excel', + 'text/x-csv', 'text/csv', 'application/csv', + 'application/excel', 'application/vnd.msexcel'), + 'xhtml' => array('text/html', 'application/xhtml+xml'), + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'jpeg' => array('image/jpeg', 'image/pjpeg'), + 'jpg' => array('image/jpeg', 'image/pjpeg'), + 'jpe' => array('image/jpeg', 'image/pjpeg'), + 'png' => array('image/png', 'image/x-png'), + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'txt' => 'text/plain', + 'json' => array('application/json', 'text/json') + ); + + public static function ext2mime($ext) { + $mimes = self::$mimes; + $mime = $mimes[$ext]; + if (!is_array($mime)) $mime = array($mime); + return $mime; + } + public static function mime2ext($my_mime) { + $ret = array(); + foreach (self::mimes as $ext => $mime) { + if (is_array($mime)) { + $match = in_array($my_mime, $mime); + } else { + $match = $my_mime==$mime; + } + if ($match) $ret[] = $ext; + } + return $ret; + } +}
\ No newline at end of file diff --git a/src/lib/Model.class.php b/src/lib/Model.class.php new file mode 100644 index 0000000..523976e --- /dev/null +++ b/src/lib/Model.class.php @@ -0,0 +1,3 @@ +<?php + +class Model {} diff --git a/src/lib/Plugin.class.php b/src/lib/Plugin.class.php new file mode 100644 index 0000000..f2251d2 --- /dev/null +++ b/src/lib/Plugin.class.php @@ -0,0 +1,16 @@ +<?php + +abstract class Plugin { + protected $config = Array(); + + public abstract static function configList(); + public abstract static function description(); + + public function configSet($param, $value) { + if (isset($this->config[$param])) { + $this->config[$param]=$value; + } + } + + public abstract function init(); +}
\ No newline at end of file diff --git a/src/lib/Router.class.php b/src/lib/Router.class.php new file mode 100644 index 0000000..459034d --- /dev/null +++ b/src/lib/Router.class.php @@ -0,0 +1,110 @@ +<?php + +require_once('Controller.class.php'); + +class Router { + /** + * Array mapping URIs to controllers. + * A controller may register itself either by using + * Router::register($URI, $controller[, $function]); + * or by adding itself to the $ROUTER global. + * + * The default here just gives us a 404 handler. + */ + private $routes = array('/*' => 'Http404'); + + /** + * Instantiate a router that looks for controllers in $controllerpath. + */ + public function Router($controllerpath) { + // create a $ROUTES global that can be used to set up our + // $this->routes. + global $ROUTES; + $ROUTES = $this->routes; + + // Split $controllerpath into directories, and load the + // controllers in each. + $dirs = explode(PATH_SEPARATOR, $controllerpath); + foreach ($dirs as $dir) { + // Find all files in $dir with the ext `.class.php' + $controllerfiles = glob($dir.'/*.class.php'); + foreach ($controllerfiles as $file) { + // and include them + require_once($file); + } + } + + $this->routes = $ROUTES; + unset($ROUTES); + } + + /** + * Route the page at the relative URL $page to the appropriate + * controller, and call the appropriate function. + */ + public function route($page) { + $parts = explode('/', $page); + $length = count($parts); // the # of segments in $controllerpart + + // if $page ends in "/", strip that off + if ($parts[$length-1]=='') { + array_pop($parts); + $length--; + } + + $controllerpart = implode('/', $parts); + + // Keep shortening $controllerpart until it matches something in + // $this->routes. The shortest it will ever become is '/*'. + // If no key exists for '/*', that's an infinite loop. + // Fortunately, the default value of $this->routes directs '/*' + // to the Http404 controller. + while(!isset($this->routes[$controllerpart])) { + $some_parts = array_slice($parts, 0, $length); + $controllerpart = implode('/', $some_parts).'/*'; + $length--; + } + $length++; + + // Figure what function to call on what controller + // Grammar Nazi Warning: `what' or `which'? + $controller = $this->routes[$controllerpart]; + if (strpos($controller, '->')===false) { + // imply function + $function = $parts[$length]; + } else { + preg_match('/(.*)->(.*)/', $controller, $matches); + $controller = $matches[1]; + $function = $matches[2]; + } + + // Default to the `index' function, provided by all controllers + if ($function=='') { + $function = 'index'; + } + + // We will pass these arrays to the function. + $routed = array_slice($parts, 0, $length); + $remainder = array_slice($parts, $length); + + // Finally, run the controller + $obj = new $controller(); + if (in_array($function, get_class_methods($obj))) { + call_user_func(array($obj, $function), + $routed, $remainder); + } else { + $obj->http404($routed, $remainder); + } + } + + /** + * This is to allow controllers to register themselves to the router. + * If $function=='', then the function will be determined by the segment + * to the right of the last segment in $path + */ + public static function register($path, $controller, $function='') { + $str = $controller.(($function=='')?'':'->'.$function); + global $ROUTES; + $ROUTES[$path] = $str; + } +}
\ No newline at end of file diff --git a/src/lib/SenderBroadcast.class.php b/src/lib/SenderBroadcast.class.php new file mode 100644 index 0000000..7510ff2 --- /dev/null +++ b/src/lib/SenderBroadcast.class.php @@ -0,0 +1,7 @@ +<?php + +require_once('Plugin.class.php'); + +abstract class SenderBroadcast extends Plugin { + public abstract function send($id, $subject, $body); +} diff --git a/src/lib/SenderPrivate.class.php b/src/lib/SenderPrivate.class.php new file mode 100644 index 0000000..e6f2807 --- /dev/null +++ b/src/lib/SenderPrivate.class.php @@ -0,0 +1,7 @@ +<?php + +require_once('Plugin.class.php'); + +abstract class SenderPrivate extends Plugin { + public abstract function send($to, $id, $subject, $body); +} diff --git a/src/lib/User.class.php b/src/lib/User.class.php new file mode 100644 index 0000000..c1888b5 --- /dev/null +++ b/src/lib/User.class.php @@ -0,0 +1,25 @@ +<?php +require_once('Auth.class.php'); + +class User extends Auth { + public function __construct($uid) { + parent::__construct($uid); + } + public function getUID() { + return $this->uid; + } + + /**********************************************************************\ + * The 'auth' table. * + \**********************************************************************/ + + public function setPassword($password) { + if (!$this->canEdit()) return false; + return $this->mm->setPassword($this->uid, $password); + } + + /**********************************************************************\ + * The 'users' table. * + \**********************************************************************/ + +} diff --git a/src/plugins/SenderGVSMS.class.php b/src/plugins/SenderGVSMS.class.php new file mode 100644 index 0000000..777586c --- /dev/null +++ b/src/plugins/SenderGVSMS.class.php @@ -0,0 +1,35 @@ +<?php + +require_once('SenderPrivate.class.php'); +require_once('GoogleVoice.class.php'); + +class SenderGVSMS extends SenderPrivate { + protected $config = array('username'=>'', + 'password'=>'', + 'length'=>160); + private $obj; + + public static function description() { + return 'Send messages over SMS via GoogleVoice.'; + } + + public static function configList() { + return array('username'=>'text', + 'password'=>'password'); + } + + public function init() { + $this->obj = new GoogleVoice($this->config['username'], + $this->config['password']); + } + + public function send($phoneNum, $id, $subject, $body) { + global $shorturl, $messenger; + $url = $shorturl->get($messenger->id2url($id)); + $maxlen = $this->config['length']-(strlen($url)+1); + if($maxlen < strlen($subject)) { + $subject = substr($subject,0,$maxlen-3).'...'; + } + $this->obj->sms($phoneNum, $subject.' '.$url); + } +} diff --git a/src/plugins/SenderIdentica.class.php b/src/plugins/SenderIdentica.class.php new file mode 100644 index 0000000..4bb20c9 --- /dev/null +++ b/src/plugins/SenderIdentica.class.php @@ -0,0 +1,36 @@ +<?php + +require_once('SenderBroadcast.class.php'); +require_once('Identica.class.php'); + +class SenderIdentica extends SenderBroadcast { + protected $config = array('username'=>'', + 'password'=>'', + 'length'=>140); + private $obj; + + public static function description() { + return ''; + } + + public static function configList() { + return array('username'=>'text', + 'password'=>'password', + 'length'=>'int'); + } + + public function init() { + $this->obj = new Identica($this->config['username'], + $this->config['password']); + } + + public function send($id, $subject, $body) { + global $shorturl, $messenger; + $url = $shorturl->get($messenger->id2url($id)); + $maxlen = $this->config['length']-(strlen($url)+1); + if($maxlen < strlen($subject)) { + $subject = substr($subject,0,$maxlen-3).'...'; + } + $this->obj->updateStatus($subject.' '.$url); + } +} diff --git a/src/plugins/maildir.php b/src/plugins/maildir.php new file mode 100644 index 0000000..28211b5 --- /dev/null +++ b/src/plugins/maildir.php @@ -0,0 +1,58 @@ +<?php +require_once('Getter.class.php'); +//////////////////////////////////////////////////////////////////////////////// +class Maildir implements Getter { + private $config = array('dir'=>''); + + public function configList() { + return array('dir'=>'text'); + } + + public function init() {} + + public function get() { + $this->handle_new(); + $this->handle_cur(); + $this->handle_tmp(); + } + + private function handle_new() { + // move files in new to cur + $new = $this->config['dir'].'/new'; + $cur = $this->config['dir'].'/cur'; + $dh = opendir($new); + + while (($file = readdir($dh)) !== false) { + if (substr($file,0,1)!='.' && is_file($new.'/'.$file)) { + rename($new.'/'.$file, + $cur.'/'.$file.':'); + } + } + } + private function handle_cur() { + $cur = $this->config['dir'].'/cur'; + $dh = opendir($cur); + + while (($file = readdir($dh)) !== false) { + if (substr($file,0,1)!='.' && is_file($cur.'/'.$file)) { + + } + } + } + private function handle_tmp() { + // Clean up files that haven't been accessed for 36 hours + $tmp = $this->config['dir'].'/tmp'; + $dh = opendir($cur); + + while (($file = readdir($dh)) !== false) { + if (is_file($tmp.'/'.$file)) { + $atime = fileatime($tmp.'/'.$file); + $time = time(); + if (($time-$atime)>(36*60*60)) { + unlink($tmp.'/'.$file); + } + } + } + } + +} diff --git a/src/views/Template.class.php b/src/views/Template.class.php new file mode 100644 index 0000000..62b8ba6 --- /dev/null +++ b/src/views/Template.class.php @@ -0,0 +1,287 @@ +<?php + +class Template { + private $indent = 0; + private $ret = false; + private $base = '/'; + private $mm = null; + + public function status($status) { + header($_SERVER["SERVER_PROTOCOL"]." $status"); + header("Status: $status"); + } + + public function __construct($base_url, $mm=null) { + $this->base = $base_url; + $this->mm = $mm; + } + + public function setRet($ret) { + $this->ret = $ret; + } + + private function tabs() { + $str = ''; + for ($i=0;$i<$this->indent;$i++) { $str .= "\t"; } + return $str; + } + + private function attr($attr='') { + $tags=''; + if (is_array($attr)) { + foreach($attr as $key=>$value) { + $tags .= " $key=\"$value\""; + } + } + return $tags; + } + + public function tag($tag, $attr='', $content=false) { + $tags = $this->attr($attr); + $str = $this->tabs()."<$tag$tags"; + if ($content===false) { + $str.= " />"; + } else { + $str.= ">$content</$tag>"; + } + $str.= "\n"; + if ($this->ret) return $str; + echo $str; + } + + public function openTag($tag, $attr='') { + $tags = $this->attr($attr); + $str = $this->tabs()."<$tag$tags>\n"; + $this->indent++; + if ($this->ret) return $str; + echo $str; + } + + public function closeTag($tag) { + $this->indent--; + $str = $this->tabs()."</$tag>\n"; + if ($this->ret) return $str; + echo $str; + } + + public function text($text) { + $str = $this->tabs().$text."\n"; + if ($this->ret) return $str; + echo $str; + } + + public function paragraph($text, $attr='', $return=false) { + $tabs = $this->tabs(); + $tags = $this->attr($attr); + $str = $tabs."<p$tags>"; + $str.= wordwrap($text, 78-($this->indent*8), "\n$tabs "); + $str.= "</p>\n"; + if ($this->ret||$return) return $str; + echo $str; + } + + public function link($target, $text, $return=false) { + $ret = $this->ret; + $this->ret = $return; + $str = $this->tag('a', array('href'=>$target), $text); + $this->ret = $ret; + if ($this->ret||$return) return $str; + echo $str; + } + public function url($page) { + return $this->base.rawurlencode($page); + } + + public function row($cells) { + $str = $this->openTag('tr'); + foreach ($cells as $cell) + $str.= $this->tag('td', array(), $cell); + $str.= $this->closeTag('tr'); + if ($this->ret) return $str; + echo $str; + } + private function css($file, $media) { + $str.= $this->tag('link', array('rel'=>"stylesheet", + 'type'=>"text/css", + 'href'=>$this->url($file), + 'media'=>$media)); + if ($this->ret) return $str; + echo $str; + } + public function header($title) { + $mm = $this->mm; + if ($mm==null) { + $username = false; + } else { + $username = $mm->getUsername($mm->isLoggedIn()); + } + + $ret = $this->ret; + $this->ret = true; + + $logged_in = ($username!==false); + + $str = '<?xml version="1.0" encoding="utf-8"?>'."\n"; + $str.= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"'."\n"; + $str.= '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'."\n"; + + $xmlns = "http://www.w3.org/1999/xhtml"; + $str.= $this->openTag('html', array('xmlns'=>$xmlns, + 'lang'=>"en-us", + 'dir'=>"ltr")); + $this->indent = 0; // don't indent for the <html> tag + + $str.= $this->openTag('head'); + $str.= $this->tag('title', array(), htmlspecialchars($title)); + $str.= $this->css('style.css', 'all'); + $str.= $this->css('screen.css', 'screen'); + $str.= $this->css('logo-style.css', 'screen'); + $str.= $this->closeTag('head'); + + $body_class = 'logged'.($logged_in?'in':'out'); + $str.= $this->openTag('body', array('class'=>$body_class)); + + $str.= $this->openTag('div', array('class'=>'infobar')); + if ($logged_in) { + $user = htmlentities($username); + + $str.= $this->link($this->url(''), "Home"); + $str.= $this->link($this->url("users/$user"),"@$user"); + $str.= $this->logout_button('Logout'); + } else { + $url=$_SERVER['REQUEST_URI']; + $str.= $this->openTag('form', + array('action'=>$this->url('auth'), + 'method'=>'post')); + $str.= $this->tag('input', array('type'=>'hidden', + 'name'=>'action', + 'value'=>'login')); + $str.= $this->tag('input', array('type'=>'hidden', + 'name'=>'url', + 'value'=>$url)); + $str.= $this->tag('label', + array('for'=>'username'),'Username:'); + $str.= $this->tag('input', array('type'=>'text', + 'name'=>'username', + 'id'=>'username')); + $str.= $this->tag('label', + array('for'=>'password'),'Password:'); + $str.= $this->tag('input', array('type'=>'password', + 'name'=>'password', + 'id'=>'password')); + $str.= $this->tag('input', array('type'=>'submit', + 'value'=>'Login')); + $str.= $this->closeTag('form'); + } + $str.= $this->closeTag('div'); + + $str.= $this->openTag('div',array('class'=>'main')); + $str.= $this->openTag('div',array('class'=>'main_sub')); + + $this->ret = $ret; + if ($this->ret) return $str; + echo $str; + } + + public function footer() { + $str = $this->closeTag('div'); + $str.= $this->closeTag('div'); + $str.= $this->closeTag('body'); + $str.= $this->closeTag('html'); + + if ($this->ret) return $str; + echo $str; + } + + public function openFieldset($name, $lock=false) { + $class = ($lock?' class="readonly"':''); + $str = $this->text("<fieldset$class><legend>$name</legend><ul>"); + $this->indent++; + if ($this->ret) return $str; + echo $str; + } + + public function closeFieldset() { + $this->indent--; + $str = $this->text("</ul></fieldset>"); + if ($this->ret) return $str; + echo $str; + } + + public function input($id, $label, $hint, $html) { + $str = $this->openTag('li'); + $str.= $this->tag('label', array('for'=>$id), $label); + $str.= $this->text($html); + if (strlen($hint)>0) { + $str.=$this->paragraph($hint, + Array('class'=>'form_data')); + } + $str.= $this->closeTag('li'); + if ($this->ret) return $str; + echo $str; + } + + private function inputStr($type, $id, $default, $lock) { + $value = htmlentities($default); + $tag = ($lock?"readonly='readonly' ":''); + return "<input type='$type' name='$id' id='$id' value=\"$value\" $tag/>"; + } + + public function inputText($id, $label, $hint='', $default='', $lock=FALSE) { + return $this->input($id, $label, $hint, + $this->inputStr('text', $id, $default, $lock)); + } + + public function inputPassword($id, $label, $hint='', $default='', $lock=FALSE) { + return $this->input($id, $label, $hint, + $this->inputStr('password', $id, $default, $lock)); + } + + public function inputNewPassword($id, $label, $default='', $lock=FALSE) { + return $this->input($id, $label, + "Type the same password twice, to make sure you don't mistype.", + $this->inputStr('password', $id, $default, $lock). + "\n".$this->tabs()."\t". + $this->inputStr('password', $id.'_verify', $default,$lock)); + } + public function inputBool($name, $value, $label, $default=FALSE, $lock=FALSE) { + $attrib = array('type'=>'checkbox', + 'id'=>$name.'_'.$value, + 'name'=>$name.'[]', + 'value'=>$value); + if ($default) $attrib['checked']='checked'; + if ($lock ) $attrib['readonly']='readonly'; + + $str = $this->openTag('li'); + $str.= $this->tag('input', $attrib); + $str.= $this->tag('label', array('for'=>$id), $label); + $str.= $this->closeTag('li'); + + if ($this->ret) return $str; + echo $str; + + } + + public function inputP($text, $error=false) { + $str = $this->openTag('li'); + $str.=$this->paragraph($text, + array('class'=>($error?' error':''))); + $str.= $this->closeTag('li'); + if ($this->ret) return $str; + echo $str; + } + + public function logout_button($text) { + $str = $this->openTag('form',array('action'=>$this->url('auth'), + 'method'=>"post", + 'style'=>'display:inline')); + $str.= $this->tag('input', array('type'=>'hidden', + 'name'=>'action', + 'value'=>'logout')); + $str.= $this->tag('input', array('type'=>'submit', + 'value'=>$text)); + $str.= $this->closeTag('form'); + if ($this->ret) return $str; + echo $str; + } +} diff --git a/src/views/pages/404.php b/src/views/pages/404.php new file mode 100644 index 0000000..f15d39e --- /dev/null +++ b/src/views/pages/404.php @@ -0,0 +1,11 @@ +<?php global $mm; +/** + * This is the global 404 page for MessageManager, top-level views + * should generally provide a more specific one for their sub-directories + */ +$mm->status('404 Not Found'); +$t = $mm->template(); + +$mm->header('Page Not Found'); +$t->paragraph("Awe man, the page you requested wasn't found."); +$mm->footer(); diff --git a/src/views/pages/auth.php b/src/views/pages/auth.php new file mode 100644 index 0000000..2132d67 --- /dev/null +++ b/src/views/pages/auth.php @@ -0,0 +1,65 @@ +<?php global $mm; +/** + * This is the view for the main login page. + */ + +// TODO: We should probably check to make sure PAGE is just 'auth' or +// 'auth/', and not something like 'auth/foobar', for which we should +// throw a 404. + +@$action = $_POST['action']; +switch ($action) { +case 'login': login(); break; +case 'logout': logout(); break; +case '': maybe_login(); break; +default: badrequest(); break; +} + +function maybe_login() { + global $mm; + $uid = $mm->isLoggedIn(); + if ($uid===false) { + login(); + } else { + $mm->header('Authentication'); + $t = $mm->template(); + + $username = $mm->getUsername($uid); + + $t->openTag('div',array('class'=>'login')); + $t->text("Logged in as ".htmlentities($username).'.'); + $t->logout_button('Logout'); + $t->closeTag('div'); + + $mm->footer(); + } +} + +function login() { + include(VIEWPATH.'/pages/auth/login.php'); +} + +function logout() { + global $mm; + $t = $mm->template(); + + $mm->logout(); + + $mm->header('Authentication'); + $t->paragraph('Logged out'); + $mm->footer(); +} + +function badrequest() { + global $mm; + $mm->status('400 Bad Request'); + $t = $mm->template(); + + $mm->header('Authentication'); + $t->paragraph('The recieved POST request was malformed/invalid. '. + 'If you got here from a link, this is a bug; '. + 'Let the admin know.'. + 'If you got here from outside, then the API is being '. + 'missused.'); + $mm->footer(); +} diff --git a/src/views/pages/auth/badrequest.html.php b/src/views/pages/auth/badrequest.html.php new file mode 100644 index 0000000..c1fe726 --- /dev/null +++ b/src/views/pages/auth/badrequest.html.php @@ -0,0 +1,11 @@ +<?php global $VARS; +$t = $VARS['template']; + +$t->status('400 Bad Request'); +$t->header('Authentication'); +$t->paragraph('The recieved POST request was malformed/invalid. '. + 'If you got here from a link, this is a bug; '. + 'Let the admin know.'. + 'If you got here from outside, then the API is being '. + 'used incorrectly.'); +$t->footer(); diff --git a/src/views/pages/auth/index.html.php b/src/views/pages/auth/index.html.php new file mode 100644 index 0000000..ac80140 --- /dev/null +++ b/src/views/pages/auth/index.html.php @@ -0,0 +1,12 @@ +<?php global $VARS; +$t = $VARS['template']; +$username = $VARS['username']; + +$t->header('Authentication'); + +$t->openTag('div',array('class'=>'login')); +$t->text("Logged in as ".htmlentities($username).'.'); +$t->logout_button('Logout'); +$t->closeTag('div'); + +$t->footer();
\ No newline at end of file diff --git a/src/views/pages/auth/login.html.php b/src/views/pages/auth/login.html.php new file mode 100644 index 0000000..a246a9e --- /dev/null +++ b/src/views/pages/auth/login.html.php @@ -0,0 +1,49 @@ +<?php global $VARS; +$t = $VARS['template']; +$username = $VARS['username']; +$password = $VARS['password']; + +$t->header('Authentication'); + +$t->openTag('form',array('action'=>$t->url('auth'), 'method'=>"post")); +$t->openFieldset('Login'); +switch ($VARS['login_code']) { +case -1: break; +case 0: + $t->inputP('Successfully logged in as '. + htmlentities($username).'.'); + if (isset($VARS['url'])) { + $url = htmlentities($VARS['url']); + $t->inputP($t->link($url, + 'Return to the page you were on.', + true)); + } + $t->closeFieldset(); + $t->closeTag('form'); + return; + break; +case 1: + $t->inputP("Password does not match username.", + array('class'=>'error')); + break; +case 2: + $t->inputP("Username <q>$username</q> does not exist."); + $username = ''; + break; +} +$t->inputText( 'username', 'Username:', '', $username); +$t->inputPassword('password', 'Password:', '', $password); +$t->openTag('li'); +$t->tag('input', array('type'=>'submit', 'value'=>'Login')); +$t->closeTag('li'); +$t->closeFieldset(); +$t->tag('input', array('type'=>'hidden', + 'name'=>'action', + 'value'=>'login')); +if (isset($VARS['url'])) { + $url = htmlentities($VARS['url']); + $t->tag('input', array('type'=>'hidden', + 'name'=>'url', + 'value'=>$url)); +} +$t->closeTag('form'); diff --git a/src/views/pages/auth/login.php b/src/views/pages/auth/login.php new file mode 100644 index 0000000..8a175eb --- /dev/null +++ b/src/views/pages/auth/login.php @@ -0,0 +1,63 @@ +<?php global $mm; +/** + * This isn't a separate URL, but this is what the 'auth' view loads + * when the user is attempting to log in. + * Logically, I don't think it should be in a separate file, but I think the + * general flow of things is easier to follow and edit and maintain. + */ +$username = ''; +$password = ''; + +$t = $mm->template(); + +$login = -1; +if ( isset($_POST['username']) && isset($_POST['password'])) { + $username = $_POST['username']; + $password = $_POST['password']; + $login = $mm->login($username, $password); +} + +$mm->header('Authentication'); + +$t->openTag('form',array('action'=>$mm->baseUrl().'auth','method'=>"post")); +$t->openFieldset('Login'); +switch ($login) { +case -1: break; +case 0: + $t->inputP('Successfully logged in as '. + htmlentities($username).'.'); + if (isset($_POST['url'])) { + $url = htmlentities($_POST['url']); + $t->inputP($t->link($url, + 'Return to the page you were on.', + true)); + } + $t->closeFieldset(); + $t->closeTag('form'); + return; + break; +case 1: + $t->inputP("Password does not match username.", + array('class'=>'error')); + break; +case 2: + $t->inputP("Username <q>$username</q> does not exist."); + $username = ''; + break; +} +$t->inputText( 'username', 'Username:', '', $username); +$t->inputPassword('password', 'Password:', '', $password); +$t->openTag('li'); +$t->tag('input', array('type'=>'submit', 'value'=>'Login')); +$t->closeTag('li'); +$t->closeFieldset(); +$t->tag('input', array('type'=>'hidden', + 'name'=>'action', + 'value'=>'login')); +if (isset($_POST['url'])) { + $url = htmlentities($_POST['url']); + $t->tag('input', array('type'=>'hidden', + 'name'=>'url', + 'value'=>$url)); +} +$t->closeTag('form'); diff --git a/src/views/pages/auth/logout.html.php b/src/views/pages/auth/logout.html.php new file mode 100644 index 0000000..2d00998 --- /dev/null +++ b/src/views/pages/auth/logout.html.php @@ -0,0 +1,6 @@ +<?php global $VARS; +$t = $VARS['template']; + +$t->header('Authentication'); +$t->paragraph('Logged out'); +$t->footer(); diff --git a/src/views/pages/groups.php b/src/views/pages/groups.php new file mode 100644 index 0000000..03f625f --- /dev/null +++ b/src/views/pages/groups.php @@ -0,0 +1,41 @@ +<?php global $mm; + +global $illegal_names; +$illegal_names = array('', 'new'); +global $groupname, $uid;// We will use these to pass the groupname to sub-views. + +$page_parts = explode('/', PAGE); +if (isset($page_parts[1])) { + $username = $page_parts[1]; + if ($username == '') { + unset($username); + } +} + +if (isset($username)) { // URI: "users/*" + // We'll be handing this off to another view. + if ($username === 'new') { + include(VIEWPATH.'/pages/users/new.php'); + } + + $uid = $mm->getUID($username); + if ($mm->getStatus($uid)===3) $uid = false; // ignore groups. + + if ($uid===false) { + include(VIEWPATH.'/pages/users/404.php'); + } else { + include(VIEWPATH.'/pages/users/individual.php'); + } +} else { // URI: "users" + $method = $_SERVER['REQUEST_METHOD']; + switch ($method) { + case 'PUT': + case 'POST': + // We're POSTing a new user + include(VIEWPATH.'/pages/users/create.php'); + case 'HEAD': // fall-through to GET + case 'GET': + // We're GETing an existing user + include(VIEWPATH.'/pages/users/index.php'); + } +} diff --git a/src/views/pages/http404.html.php b/src/views/pages/http404.html.php new file mode 100644 index 0000000..ffdeb07 --- /dev/null +++ b/src/views/pages/http404.html.php @@ -0,0 +1,15 @@ +<?php global $VARS; +$t = $VARS['template']; + +$routed = implode('/', $VARS['routed']); +$remainder = implode('/', $VARS['remainder']); +$full = $routed.'/'.$remainder; + +$t->status('404 Not Found'); +$t->header('Page Not Found'); +$t->paragraph("Awe man, the page you requested wasn't found."); +$t->paragraph('This folder was found: '. + '<tt>'.$t->link($t->url($routed), $routed.'/', true).'</tt>'); +$t->paragraph("But this file in it wasn't: ". + '<tt>'.$full.'</tt>'); +$t->footer(); diff --git a/src/views/pages/index.php b/src/views/pages/index.php new file mode 100644 index 0000000..ad68559 --- /dev/null +++ b/src/views/pages/index.php @@ -0,0 +1,7 @@ +<?php global $mm; +$t = $mm->template(); + +$mm->header("Main Page"); +$t->paragraph("This is the main index page."); +$t->link($mm->baseUrl().'users', 'List of all users'); +$mm->footer(); diff --git a/src/views/pages/messages.php b/src/views/pages/messages.php new file mode 100644 index 0000000..da57596 --- /dev/null +++ b/src/views/pages/messages.php @@ -0,0 +1,222 @@ +<?php
+// the first ~20 lines are so that this can be called from the command line,
+// with mail piped in. This allows us to hook it into a local mail handler.
+
+global $BASE, $m;
+
+$cmdline = isset($argv[0]); // called from the command line
+@$method = $_SERVER['REQUEST_METHOD']; // What HTTP method was used
+
+if (!isset($BASE)) {
+ $pages = dirname(__FILE__);
+ $src = dirname($pages);
+ $BASE = dirname($src);
+ set_include_path(get_include_path()
+ .PATH_SEPARATOR. "$BASE/src/lib"
+ .PATH_SEPARATOR. "$BASE/src/ext"
+ );
+}
+
+if (!$cmdline) {
+ require_once('MessageManager.class.php');
+ $m = new MessageManager($BASE.'/conf.php');
+}
+
+$uid = $m->isLoggedIn();
+$auth = ($uid!==false) && ($m->getStatus($uid)>0);
+if (!$cmdline && !$auth) {
+ $m->status('401 Unauthorized');
+ $m->header('Unauthorized');
+ $t = $m->template();
+ $t->tag('h1',array(),"401: Unauthorized");
+ $t->paragraph('You need to be logged in to view messages. :(');
+ $m->footer();
+ exit();
+}
+
+@$method = $_SERVER['REQUEST_METHOD'];
+if ( ($method=='PUT') || ($method=='POST') || $cmdline ) {
+ // We're going to be uploading a new message.
+
+ // so uniqid isn't 'secure', it doesn't need to be, it's to prevent
+ // random collisions.
+ $tmpfile = "$BASE/tmp/".uniqid(getmypid().'.');
+ $infile = ($cmdline?'php://stdin':'php://input');
+ $out = fopen($tmpfile, "w");
+ $in = fopen($infile, "r");
+ while ($data = fread($in, 1024))
+ fwrite($out, $data);
+ fclose($out);
+ fclose($in);
+ //apache_request_headers()
+ require_once('MimeMailParser.class.php');
+ $parser = new MimeMailParser();
+ $parser->setPath($tmpfile);
+ $id = preg_replace('/<(.*)>/', '$1',
+ $parser->getHeader('message-id'));
+ $id = str_replace('/', '', $id); // for security reasons
+ $msg_file = "$BASE/msg/$id";
+ rename($tmpfile, $msg_file);
+
+ if (!$cmdline) {
+ $m->status('201 Created');
+ header("Location: ".$m->baseUrl().'messages/'.$id);
+ }
+ exit();
+}
+
+global $PAGE, $BASE;
+$page_parts = explode('/',$PAGE);
+@$msg = $page_parts[1];
+if ($msg == '') {
+ $m->header('Message Index');
+ $t = $m->template();
+ $t->tag('h1',array(),"Message Index");
+
+ require_once('MimeMailParser.class.php');
+ $parser = new MimeMailParser();
+ $messages = array();
+ $dh = opendir("$BASE/msg");
+ while (($file = readdir($dh)) !== false) {
+ $path = "$BASE/msg/$file";
+ if (is_file($path)) {
+ $parser->setPath($path);
+
+ $date_string = $parser->getHeader('date');
+ $date = strtotime($date_string);
+ if (!isset($messages[$date])) $messages[$date] = array();
+ $messages[$date][] =
+ array('id'=>$file,
+ 'subject'=>$parser->getHeader('subject'));
+ }
+ }
+ closedir($dh);
+
+ $t->openTag('table');
+ foreach ($messages as $date => $message_array) {
+ foreach ($message_array as $message) {
+ $url = $m->baseUrl().'messages/'.$message['id'];
+ $subject = htmlentities($message['subject']);
+ $date_str = date('Y-m-d H:i:s',$date);
+ $t->row(array(
+ $t->link($url, $subject, true),
+ $t->link($url, $date_str, true)
+ ));
+ }
+ }
+ $t->closeTag('table');
+
+ $m->footer();
+ exit();
+}
+
+@$msg_file = "$BASE/msg/$msg";
+if (!is_file($msg_file)) {
+ $m->status('404 Not Found');
+ $m->header('Message not found | MessageManager');
+ $t = $m->template();
+ $t->tag('h1',array(),'404: Not Found');
+ $t->paragraph('The message <q>'.htmlentities($msg).'</q> was not '.
+ 'found in our database.');
+ $m->footer();
+ exit();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// In the interest of code reusability, most of the following code is //
+// independent of message manager. This section is stubs to bind into //
+// MessageManager. //
+$msg_file = $msg_file;
+$msg_id = $msg;
+@$part = $page_parts[2];
+@$subpart = $page_parts[3];
+function url($id, $part='',$subpart='') {
+ global $m;
+ return $m->baseUrl().'messages/'.$id.'/'.($part?"$part/$subpart":'');
+}
+// With the exception of one line (tagged with XXX), the following code is //
+// not specific to MessageManager. //
+// At some point I may contemplate making this use the template engine, but //
+// I like the idea of it being self-standing. //
+////////////////////////////////////////////////////////////////////////////////
+
+require_once('MimeMailParser.class.php');
+$parser = new MimeMailParser();
+$parser->setPath($msg_file);
+
+function messageLink($id) {
+ if (is_array($id)) { $id = $id[1]; }
+ return '<<a href="'.url($id).'">'.$id.'</a>>';
+}
+function parseMessageIDs($string) {
+ $base = $_SERVER['REQUEST_URL'];
+ $safe = htmlentities($string);
+ $html = preg_replace_callback(
+ '/<([^>]*)>/',
+ 'messageLink',
+ $safe);
+ return $html;
+}
+
+function row($c1, $c2) {
+ echo '<tr><td>'.$c1.'</td><td>'.$c2."</td></tr>\n";
+}
+switch ($part) {
+case '': // Show a frame for all the other parts
+ $m->header('View Message | MessageManager');
+ $t = $m->template();
+ echo "<table>\n";
+ row('To:' , htmlentities($parser->getHeader('to' )));
+ row('From:' , htmlentities($parser->getHeader('from' )));
+ row('Subject:' , htmlentities($parser->getHeader('subject' )));
+ row('In-Reply-to:', parseMessageIDs($parser->getHeader('in-reply-to')));
+ row('References:' , parseMessageIDs($parser->getHeader('references' )));
+ echo "</table>\n";
+ echo "<div class='message-body'>\n";
+ if ($parser->getMessageBodyPart('html')!==false) {
+ echo "<h2>HTML</h2>\n";
+ echo '<iframe src="'.url($msg_id,'body','html').'" ></iframe>'."\n";
+ }
+ if ($parser->getMessageBodyPart('text')!==false) {
+ echo "<h2>Plain Text</h2>\n";
+ echo '<iframe src="'.url($msg_id,'body','text').'" ></iframe>'."\n";
+ }
+ echo "</div>\n";
+ echo "<h2>Attachments</h2>\n";
+ echo "<table>\n";
+ $attachments = $parser->getAttachments();
+ foreach ($attachments as $id => $attachment) {
+ echo "<tr>";
+ echo '<td>'.htmlentities($attachment->getContentType())."</td>";
+ echo '<td><a href="'.url($msg_id,'attachment',$id).'">';
+ echo htmlentities($attachment->getFilename());
+ echo "</a></td>";
+ echo "</tr>\n";
+ }
+ echo "</table>\n";
+ $m->footer();// XXX: this is specific to MessageManager
+ break;
+case 'body':
+ $type = $subpart;
+ switch ($type) {
+ case 'text': header('Content-type: text/plain'); break;
+ case 'html': header('Content-type: text/html' ); break;
+ default:
+ }
+ echo $parser->getMessageBody($type);
+ break;
+case 'attachment':
+ $attachment_id = $subpart;
+ $attachments = $parser->getAttachments();
+ $attachment = $attachments[$attachment_id];
+
+ $type = $attachment->getContentType();
+ $filename = $attachment->getFilename();
+
+ header('Content-Type: '.$type);
+ header('Content-Disposition: attachment; filename='.$filename );
+ while($bytes = $attachment->read()) {
+ echo $bytes;
+ }
+ break;
+}
diff --git a/src/views/pages/plugins.php b/src/views/pages/plugins.php new file mode 100644 index 0000000..a526871 --- /dev/null +++ b/src/views/pages/plugins.php @@ -0,0 +1,61 @@ +<?php + +global $m; +require_once('MessageManager.class.php'); +$m = new MessageManager($BASE.'/conf.php'); + +$uid = $m->isLoggedIn(); +$auth = ($uid!==false) && ($m->getStatus($uid)>=2); + +if (!$auth) { + $m->status('401 Unauthorized'); + $m->header('Unauthorized'); + $t = $m->template(); + $t->tag('h1',array(),"401: Unauthorized"); + $t->paragraph('You need to be logged in as an admin (at least user '. + 'level 2) to edit global plugin settings. :('); + $m->footer(); + exit(); +} + +$m->header('Administrator Plugin Management'); + +$t = $m->template(); + +$t->openTag('form',array('method'=>'post','action'=>$m->baseUrl().plugins)); + +global $BASE; +set_include_path(get_include_path().PATH_SEPARATOR."$BASE/src/plugins"); + +$plugin_list = $m->getSysConf('plugins'); +$plugins = explode(',', $plugin_list); +foreach ($plugins as $plugin) { + $t->openFieldSet($plugin); + + require_once("$plugin.class.php"); + $description = call_user_func("$plugin::description"); + $params = call_user_func("$plugin::configList"); + + $t->inputP($description); + + foreach ($params as $param => $type) { + $name = $plugin.'_'.$param; + if (isset($_POST[$name])) { + $m->setPluginConf($plugin, $param, $_POST[$name]); + } + $value = $m->getPluginConf($plugin, $param); + $hint = "Type: $type"; + switch ($type) { + case 'text': + case 'int': + $t->inputText( $name, $param, $hint, $value); break; + case 'password': + $t->inputPassword($name, $param, $hint, $value); break; + } + } + $t->closeFieldSet(); +} + +$t->tag('input', array('type'=>'submit', 'value'=>'Save')); +$t->closeTag('form'); +$m->footer();
\ No newline at end of file diff --git a/src/views/pages/users.php b/src/views/pages/users.php new file mode 100644 index 0000000..9c12ee7 --- /dev/null +++ b/src/views/pages/users.php @@ -0,0 +1,44 @@ +<?php global $mm; + +global $illegal_names; +$illegal_names = array('', 'new'); +global $username, $uid;// We will use these to pass the username to sub-views. + +$page_parts = explode('/', PAGE); +if (isset($page_parts[1])) { + $username = $page_parts[1]; + if ($username == '') { + unset($username); + } +} + +if (isset($username)) { // URI: "users/*" + // We'll be handing this off to another view. + if ($username === 'new') { + include(VIEWPATH.'/pages/users/new.php'); + exit(); + } + + $uid = $mm->getUID($username); + if ($mm->getStatus($uid)===3) $uid = false; // ignore groups. + + if ($uid===false) { + include(VIEWPATH.'/pages/users/404.php'); + } else { + include(VIEWPATH.'/pages/users/individual.php'); + } +} else { // URI: "users" + $method = $_SERVER['REQUEST_METHOD']; + switch ($method) { + case 'PUT': + case 'POST': + // We're POSTing a new user + include(VIEWPATH.'/pages/users/create.php'); + break; + case 'HEAD': // fall-through to GET + case 'GET': + // We're GETing an existing user + include(VIEWPATH.'/pages/users/index.php'); + break; + } +} diff --git a/src/views/pages/users/401.html.php b/src/views/pages/users/401.html.php new file mode 100644 index 0000000..0a5a1ce --- /dev/null +++ b/src/views/pages/users/401.html.php @@ -0,0 +1,15 @@ +<?php global $VARS; +$t = $VARS['template']; + +$t->status('401 Unauthorized'); +$t->header('Unauthorized'); +$t->tag('h1', array(), "401: Unauthorized"); +if ($VARS['uid']===false) { + // Not logged in + $t->paragraph('You need to be logged in to view user-data.'); +} else { + // Logged in, so the account must not activated + $t->paragraph('Your account needs to be activated by an administrator '. + 'to view user-data.'); +} +$t->footer(); diff --git a/src/views/pages/users/404.html.php b/src/views/pages/users/404.html.php new file mode 100644 index 0000000..00f9dca --- /dev/null +++ b/src/views/pages/users/404.html.php @@ -0,0 +1,10 @@ +<?php global $VARS; +$t = $VARS['template']; +$username = $VARS['username']; + +$t->status('404 Not Found'); +$t->header('User Not Found'); +$t->tag('h1',array(),"404: Not Found"); +$t->paragraph('No user with the name <q>'. + htmlentities($username).'</q> exists.'); +$t->footer(); diff --git a/src/views/pages/users/500.html.php b/src/views/pages/users/500.html.php new file mode 100644 index 0000000..27038a4 --- /dev/null +++ b/src/views/pages/users/500.html.php @@ -0,0 +1,13 @@ +<?php global $VARS, $mm; +$t = $VARS['template']; + +$t->status('500 Internal Server Error'); +$t->header('Unknown error'); +$t->paragraph("An unknown error was encountered when creating ". + "the user. The username appears to be free, and ". + "the passwords match, so I'm assuming that the ". + "error is on our end. Sorry."); +$t->paragraph("Here's a dump of the SQL error stack, it may ". + "help us find the issue:"); +$t->tag('pre', array(), htmlentities($mm->mysql_error())); +$t->footer(); diff --git a/src/views/pages/users/created.html.php b/src/views/pages/users/created.html.php new file mode 100644 index 0000000..72aa26e --- /dev/null +++ b/src/views/pages/users/created.html.php @@ -0,0 +1,16 @@ +<?php global $VARS; +$t = $VARS['template']; +$username = $VARS['username']; + +$t->status('201 Created'); +header('Location: '.$t->url("users/$username")); +$t->header('User created'); +$t->paragraph("You can go ahead and fill out more of your ". + "user information, (click the @username link at ". + "the top) but will need to wait for an ". + "administrator to approve your account before ". + "you can really use the site. Actually, ". + "filling your info out might help approval, so ". + "that the administrator can more easily see who ". + "you are."); +$t->footer(); diff --git a/src/views/pages/users/include.php b/src/views/pages/users/include.php new file mode 100644 index 0000000..6e8c90b --- /dev/null +++ b/src/views/pages/users/include.php @@ -0,0 +1,60 @@ +<?php global $mm; + +require_once('User.class.php'); + +/** + * This will take care of possibly updating and displaying a value in the + * 'users' table. + */ +function inputText($user, $name, $label, $hint='') { + if ($user->canEdit()) { + if (isset($_POST["user_$name"])) { + $user->setConf($name, $_POST["user_$name"]); + } + } + + $current_setting = $user->getConf($name); + + global $mm; + $t = $mm->template(); + $t->inputText("user_$name", $label, $hint, $current_setting, + !$user->canEdit()); +} + +function inputArray($user, $name, $arr) { + global $mm; + $t = $mm->template(); + + if (isset($_POST[$name]) && is_array($_POST[$name])) { + $user->setConfArray($name, $_POST[$name]); + } + $defaults = $user->getConfArray($name); + + foreach ($arr as $value => $label) { + $t->inputBool($name, $value, $label, + in_array($value, $defaults), !$user->canEdit()); + } +} + +function inputNewPassword($user, $name, $label) { + @$password1 = $_POST[$name ]; + @$password2 = $_POST[$name.'_verify']; + + // Check the verify box, not main box, so that we don't get tripped by + // browsers annoyingly autocompleting the password. + $is_set = ($password2 != ''); + + global $mm; + $t = $mm->template(); + + if ($is_set) { + $matches = ( $password1 == $password2 ); + if ($matches) { + $user->setPassword($password1); + $t->inputP('Password successfully updated.'); + } else { + $t->inputP("Passwords don't match.", true); + } + } + $t->inputNewPassword($name, $label); +} diff --git a/src/views/pages/users/index.csv.php b/src/views/pages/users/index.csv.php new file mode 100644 index 0000000..527e508 --- /dev/null +++ b/src/views/pages/users/index.csv.php @@ -0,0 +1,27 @@ +<?php global $VARS; +$attribs = $VARS['template']; +$users = $VARS['users']; + +function escape($value) { + if (is_bool($value)) { + return ($value?'true':'false'); + } else { + $chars = "'" . '"' . '\\' . ','; + return addcslashes($str, $chars); + } +} + +$arr = array(); +foreach ($attribs as $attrib) { + $arr[] = escape($attrib['name']); +} +echo implode(',', $arr)."\n"; + +foreach ($users as $user) { + $arr = array(); + foreach ($attribs as $attrib) { + $props = $user[$attrib['key']]; + $arr[] = escape($props['value']); + } + echo implode(',', $arr)."\n"; +} diff --git a/src/views/pages/users/index.html.php b/src/views/pages/users/index.html.php new file mode 100644 index 0000000..5f1ab02 --- /dev/null +++ b/src/views/pages/users/index.html.php @@ -0,0 +1,65 @@ +<?php global $VARS; +$t = $VARS['template']; +$attribs = $VARS['template']; +$users = $VARS['users']; + +$t->header('Users'); + +$t->openTag('form', array('action'=>$t->url('users/index'), + 'method'=>'post')); + +$t->openTag('table'); + +$t->openTag('tr'); +foreach ($attribs as $attrib) { + $t->tag('th', array(), $attrib['name']); +} +$t->tag('th'); +$t->closeTag('tr'); + +foreach ($users as $user) { + $t->openTag('tr'); + + foreach ($attribs as $attrib) { + $props = $user[$attrib['key']]; + + $value = $props['value']; + $editable = $props['editable']; + $post_key = $props['post_key']; + $bool = is_bool($value); + + $arr = array('name'=>$post_key); + if (!$editable) { + $arr['readonly'] = 'readonly'; + if ($bool) $arr['disabled'] = $disabled; + } + if ($bool) { + if ($value==true) { + $arr['checked'] = 'checked'; + } + $arr['value'] = 'true'; + $arr['type'] = 'checkbox'; + } else { + $arr['value'] = $value; + $arr['type'] = 'text'; + } + + $t->openTag('td'); + $t->tag('input', $arr); + $t->closeTag('td'); + } + + $t->openTag('td'); + $t->link($t->url('users/'.$user['auth_name']['value']), 'More'); + $t->closeTag('td'); + + $t->closeTag('tr'); +} + +$t->closeTag('table'); + +$t->tag('input', array('type'=>'submit', + 'value'=>'Save/Update')); +$t->closeTag('form'); + +$t->footer(); diff --git a/src/views/pages/users/index.php b/src/views/pages/users/index.php new file mode 100644 index 0000000..d801faf --- /dev/null +++ b/src/views/pages/users/index.php @@ -0,0 +1,116 @@ +<?php global $mm; + +$logged_in_user = $mm->getAuthObj($mm->isLoggedIn()); +if (!$logged_in_user->isUser()) { + include(VIEWPATH.'/pages/users/401.php'); + exit(); +} + +function attrib($key, $name, $check=false) { + return array('key'=>$key, 'name'=>$name, 'checkbox'=>$check); +} + +function getSetConf($user, $key) { + global $mm; + $logged_in_user = $mm->getAuthObj($mm->isLoggedIn()); + $uid = $user->getUID(); + $post_key = $key."[$uid]"; + @$value = $_POST[$post_key]; + $editable = $user->canEdit(); + $edit = isset($_POST[$post_key]); + + switch ($key) { + case 'auth_name': + if ($editable && $edit) $user->setName($value); + $value = $user->getName(); + break; + case 'auth_user': + $editable = $editable && $logged_in_user->isAdmin(); + if ($editable && $edit) $user->setUser($value=='true'); + $value = $user->isUser(); + break; + case 'auth_admin': + $editable = $editable && $logged_in_user->isAdmin(); + if ($editable && $edit) $user->setAdmin($value=='true'); + $value = $user->isAdmin(); + break; + default: + if ($editable && $edit) $user->setConf($key, $value); + $value = $user->getConf($key); + break; + } + + return array( + 'value'=>$value, + 'post_key'=>$post_key, + 'editable'=>$editable); +} + +$attribs = array(attrib('auth_user', 'Active', true), + attrib('lastname','Last'), + attrib('firstname','First'), + attrib('hsclass','Class of'), + attrib('phone','Phone number'), + attrib('email','Email'), + attrib('auth_name', 'Username'), + ); + +//////////////////////////////////////////////////////////////////////////////// + +$t = $mm->template(); +$mm->header('Users'); + +$t->openTag('form', array('action'=>$mm->baseUrl().'users', + 'method'=>'post')); + +$t->openTag('table'); + +$t->openTag('tr'); +foreach ($attribs as $attrib) { + $t->tag('th', array(), $attrib['name']); +} +$t->tag('th'); +$t->closeTag('tr'); + +$uids = $mm->listUsers(); +foreach ($uids as $uid) { + $user = $mm->getAuthObj($uid); + $t->openTag('tr'); + + foreach ($attribs as $attrib) { + $props = getSetConf($user, $attrib['key']); + + $arr = array('name'=>$props['post_key']); + if (!$props['editable']) { + $arr['readonly'] = 'readonly'; + if ($attrib['checkbox']) $arr['disabled'] = $disabled; + } + if ($attrib['checkbox']) { + if ($props['value']) + $arr['checked'] = 'checked'; + $arr['value'] = 'true'; + $arr['type'] = 'checkbox'; + } else { + $arr['value'] = $props['value']; + $arr['type'] = 'text'; + } + + $t->openTag('td'); + $t->tag('input', $arr); + $t->closeTag('td'); + } + + $t->openTag('td'); + $t->link($mm->baseUrl().'users/'.$user->getName(), 'More'); + $t->closeTag('td'); + + $t->closeTag('tr'); +} + +$t->closeTag('table'); + +$t->tag('input', array('type'=>'submit', + 'value'=>'Save/Update')); +$t->closeTag('form'); + +$mm->footer();
\ No newline at end of file diff --git a/src/views/pages/users/individual.html.php b/src/views/pages/users/individual.html.php new file mode 100644 index 0000000..4d6e4fc --- /dev/null +++ b/src/views/pages/users/individual.html.php @@ -0,0 +1,105 @@ +<?php global $VARS, $CONTACT_METHODS; +$t = $VARS['template']; +$user = $VARS['user']; + +function inputText($user, $key, $label, $hint='') { + global $VARS; $t = $VARS['template']; + $current_setting = $user->getConf($key); + $t->inputText("user_$key", $label, $hint, $current_setting, + !$user->canEdit()); +} + +function inputArray($user, $key, $arr) { + global $VARS; $t = $VARS['template']; + $defaults = $user->getConfArray($key); + + foreach ($arr as $value => $label) { + $t->inputBool($name, $value, $label, + in_array($value, $defaults), !$user->canEdit()); + } +} + + +//////////////////////////////////////////////////////////////////////////////// + +$t->header("Users: $username"); + +$t->tag('h1', array(), ($user->canEdit()?'Edit':'View')." User (UID: $uid)"); + +if ($user->canEdit()) { + $t->openTag('form', array('method'=>'post', + 'action'=>$t->url("users/$username"))); +} else { + $t->openTag('form'); +} + +$t->openFieldset("Login / Authentication"); +// Username //////////////////////////////////////////////////////////////////// +if (isset($VARS['changed name']) && !$VARS['changed_name']) { + $t->inputP("Error setting username to ". + "<q>$new_name</q>. This is probably because". + " a user with that name already exists.", + true); +} +$t->inputText('auth_name','Username', + "This is the name you use to log in, but it is also a ". + "short name that is used in various places, think of it ". + "as a sort of <q>Twitter name</q>.", + $user->getName(), !$user->canEdit()); +// Password //////////////////////////////////////////////////////////////////// +if (@$VARS['pw_updated']===true) { + $t->inputP('Password successfully updated.'); +} +if (@$VARS['pw mixmatch']===true) { + $t->inputP("Passwords don't match.", true); +} +if ($user->canEdit()) inputNewPassword($user, 'auth_password','Reset Password'); +//////////////////////////////////////////////////////////////////////////////// +$t->closeFieldset(); + +$t->openFieldset("Information"); +inputText($user, 'firstname','First Name',''); +inputText($user, 'lastname','Last Name',''); +inputText($user, 'hsclass','Highschool Class of', + 'Please put the full year (ex: 2012)'); +$t->closeFieldset(); + + +$t->openFieldset("Contact"); +// TODO: I should make this a setting for admins to set. +$hints = array('email'=> + "Right now you can only have one email address, ". + "but I'm working on making it so you can have ". + "multiple.", + 'phone'=> + "A home phone number isn't much use here because it is ". + "used to text-message you (if you enable it), and ". + "contact you at competition." + ); +$use_arr = array(); +foreach ($CONTACT_METHODS as $method) { + inputText($user, + $method->addr_slug, + ucwords($method->addr_word), + $hints[$method->addr_slug]); + $use_arr[$method->verb_slug] = ucwords($method->verb_word); +} + +$t->inputP("When I recieve a message, notify me using the following methods:"); +inputArray($user, 'use', $use_arr); +$t->closeFieldSet(); + + +$t->openFieldSet('Groups'); +$group_arr = array(); +foreach ($VARS['groups'] as $group_name) { + $group_arr[$group_name] = ucwords($group_name); +} +inputArray($user, 'groups', $group_arr); +$t->closeFieldset(); + +if ($user->canEdit()) { + $t->tag('input', array('type'=>'submit', 'value'=>'Save')); +} +$t->closeTag('form'); +$t->footer(); diff --git a/src/views/pages/users/individual.php b/src/views/pages/users/individual.php new file mode 100644 index 0000000..2483e6b --- /dev/null +++ b/src/views/pages/users/individual.php @@ -0,0 +1,89 @@ +<?php global $mm, $uid; +// Honestly, the functions in this include should be in this file, but that +// would make this file too messy. +require_once(VIEWPATH.'/pages/users/include.php'); + +$user = $mm->getAuthObj($uid); + +if (!$user->canRead()) { + include(VIEWPATH.'/pages/users/401.php'); + exit(); +} + +// Read/Change the username +$username = $user->getName(); +if (isset($_POST['auth_name'])) { + $new_name = $_POST['auth_name']; + if ($new_name != $username) { + global $illegal_names; + if (!in_array($new_name, $illegal_names)) { + $changed_name = $user->setName($new_name); + $username = $user->getName(); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// + +$t = $mm->template(); +$mm->header("Users: $username"); + +$t->tag('h1', array(), ($user->canEdit()?'Edit':'View')." User (UID: $uid)"); + +if ($user->canEdit()) { + $t->openTag('form', array('method'=>'post', + 'action'=>$mm->baseUrl()."users/$username")); +} else { + $t->openTag('form'); +} + +$t->openFieldset("Login / Authentication"); +if (isset($changed_name) && !$changed_name) { + $t->inputP("Error setting username to ". + "<q>$new_name</q>. This is probably because". + " a user with that name already exists.", + true); +} + +$t->inputText('auth_name','Username', + "This is the name you use to log in, but it is also a ". + "short name that is used in various places, think of it ". + "as a sort of <q>Twitter name</q>.", + $username,!$user->canEdit()); +if ($user->canEdit()) inputNewPassword($user, 'auth_password','Reset Password'); +$t->closeFieldset(); + +$t->openFieldset("Information"); +inputText($user, 'firstname','First Name',''); +inputText($user, 'lastname','Last Name',''); +inputText($user, 'hsclass','Highschool Class of','Please put the full year (ex: 2012)'); +$t->closeFieldset(); + +$t->openFieldset("Contact"); +inputText($user, 'email', 'Email', + "Right now you can only have one email address, ". + "but I'm working on making it so you can have ". + "multiple."); +inputText($user, 'phone', 'Cell Number', + "A home phone number isn't much use here because it is ". + "used to text-message you (if you enable it), and ". + "contact you at competition."); +$t->inputP("When I recieve a message, notify me using the following methods:"); +inputArray($user, 'use', array('email'=>'Email', + 'sms'=>'Text Message')); +$t->closeFieldSet(); + +$t->openFieldSet('Groups'); +$groups = $mm->listGroupNames(); +$group_arr = array(); +foreach ($groups as $group_name) { + $group_arr[$group_name] = ucwords($group_name); +} +inputArray($user, 'groups', $group_arr); +$t->closeFieldset(); + +if ($user->canEdit()) { + $t->tag('input', array('type'=>'submit', 'value'=>'Save')); +} +$t->closeTag('form'); +$mm->footer(); diff --git a/src/views/pages/users/new.html.php b/src/views/pages/users/new.html.php new file mode 100644 index 0000000..f2dacb5 --- /dev/null +++ b/src/views/pages/users/new.html.php @@ -0,0 +1,37 @@ +<?php global $VARS; +$t = $VARS['template']; + +$t->header('Create new user'); + +$t->openTag('form', array('method'=>'post', + 'action'=>$t->url('users'))); + +$t->openFieldset("New User: basic login"); +if (in_array('illegal name', $VARS['errors'])) { + $t->inputP("That is a forbidden username.", true); +} +if (in_array('user exists', $VARS['errors'])) { + $t->inputP("A user with that name already exists."); +} +$t->inputText('auth_name','Username', + "This is the name you use to log in, but it is also a ". + "short name that is used in various places, think of it ". + "as a sort of <q>Twitter name</q>.",'',$VARS['username']); + +@$password = $VARS['password1']; +if ($in_array('pw mixmatch', $VARS['errors'])) { + $t->inputP("The passwords didn't match.", true); + $password = ''; +} +if (in_array('no pw', $VARS['errors'])) { + $t->inputP("You must set a password.", true); + $password = ''; +} +$t->inputNewPassword('auth_password','Password', $password); +$t->closeFieldset(); + +$t->tag('input', array('type'=>'submit', 'value'=>'Submit')); + +$t->closeTag('form'); + +$t->footer(); diff --git a/style.css b/style.css new file mode 100644 index 0000000..2c687fc --- /dev/null +++ b/style.css @@ -0,0 +1,52 @@ +body { + font-family: Sans; + margin: 0; + padding: 0; } + div.infobar { + text-align: right; + padding: .1em 0; } + .loggedin div.infobar * { + margin: 0 1em; } + div.infobar input[type="text"], + div.infobar input[type="password"] { + width: 20%; } + div.infobar input[type="submit"] { + background: transparent; + border: none; + font-size: 1em; + padding: 0; } + div.infobar a { + color: #000000; + } + div.main { + } + div.main form fieldset li { + padding: .5em 0; } + div.main form fieldset li label { + width: 25%; + float: left; } + div.main form fieldset li input { + width: 30%; + float: left; } + div.main form fieldset li p.form_data { + margin-left: 25%; } + +h1 { + text-align: center; } +a { + text-decoration: none; } +input[type="text"], textarea { + font-family: monospace; } +iframe { + width: 100%; + height: 100%; } +table, td { + border: solid 1px black; } +table input { + border: none; + width: 100%; + background: transparent; +} +.error { + font-weight: bold; + color: red; }
\ No newline at end of file |