considerable coding

darcs-hash:20080514145436-84dde-d0994cb35d3fe8545d3f08abeec3cdfe7559c67d.gz
This commit is contained in:
Evan Prodromou 2008-05-14 10:54:36 -04:00
parent f0a30cc89d
commit 67a347bafb
15 changed files with 552 additions and 58 deletions

62
TODO Normal file
View File

@ -0,0 +1,62 @@
+ 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

View File

@ -11,6 +11,7 @@
/main/login login to site /main/login login to site
/main/register register to site /main/register register to site
/main/settings change account settings
/main/recover recover password /main/recover recover password
/doc/ documentation /doc/ documentation
about about this site about about this site

View File

@ -1,25 +1,59 @@
<?php <?php
function handle_login() { class LoginAction extends Action {
if ($_REQUEST['METHOD'] == 'POST') {
if (login_check_user($_REQUEST['user'], $_REQUEST['password'])) {
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 { } else {
} $this->show_form();
} else {
if (user_logged_in()) {
} else {
login_show_form();
} }
} }
}
function login_show_form() { function check_login() {
html_start(); # XXX: form token in $_SESSION to prevent XSS
html_head("Login"); # XXX: login throttle
html_body(); $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 {
$this->show_form(_t('Incorrect username or password.'));
}
}
function login_check_user($username, $password) { 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'));
}
} }

13
actions/logout.php Normal file
View File

@ -0,0 +1,13 @@
<?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'));
}
}
}

48
actions/newnotice.php Normal file
View File

@ -0,0 +1,48 @@
<?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');
}
}

115
actions/register.php Normal file
View File

@ -0,0 +1,115 @@
<?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'));
}
}

34
actions/settings.php Normal file
View File

@ -0,0 +1,34 @@
<?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();
}

View File

@ -37,7 +37,8 @@ class ShownoticeAction extends Action {
'class' => 'nickname'), 'class' => 'nickname'),
$profile->nickname); $profile->nickname);
# FIXME: URL, image, video, audio # 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_element('span', array('class' => 'date'),
common_date_string($notice->created)); common_date_string($notice->created));
common_end_element('div'); common_end_element('div');

View File

@ -9,25 +9,26 @@ class ShowstreamAction extends StreamAction {
parent::handle($args); parent::handle($args);
$nickname = $this->arg('profile'); $nickname = common_canonicalize_nickname($this->arg('profile'));
$profile = Profile::staticGet('nickname', strtolower($nickname)); $user = User::staticGet('nickname', $nickname);
if (!$profile) { if (!$user) {
$this->no_such_user(); $this->no_such_user();
} }
$user = User::staticGet($profile->id); $profile = $user->getProfile();
if (!$user) { if (!$profile) {
// remote profile common_server_error(_t('User record exists without profile.'));
$this->no_such_user();
} }
# Looks like we're good; show the header # Looks like we're good; show the header
common_show_header($profile->nickname); common_show_header($profile->nickname);
if ($profile->id == current_user()->id) { $cur = common_current_user();
if ($cur && $profile->id == $cur->id) {
$this->notice_form(); $this->notice_form();
} }
@ -35,6 +36,14 @@ class ShowstreamAction extends StreamAction {
$this->show_last_notice($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_statistics($profile);
$this->show_subscriptions($profile); $this->show_subscriptions($profile);
@ -76,12 +85,32 @@ class ShowstreamAction extends StreamAction {
} }
} }
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) { function show_subscriptions($profile) {
# XXX: add a limit # XXX: add a limit
$subs = $profile->getLink('id', 'subscription', 'subscriber'); $subs = $profile->getLink('id', 'subscription', 'subscriber');
common_start_element('div', 'subscriptions'); common_start_element('div', 'subscriptions');
$cnt = 0; $cnt = 0;
@ -174,7 +203,8 @@ class ShowstreamAction extends StreamAction {
while ($notice->fetch()) { while ($notice->fetch()) {
# FIXME: URL, image, video, audio # 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_element('span', array('class' => 'date'),
common_date_string($notice->created)); common_date_string($notice->created));
} }

42
actions/subscribe.php Normal file
View File

@ -0,0 +1,42 @@
<?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)));
}
}

35
actions/unsubscribe.php Normal file
View File

@ -0,0 +1,35 @@
<?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)));
}
}

View File

@ -21,4 +21,16 @@ class User extends DB_DataObject
/* the code above is auto generated do not remove the tag below */ /* the code above is auto generated do not remove the tag below */
###END_AUTOCODE ###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();
}
} }

View File

@ -20,20 +20,7 @@ $config =
require_once(INSTALLDIR . '/config.php'); require_once(INSTALLDIR . '/config.php');
require_once('DB.php'); require_once('DB.php');
function common_database() { # Show a server error
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();
}
function common_server_error($msg) { function common_server_error($msg) {
header('Status: 500 Server Error'); header('Status: 500 Server Error');
@ -43,12 +30,14 @@ function common_server_error($msg) {
exit(); exit();
} }
function common_user_error($msg) { # Show a user error
function common_user_error($msg, $code=200) {
common_show_header('Error'); common_show_header('Error');
common_element('div', array('class' => 'error'), $msg); common_element('div', array('class' => 'error'), $msg);
common_show_footer(); common_show_footer();
} }
# Start an HTML element
function common_element_start($tag, $attrs=NULL) { function common_element_start($tag, $attrs=NULL) {
print "<$tag"; print "<$tag";
if (is_array($attrs)) { if (is_array($attrs)) {
@ -67,7 +56,7 @@ function common_element_end($tag) {
function common_element($tag, $attrs=NULL, $content=NULL) { function common_element($tag, $attrs=NULL, $content=NULL) {
common_element_start($tag, $attrs); common_element_start($tag, $attrs);
if ($content) print $content; if ($content) print htmlspecialchars($content);
common_element_end($tag); common_element_end($tag);
} }
@ -75,7 +64,8 @@ function common_show_header($pagetitle) {
global $config; global $config;
common_element_start('html'); common_element_start('html');
common_element_start('head'); 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_end('head');
common_element_start('body'); common_element_start('body');
} }
@ -85,6 +75,82 @@ function common_show_footer() {
common_element_end('html'); 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 } function _t($str) { $str }

View File

@ -9,7 +9,7 @@ $actionfile = INSTALLDIR."/actions/$action.php";
if (file_exists($actionfile)) { if (file_exists($actionfile)) {
require_once($actionfile); require_once($actionfile);
$action_function = 'handle_' . $action; $action_class = ucfirst($action) . "Action";
if (function_exists($action_function)) { if (function_exists($action_function)) {
call_user_func($action_function); call_user_func($action_function);
} else { } else {

View File

@ -18,6 +18,7 @@ create table profile (
create table user ( create table user (
id integer primary key comment 'foreign key to profile table' references profile (id), 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', password varchar(255) comment 'salted password, can be null for OpenID users',
email varchar(255) unique key comment 'email address for password recovery etc.', email varchar(255) unique key comment 'email address for password recovery etc.',
created datetime not null comment 'date this record was created', created datetime not null comment 'date this record was created',
@ -49,7 +50,7 @@ create table notice (
id integer auto_increment primary key comment 'unique identifier', id integer auto_increment primary key comment 'unique identifier',
profile_id integer not null comment 'who made the update' references profile (id), profile_id integer not null comment 'who made the update' references profile (id),
content varchar(140) comment 'update content', 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)', url varchar(255) comment 'URL of any attachment (image, video, bookmark, whatever)',
created datetime not null comment 'date this record was created', created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified', modified timestamp comment 'date this record was modified',