summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO62
-rw-r--r--URLS.txt1
-rw-r--r--actions/login.php66
-rw-r--r--actions/logout.php13
-rw-r--r--actions/newnotice.php48
-rw-r--r--actions/register.php115
-rw-r--r--actions/settings.php34
-rw-r--r--actions/shownotice.php3
-rw-r--r--actions/showstream.php64
-rw-r--r--actions/subscribe.php42
-rw-r--r--actions/unsubscribe.php35
-rw-r--r--classes/User.php12
-rw-r--r--common.php104
-rw-r--r--index.php2
-rw-r--r--stoica.sql3
15 files changed, 549 insertions, 55 deletions
diff --git a/TODO b/TODO
new file mode 100644
index 000000000..08ac0a5eb
--- /dev/null
+++ b/TODO
@@ -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
diff --git a/URLS.txt b/URLS.txt
index b544bd036..bcbad40da 100644
--- a/URLS.txt
+++ b/URLS.txt
@@ -11,6 +11,7 @@
/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
diff --git a/actions/login.php b/actions/login.php
index a95dc9e3a..b93936297 100644
--- a/actions/login.php
+++ b/actions/login.php
@@ -1,25 +1,59 @@
<?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
diff --git a/actions/logout.php b/actions/logout.php
new file mode 100644
index 000000000..a40400e7e
--- /dev/null
+++ b/actions/logout.php
@@ -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'));
+ }
+ }
+}
diff --git a/actions/newnotice.php b/actions/newnotice.php
new file mode 100644
index 000000000..bbfa3285d
--- /dev/null
+++ b/actions/newnotice.php
@@ -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');
+ }
+} \ No newline at end of file
diff --git a/actions/register.php b/actions/register.php
new file mode 100644
index 000000000..5972d5838
--- /dev/null
+++ b/actions/register.php
@@ -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'));
+ }
+}
diff --git a/actions/settings.php b/actions/settings.php
new file mode 100644
index 000000000..826770ad7
--- /dev/null
+++ b/actions/settings.php
@@ -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();
+ }
+ \ No newline at end of file
diff --git a/actions/shownotice.php b/actions/shownotice.php
index 4d4876122..b3204d063 100644
--- a/actions/shownotice.php
+++ b/actions/shownotice.php
@@ -37,7 +37,8 @@ class ShownoticeAction extends Action {
'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');
diff --git a/actions/showstream.php b/actions/showstream.php
index 1eb060fdc..5950a4ead 100644
--- a/actions/showstream.php
+++ b/actions/showstream.php
@@ -9,34 +9,43 @@ class ShowstreamAction extends StreamAction {
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);
@@ -75,13 +84,33 @@ class ShowstreamAction extends StreamAction {
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;
@@ -113,7 +142,7 @@ class ShowstreamAction extends StreamAction {
array('profile' => $profile->nickname))
'class' => 'moresubscriptions'),
_t('All subscriptions'));
-
+
common_end_element('div');
}
@@ -174,7 +203,8 @@ class ShowstreamAction extends StreamAction {
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));
}
diff --git a/actions/subscribe.php b/actions/subscribe.php
new file mode 100644
index 000000000..35961d051
--- /dev/null
+++ b/actions/subscribe.php
@@ -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)));
+ }
+} \ No newline at end of file
diff --git a/actions/unsubscribe.php b/actions/unsubscribe.php
new file mode 100644
index 000000000..c4e6b9891
--- /dev/null
+++ b/actions/unsubscribe.php
@@ -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)));
+ }
+}
diff --git a/classes/User.php b/classes/User.php
index 4ed6003dc..8234e0784 100644
--- a/classes/User.php
+++ b/classes/User.php
@@ -21,4 +21,16 @@ class User extends DB_DataObject
/* 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();
+ }
}
diff --git a/common.php b/common.php
index a6061920d..de0529a2e 100644
--- a/common.php
+++ b/common.php
@@ -5,7 +5,7 @@
// default configuration, overwritten in config.php
$config =
- array('site' =>
+ array('site' =>
array('name' => 'Just another µB'),
'dsn' =>
array('phptype' => 'mysql',
@@ -20,20 +20,7 @@ $config =
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');
@@ -43,12 +30,14 @@ function common_server_error($msg) {
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)) {
@@ -67,7 +56,7 @@ function common_element_end($tag) {
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);
}
@@ -75,7 +64,8 @@ function common_show_header($pagetitle) {
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');
}
@@ -85,6 +75,82 @@ function common_show_footer() {
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 }
diff --git a/index.php b/index.php
index d63d09edf..7237c08bc 100644
--- a/index.php
+++ b/index.php
@@ -9,7 +9,7 @@ $actionfile = INSTALLDIR."/actions/$action.php";
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 {
diff --git a/stoica.sql b/stoica.sql
index f7f32ad76..28e8f7662 100644
--- a/stoica.sql
+++ b/stoica.sql
@@ -18,6 +18,7 @@ create table profile (
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',
@@ -49,7 +50,7 @@ create table notice (
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',