--- /dev/null
++ login
++ register
+- settings
++ disallow login if user is logged in
++ disallow register if user is logged in
++ common_current_user()
++ common_logged_in()
++ session variable for login
++ post notice
++ logout
++ subscribe
++ unsubscribe
++ subscribe links on profile
+- licenses
+- header menu
+- footer menu
+- disallow direct to PHP files
+- common_local_url()
+- configuration system ($config)
+- RSS 1.0 feeds of a user's notices
+- RSS 1.0 dump of a user's notices
+- RSS 1.0 feed of all public notices
+- RDF dump of entire site
+- FOAF dump for user
+- delete a notice
+- make sure canonical usernames are unique
+- upload avatar
+- design from Open Source Web Designs
+- release 0.1
+- gettext
+- subscribe remote
+- add subscriber remote
+- send remote notice
+- receive remote notice
+- confirmation email
+- tinyurl-ification of URLs
+- jQuery for as much as possible
+- themes
+- release 0.2
+- @ messages
+- # tags
+- L: location
+- stay logged in between sessions
+- use RSS as a subscription
+- URL notices
+- image notices
+- video notices
+- audio notices
+- release 0.3
+- forward notices to Jabber
+- forward notices to other IM
+- forward notices to mobile phone
+- machine tags
+- release 0.4
+- include twitter subscriptions
+- include Pownce subscriptions
+- privacy
+- Wrap DB_DataObject with memcached caching layer
+- login throttle to prevent brute-force attacks
+- form token in login to prevent XSS
+- release 1.0
+- Atom Publishing Protocol
/main/login login to site
/main/register register to site
+/main/settings change account settings
/main/recover recover password
/doc/ documentation
about about this site
<?php
-function handle_login() {
- if ($_REQUEST['METHOD'] == 'POST') {
- if (login_check_user($_REQUEST['user'], $_REQUEST['password'])) {
-
+class LoginAction extends Action {
+
+ function handle($args) {
+ parent::handle($args);
+ if (common_logged_in()) {
+ common_user_error(_t('Already logged in.'));
+ } else if ($this->arg('METHOD') == 'POST') {
+ $this->check_login();
} else {
+ $this->show_form();
}
- } else {
- if (user_logged_in()) {
+ }
+
+ function check_login() {
+ # XXX: form token in $_SESSION to prevent XSS
+ # XXX: login throttle
+ $nickname = $this->arg('nickname');
+ $password = $this->arg('password');
+ if (common_check_user($nickname, $password)) {
+ common_set_user($nickname);
+ common_redirect(common_local_url('all',
+ array('nickname' =>
+ $nickname)));
} else {
- login_show_form();
+ $this->show_form(_t('Incorrect username or password.'));
}
}
-}
-function login_show_form() {
- html_start();
- html_head("Login");
- html_body();
+ function show_form($error=NULL) {
+
+ common_show_header(_t('Login'));
+ if (!is_null($error)) {
+ common_element('div', array('class' => 'error'), $msg);
+ }
+ common_start_element('form', array('method' => 'POST',
+ 'id' => 'login',
+ 'action' => common_local_url('login')));
+ common_element('label', array('for' => 'username'),
+ _t('Name'));
+ common_element('input', array('name' => 'username',
+ 'type' => 'text',
+ 'id' => 'username'));
+ common_element('label', array('for' => 'password'),
+ _t('Password'));
+ common_element('input', array('name' => 'password',
+ 'type' => 'password',
+ 'id' => 'password'));
+ common_element('input', array('name' => 'submit',
+ 'type' => 'submit',
+ 'id' => 'submit'),
+ _t('Login'));
+ common_element('input', array('name' => 'cancel',
+ 'type' => 'button',
+ 'id' => 'cancel'),
+ _t('Cancel'));
+ }
}
-
-function login_check_user($username, $password) {
-
-}
\ No newline at end of file
--- /dev/null
+<?php
+
+class LogoutAction extends Action {
+ function handle($args) {
+ parent::handle($args);
+ if (!common_logged_in()) {
+ common_user_error(_t('Not logged in.'));
+ } else {
+ common_set_user(NULL);
+ common_redirect(common_local_url('main'));
+ }
+ }
+}
--- /dev/null
+<?php
+
+class NewnoticeAction extends Action {
+
+ function handle($args) {
+ parent::handle($args);
+ # XXX: Ajax!
+
+ if (!common_logged_in()) {
+ common_user_error(_t('Not logged in.'));
+ } else if ($this->arg('METHOD') == 'POST') {
+ if ($this->save_new_notice()) {
+ # XXX: smarter redirects
+ $user = common_current_user();
+ assert(!is_null($user)); # see if... above
+ # XXX: redirect to source
+ # XXX: use Ajax instead of a redirect
+ common_redirect(common_local_url('all',
+ array('nickname' =>
+ $user->nickname)));
+ } else {
+ common_server_error(_t('Problem saving notice.'));
+ }
+ } else {
+ $this->show_form();
+ }
+ }
+
+ function save_new_notice() {
+ $user = common_current_user();
+ assert($user); # XXX: maybe an error instead...
+ $notice = DB_DataObject::factory('notice');
+ assert($notice);
+ $notice->profile_id = $user->id; # user id *is* profile id
+ $notice->content = $this->arg('content');
+ $notice->created = time();
+ return $notice->insert();
+ }
+
+ function show_form() {
+ common_start_element('form', array('id' => 'newnotice', 'method' => 'POST',
+ 'action' => common_local_url('newnotice')));
+ common_element('span', 'nickname', $profile->nickname);
+ common_element('textarea', array('rows' => 4, 'cols' => 80, 'id' => 'content'));
+ common_element('input', array('type' => 'submit'), 'Send');
+ common_end_element('form');
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+
+class RegisterAction extends Action {
+
+ function handle($args) {
+ parent::handle($args);
+
+ if (common_logged_in()) {
+ common_user_error(_t('Already logged in.'));
+ } else if ($this->arg('METHOD') == 'POST') {
+ $this->try_register();
+ } else {
+ $this->show_form();
+ }
+ }
+
+ function try_register() {
+ $nickname = $this->arg('nickname');
+ $password = $this->arg('password');
+ $confirm = $this->arg('confirm');
+ $email = $this->arg('email');
+
+ # Input scrubbing
+
+ $nickname = common_canonical_nickname($nickname);
+ $email = common_canonical_email($email);
+
+ if ($this->nickname_exists($nickname)) {
+ $this->show_form(_t('Username already exists.'));
+ } else if ($this->email_exists($email)) {
+ $this->show_form(_t('Email address already exists.'));
+ } else if ($password != $confirm) {
+ $this->show_form(_t('Passwords don\'t match.'));
+ } else if ($this->register_user($nickname, $password, $email)) {
+ common_set_user($nickname);
+ common_redirect(common_local_url('settings'));
+ } else {
+ $this->show_form(_t('Invalid username or password.'));
+ }
+ }
+
+ # checks if *CANONICAL* nickname exists
+
+ function nickname_exists($nickname) {
+ $user = User::staticGet('nickname', $nickname);
+ return ($user !== false);
+ }
+
+ # checks if *CANONICAL* email exists
+
+ function email_exists($email) {
+ $email = common_canonicalize_email($email);
+ $user = User::staticGet('email', $email);
+ return ($user !== false);
+ }
+
+ function register_user($nickname, $password, $email) {
+ # TODO: wrap this in a transaction!
+ $profile = new Profile();
+ $profile->nickname = $nickname;
+ $profile->created = time();
+ $id = $profile->insert();
+ if (!$id) {
+ return FALSE;
+ }
+ $user = new User();
+ $user->id = $id;
+ $user->nickname = $nickname;
+ $user->password = common_munge_password($password, $id);
+ $user->email = $email;
+ $user->created = time();
+ $result = $user->insert();
+ if (!$result) {
+ # Try to clean up...
+ $profile->delete();
+ }
+ return $result;
+ }
+
+ function show_form($error=NULL) {
+
+ common_show_header(_t('Login'));
+ common_start_element('form', array('method' => 'POST',
+ 'id' => 'login',
+ 'action' => common_local_url('login')));
+ common_element('label', array('for' => 'username'),
+ _t('Name'));
+ common_element('input', array('name' => 'username',
+ 'type' => 'text',
+ 'id' => 'username'));
+ common_element('label', array('for' => 'password'),
+ _t('Password'));
+ common_element('input', array('name' => 'password',
+ 'type' => 'password',
+ 'id' => 'password'));
+ common_element('label', array('for' => 'confirm'),
+ _t('Confirm'));
+ common_element('input', array('name' => 'confirm',
+ 'type' => 'password',
+ 'id' => 'confirm'));
+ common_element('label', array('for' => 'email'),
+ _t('Email'));
+ common_element('input', array('name' => 'email',
+ 'type' => 'text',
+ 'id' => 'email'));
+ common_element('input', array('name' => 'submit',
+ 'type' => 'submit',
+ 'id' => 'submit'),
+ _t('Login'));
+ common_element('input', array('name' => 'cancel',
+ 'type' => 'button',
+ 'id' => 'cancel'),
+ _t('Cancel'));
+ }
+}
--- /dev/null
+<?php
+
+class SettingsAction extends Action {
+
+ function handle($args) {
+ parent::handle($args);
+ if ($this->arg('METHOD') == 'POST') {
+ $nickname = $this->arg('nickname');
+ $fullname = $this->arg('fullname');
+ $email = $this->arg('email');
+ $homepage = $this->arg('homepage');
+ $bio = $this->arg('bio');
+ $location = $this->arg('location');
+ $oldpass = $this->arg('oldpass');
+ $password = $this->arg('password');
+ $confirm = $this->arg('confirm');
+
+ if ($password) {
+ if ($password != $confirm) {
+ $this->show_form(_t('Passwords don\'t match.'));
+ }
+ } else if (
+
+ $error = $this->save_settings($nickname, $fullname, $email, $homepage,
+ $bio, $location, $password);
+ if (!$error) {
+ $this->show_form(_t('Settings saved.'), TRUE);
+ } else {
+ $this->show_form($error);
+ }
+ } else {
+ $this->show_form();
+ }
+
\ No newline at end of file
'class' => 'nickname'),
$profile->nickname);
# FIXME: URL, image, video, audio
- common_element('span', array('class' => 'content'), $notice->content);
+ common_element('span', array('class' => 'content'),
+ $notice->content);
common_element('span', array('class' => 'date'),
common_date_string($notice->created));
common_end_element('div');
parent::handle($args);
- $nickname = $this->arg('profile');
- $profile = Profile::staticGet('nickname', strtolower($nickname));
-
- if (!$profile) {
- $this->no_such_user();
- }
-
- $user = User::staticGet($profile->id);
-
+ $nickname = common_canonicalize_nickname($this->arg('profile'));
+ $user = User::staticGet('nickname', $nickname);
+
if (!$user) {
- // remote profile
$this->no_such_user();
+ }
+
+ $profile = $user->getProfile();
+
+ if (!$profile) {
+ common_server_error(_t('User record exists without profile.'));
}
# Looks like we're good; show the header
common_show_header($profile->nickname);
+
+ $cur = common_current_user();
- if ($profile->id == current_user()->id) {
+ if ($cur && $profile->id == $cur->id) {
$this->notice_form();
}
$this->show_profile($profile);
$this->show_last_notice($profile);
+
+ if ($cur) {
+ if ($cur->isSubscribed($profile)) {
+ $this->show_unsubscribe_form($profile);
+ } else {
+ $this->show_subscribe_form($profile);
+ }
+ }
$this->show_statistics($profile);
-
+
$this->show_subscriptions($profile);
$this->show_notices($profile);
common_element('div', 'bio', $profile->bio);
}
}
+
+ function show_subscribe_form($profile) {
+ common_start_element('form', array('id' => 'subscribe', 'method' => 'POST',
+ 'action' => common_local_url('subscribe')));
+ common_element('input', array('id' => 'subscribeto',
+ 'name' => 'subscribeto',
+ 'type' => 'hidden',
+ 'value' => $profile->nickname));
+ common_element('input', array('type' => 'submit'), _t('subscribe'));
+ common_end_element('form');
+ }
+
+ function show_unsubscribe_form($profile) {
+ common_start_element('form', array('id' => 'unsubscribe', 'method' => 'POST',
+ 'action' => common_local_url('unsubscribe')));
+ common_element('input', array('id' => 'unsubscribeto',
+ 'name' => 'unsubscribeto',
+ 'type' => 'hidden',
+ 'value' => $profile->nickname));
+ common_element('input', array('type' => 'submit'), _t('unsubscribe'));
+ common_end_element('form');
+ }
function show_subscriptions($profile) {
-
- # XXX: add a limit
+ # XXX: add a limit
$subs = $profile->getLink('id', 'subscription', 'subscriber');
-
common_start_element('div', 'subscriptions');
$cnt = 0;
array('profile' => $profile->nickname))
'class' => 'moresubscriptions'),
_t('All subscriptions'));
-
+
common_end_element('div');
}
while ($notice->fetch()) {
# FIXME: URL, image, video, audio
- common_element('span', array('class' => 'content'), $notice->content);
+ common_element('span', array('class' => 'content'),
+ $notice->content);
common_element('span', array('class' => 'date'),
common_date_string($notice->created));
}
--- /dev/null
+<?php
+
+class SubscribeAction extends Action {
+ function handle($args) {
+ parent::handle($args);
+
+ if (!common_logged_in()) {
+ common_user_error(_t('Not logged in.'));
+ return;
+ }
+
+ $other_nickname = $this->arg('subscribeto');
+
+ $other = User::staticGet('nickname', $other_nickname);
+
+ if (!$other) {
+ common_user_error(_t('No such user.'));
+ return;
+ }
+
+ $user = common_current_user();
+
+ if ($user->isSubscribed($other)) {
+ common_user_error(_t('Already subscribed!.'));
+ return;
+ }
+
+ $sub = new Subscription();
+ $sub->subscriber = $user->id;
+ $sub->subscribed = $other->id;
+
+ $sub->created = time();
+
+ if (!$sub->insert()) {
+ common_server_error(_t('Couldn\'t create subscription.'));
+ return;
+ }
+
+ common_redirect(common_local_url('all', array('nickname' =>
+ $user->nickname)));
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+
+class UnsubscribeAction extends Action {
+ function handle($args) {
+ parent::handle($args);
+ if (!common_logged_in()) {
+ common_user_error(_t('Not logged in.'));
+ return;
+ }
+ $other_nickname = $this->arg('unsubscribeto');
+ $other = User::staticGet('nickname', $other_nickname);
+ if (!$other) {
+ common_user_error(_t('No such user.'));
+ return;
+ }
+
+ $user = common_current_user();
+
+ if (!$user->isSubscribed($other)) {
+ common_server_error(_t('Not subscribed!.'));
+ }
+
+ $sub = new Subscription();
+ $sub->subscriber = $user->id;
+ $sub->subscribed = $other->id;
+
+ if (!$sub->delete()) {
+ common_server_error(_t('Couldn\'t delete subscription.'));
+ return;
+ }
+
+ common_redirect(common_local_url('all', array('nickname' =>
+ $user->nickname)));
+ }
+}
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
+
+ function getProfile() {
+ return Profile::staticGet($this->$id);
+ }
+
+ function isSubscribed($other) {
+ assert(!is_null($other));
+ $sub = DB_DataObject::factory('subscription');
+ $sub->subscriber = $this->id;
+ $sub->subscribed = $other->id;
+ return $sub->find();
+ }
}
// default configuration, overwritten in config.php
$config =
- array('site' =>
+ array('site' =>
array('name' => 'Just another µB'),
'dsn' =>
array('phptype' => 'mysql',
require_once(INSTALLDIR . '/config.php');
require_once('DB.php');
-function common_database() {
- global $config;
- $db =& DB::connect($config['dsn'], $config['dboptions']);
- if (PEAR::isError($db)) {
- common_server_error($db->getMessage());
- } else {
- return $db;
- }
-}
-
-function common_read_database() {
- // XXX: read from slave server
- return common_database();
-}
+# Show a server error
function common_server_error($msg) {
header('Status: 500 Server Error');
exit();
}
-function common_user_error($msg) {
+# Show a user error
+function common_user_error($msg, $code=200) {
common_show_header('Error');
common_element('div', array('class' => 'error'), $msg);
common_show_footer();
}
+# Start an HTML element
function common_element_start($tag, $attrs=NULL) {
print "<$tag";
if (is_array($attrs)) {
function common_element($tag, $attrs=NULL, $content=NULL) {
common_element_start($tag, $attrs);
- if ($content) print $content;
+ if ($content) print htmlspecialchars($content);
common_element_end($tag);
}
global $config;
common_element_start('html');
common_element_start('head');
- common_element('title', NULL, $pagetitle . " - " . $config['site']['name']);
+ common_element('title', NULL,
+ $pagetitle . " - " . $config['site']['name']);
common_element_end('head');
common_element_start('body');
}
common_element_end('html');
}
-// TODO: set up gettext
+# salted, hashed passwords are stored in the DB
+
+function common_munge_password($id, $password) {
+ return md5($id . $password);
+}
+
+# check if a username exists and has matching password
+function common_check_user($nickname, $password) {
+ $user = User::staticGet('nickname', $nickname);
+ if (is_null($user)) {
+ return false;
+ } else {
+ return (0 == strcmp(common_munge_password($password, $user->id),
+ $user->password));
+ }
+}
+
+# is the current user logged in?
+function common_logged_in() {
+ return (!is_null(common_current_user()));
+}
+
+function common_have_session() {
+ return (0 != strcmp(session_id(), ''));
+}
+
+function common_ensure_session() {
+ if (!common_have_session()) {
+ @session_start();
+ }
+}
+
+function common_set_user($nickname) {
+ if (is_null($nickname) && common_have_session()) {
+ unset($_SESSION['userid']);
+ return true;
+ } else {
+ $user = User::staticGet('nickname', $nickname);
+ if ($user) {
+ common_ensure_session();
+ $_SESSION['userid'] = $user->id;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ return false;
+}
+
+# who is the current user?
+function common_current_user() {
+ static $user = NULL; # FIXME: global memcached
+ if (is_null($user)) {
+ if (common_have_session()) {
+ $id = $_SESSION['userid'];
+ if ($id) {
+ $user = User::staticGet($id);
+ }
+ }
+ }
+ return $user;
+}
+
+# get canonical version of nickname for comparison
+function common_canonical_nickname($nickname) {
+ # XXX: UTF-8 canonicalization (like combining chars)
+ return strtolower($nickname);
+}
+
+function common_render_content($text) {
+ # XXX: @ messages
+ # XXX: # tags
+ # XXX: machine tags
+ return htmlspecialchars($text);
+}
+
+// XXX: set up gettext
function _t($str) { $str }
if (file_exists($actionfile)) {
require_once($actionfile);
- $action_function = 'handle_' . $action;
+ $action_class = ucfirst($action) . "Action";
if (function_exists($action_function)) {
call_user_func($action_function);
} else {
create table user (
id integer primary key comment 'foreign key to profile table' references profile (id),
+ nickname varchar(64) unique key comment 'nickname or username, duped in profile',
password varchar(255) comment 'salted password, can be null for OpenID users',
email varchar(255) unique key comment 'email address for password recovery etc.',
created datetime not null comment 'date this record was created',
id integer auto_increment primary key comment 'unique identifier',
profile_id integer not null comment 'who made the update' references profile (id),
content varchar(140) comment 'update content',
- rendered varchar(140) comment 'pre-rendered content',
+ /* XXX: cache rendered content. */
url varchar(255) comment 'URL of any attachment (image, video, bookmark, whatever)',
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',