summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Shumaker <LukeShu@sbcglobal.net>2011-08-01 01:22:36 -0400
committerLuke Shumaker <LukeShu@sbcglobal.net>2011-08-01 01:22:36 -0400
commit09dfe32eb6b538225686fd6ed0220240010bc574 (patch)
tree29c1afc5e79519ba8689a3d5d170c312d3cf5033
initial commit.
Partway through a rewrite. I have some old files I didn't want to entirely delete.
-rw-r--r--.gitignore4
-rw-r--r--.htaccess9
-rw-r--r--README.txt115
-rw-r--r--index.php44
-rw-r--r--installer/include.php81
-rw-r--r--installer/index.php235
-rw-r--r--screen.css50
-rw-r--r--src/controllers/Auth.class.php57
-rw-r--r--src/controllers/Http404.class.php7
-rw-r--r--src/controllers/Users.class.php291
-rw-r--r--src/ext/GoogleVoice.class.php84
-rw-r--r--src/ext/HTTP_Accept.class.php659
-rw-r--r--src/ext/Identica.class.php491
-rw-r--r--src/ext/MimeMailParser.class.php447
-rw-r--r--src/ext/MimeMailParser_attachment.class.php136
-rw-r--r--src/ext/PasswordHash.class.php253
-rw-r--r--src/ext/README.txt16
-rw-r--r--src/lib/Auth.class.php105
-rw-r--r--src/lib/ContactMethod.class.php35
-rw-r--r--src/lib/Controller.class.php80
-rw-r--r--src/lib/Group.class.php23
-rw-r--r--src/lib/MessageHandler.class.php55
-rw-r--r--src/lib/MessageManager.class.php489
-rw-r--r--src/lib/Mime.class.php45
-rw-r--r--src/lib/Model.class.php3
-rw-r--r--src/lib/Plugin.class.php16
-rw-r--r--src/lib/Router.class.php110
-rw-r--r--src/lib/SenderBroadcast.class.php7
-rw-r--r--src/lib/SenderPrivate.class.php7
-rw-r--r--src/lib/User.class.php25
-rw-r--r--src/plugins/SenderGVSMS.class.php35
-rw-r--r--src/plugins/SenderIdentica.class.php36
-rw-r--r--src/plugins/maildir.php58
-rw-r--r--src/views/Template.class.php287
-rw-r--r--src/views/pages/404.php11
-rw-r--r--src/views/pages/auth.php65
-rw-r--r--src/views/pages/auth/badrequest.html.php11
-rw-r--r--src/views/pages/auth/index.html.php12
-rw-r--r--src/views/pages/auth/login.html.php49
-rw-r--r--src/views/pages/auth/login.php63
-rw-r--r--src/views/pages/auth/logout.html.php6
-rw-r--r--src/views/pages/groups.php41
-rw-r--r--src/views/pages/http404.html.php15
-rw-r--r--src/views/pages/index.php7
-rw-r--r--src/views/pages/messages.php222
-rw-r--r--src/views/pages/plugins.php61
-rw-r--r--src/views/pages/users.php44
-rw-r--r--src/views/pages/users/401.html.php15
-rw-r--r--src/views/pages/users/404.html.php10
-rw-r--r--src/views/pages/users/500.html.php13
-rw-r--r--src/views/pages/users/created.html.php16
-rw-r--r--src/views/pages/users/include.php60
-rw-r--r--src/views/pages/users/index.csv.php27
-rw-r--r--src/views/pages/users/index.html.php65
-rw-r--r--src/views/pages/users/index.php116
-rw-r--r--src/views/pages/users/individual.html.php105
-rw-r--r--src/views/pages/users/individual.php89
-rw-r--r--src/views/pages/users/new.html.php37
-rw-r--r--style.css52
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 '&lt;<a href="'.url($id).'">'.$id.'</a>&gt;';
+}
+function parseMessageIDs($string) {
+ $base = $_SERVER['REQUEST_URL'];
+ $safe = htmlentities($string);
+ $html = preg_replace_callback(
+ '/&lt;([^>]*)&gt;/',
+ '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