', $/, + '', $msg, '
Powered by Chirpy!
', $/, + '', $/, + ''; + exit; + }); +} + +use Chirpy 0.3; + +chirpy('./chirpy.ini'); + +############################################################################### diff --git a/pub/qdb/install.txt b/pub/qdb/install.txt new file mode 100644 index 0000000..99d7736 --- /dev/null +++ b/pub/qdb/install.txt @@ -0,0 +1,533 @@ +############################################################################### +# Chirpy! 0.3, a quote management system # +# Copyright (C) 2005-2007 Tim De Pauw, I and I.
+
+=back
+
+=head2 Sessions
+
+I
+
+In addition, you may want to provide compatibility with
+L's session manager by making your data
+manager class extend L as well.
+Please check L for
+instructions.
+
+=head1 AUTHOR
+
+Tim De Pauw Eceetee@users.sourceforge.netE
+
+=head1 SEE ALSO
+
+L, L, L, L,
+L, L,
+L, L
+
+=head1 COPYRIGHT
+
+Copyright 2005-2007 Tim De Pauw. All rights reserved.
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; either version 2 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+=cut
+
+package Chirpy::DataManager;
+
+use strict;
+use warnings;
+
+use vars qw($VERSION);
+
+$VERSION = '0.3';
+
+use Chirpy 0.3;
+use Chirpy::Util 0.3;
+
+sub new {
+ my ($class, $params) = @_;
+ return bless {
+ 'params' => $params
+ }, $class;
+}
+
+sub param {
+ my ($self, $name) = @_;
+ return defined $self->{'params'} ? $self->{'params'}{$name} : undef;
+}
+
+*get_target_version = \&Chirpy::Util::abstract_method;
+
+*set_up = \&Chirpy::Util::abstract_method;
+
+*remove = \&Chirpy::Util::abstract_method;
+
+*get_quote = \&Chirpy::Util::abstract_method;
+
+*quote_count = \&Chirpy::Util::abstract_method;
+
+*get_quotes = \&Chirpy::Util::abstract_method;
+
+*add_quote = \&Chirpy::Util::abstract_method;
+
+*modify_quote = \&Chirpy::Util::abstract_method;
+
+*increase_quote_rating = \&Chirpy::Util::abstract_method;
+
+*decrease_quote_rating = \&Chirpy::Util::abstract_method;
+
+*get_tag_use_counts = \&Chirpy::Util::abstract_method;
+
+*approve_quotes = \&Chirpy::Util::abstract_method;
+
+*flag_quotes = \&Chirpy::Util::abstract_method;
+
+*unflag_quotes = \&Chirpy::Util::abstract_method;
+
+*remove_quote = \&Chirpy::Util::abstract_method;
+
+*remove_quotes = \&Chirpy::Util::abstract_method;
+
+*get_news_items = \&Chirpy::Util::abstract_method;
+
+*add_news_item = \&Chirpy::Util::abstract_method;
+
+*modify_news_item = \&Chirpy::Util::abstract_method;
+
+*remove_news_item = \&Chirpy::Util::abstract_method;
+
+*remove_news_items = \&Chirpy::Util::abstract_method;
+
+*get_accounts = \&Chirpy::Util::abstract_method;
+
+*add_account = \&Chirpy::Util::abstract_method;
+
+*modify_account = \&Chirpy::Util::abstract_method;
+
+*remove_account = \&Chirpy::Util::abstract_method;
+
+*remove_accounts = \&Chirpy::Util::abstract_method;
+
+*username_exists = \&Chirpy::Util::abstract_method;
+
+*account_count = \&Chirpy::Util::abstract_method;
+
+*get_events = \&Chirpy::Util::abstract_method;
+
+*log_event = \&Chirpy::Util::abstract_method;
+
+*get_parameter = \&Chirpy::Util::abstract_method;
+
+*set_parameter = \&Chirpy::Util::abstract_method;
+
+1;
+
+###############################################################################
\ No newline at end of file
diff --git a/pub/qdb/src/modules/Chirpy/DataManager/MySQL.pm b/pub/qdb/src/modules/Chirpy/DataManager/MySQL.pm
new file mode 100644
index 0000000..5270e33
--- /dev/null
+++ b/pub/qdb/src/modules/Chirpy/DataManager/MySQL.pm
@@ -0,0 +1,1256 @@
+###############################################################################
+# Chirpy! 0.3, a quote management system #
+# Copyright (C) 2005-2007 Tim De Pauw #
+###############################################################################
+# This program is free software; you can redistribute it and/or modify it #
+# under the terms of the GNU General Public License as published by the Free #
+# Software Foundation; either version 2 of the License, or (at your option) #
+# any later version. #
+# #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
+# more details. #
+# #
+# You should have received a copy of the GNU General Public License along #
+# with this program; if not, write to the Free Software Foundation, Inc., 51 #
+# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #
+###############################################################################
+
+###############################################################################
+# $Id:: MySQL.pm 304 2007-02-09 01:06:15Z ceetee $ #
+###############################################################################
+
+=head1 NAME
+
+Chirpy::DataManager::MySQL - Data manager class, based on a MySQL backend
+
+=head1 REQUIREMENTS
+
+Apart from a proper Chirpy! installation, this module requires the following
+Perl modules:
+
+ Data::Dumper
+ DBD::mysql
+ DBI
+
+In addition, it needs access to a server running MySQL version 4.1 or higher.
+
+=head1 CONFIGURATION
+
+This module uses the following values from your configuration file:
+
+=over 4
+
+=item mysql.hostname
+
+The name of the server to use. In most cases, this will be C.
+
+=item mysql.port
+
+The port on which the MySQL server accepts connections. The default is C<3306>.
+
+=item mysql.username
+
+The username to use for MySQL connections.
+
+=item mysql.password
+
+The password to use for MySQL connections. Note that this password is readable
+if the user gains access to the file, and while Chirpy! takes precautions to
+make the configuration file inaccessible, it is also your responsibility to
+make sure that no one finds your password.
+
+=item mysql.database
+
+The name of the MySQL database where Chirpy! will store its data.
+
+=item mysql.prefix
+
+If your host only allows one database, Chirpy! can prefix its table names with
+a custom prefix, so you can easily distinguish them. For instance, if you set
+this value to C, the tables will be called C,
+C, etc.
+
+=back
+
+=head1 SESSIONS
+
+This class fully implements L, which
+allows L to use sessions.
+
+=head1 AUTHOR
+
+Tim De Pauw Eceetee@users.sourceforge.netE
+
+=head1 SEE ALSO
+
+L, L, L,
+L
+
+=head1 COPYRIGHT
+
+Copyright 2005-2007 Tim De Pauw. All rights reserved.
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; either version 2 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+=cut
+
+package Chirpy::DataManager::MySQL;
+
+use strict;
+use warnings;
+
+use vars qw($VERSION $TARGET_VERSION @ISA $SCORE_EXPR);
+
+$VERSION = '0.3';
+@ISA = qw(Chirpy::DataManager Chirpy::UI::WebApp::Session::DataManager);
+
+$TARGET_VERSION = '0.3';
+
+$SCORE_EXPR = '`score` = ((`votes` + `rating`) / 2 + 1)'
+ . ' / ((`votes` - `rating`) / 2 + 1)';
+
+use Chirpy 0.3;
+
+use Chirpy::DataManager 0.3;
+use Chirpy::UI::WebApp::Session::DataManager 0.3;
+
+use Chirpy::Quote 0.3;
+use Chirpy::Account 0.3;
+use Chirpy::NewsItem 0.3;
+use Chirpy::Event 0.3;
+
+use DBI;
+
+use Data::Dumper;
+
+sub new {
+ my $class = shift;
+ my $self = $class->SUPER::new(@_);
+ my $dbh = DBI->connect('DBI:mysql:database=' . $self->param('database')
+ . ($self->param('hostname')
+ ? ';host=' . $self->param('hostname')
+ : '')
+ . ($self->param('port') ? ';port=' . $self->param('port') : ''),
+ $self->param('username'), $self->param('password'));
+ Chirpy::die('Failed to connect to database: ' . DBI->errstr())
+ unless (defined $dbh);
+ $dbh->do('SET NAMES utf8');
+ $self->{'dbh'} = $dbh;
+ $self->{'prefix'} = $self->param('prefix');
+ return $self;
+}
+
+sub get_target_version {
+ return $TARGET_VERSION;
+}
+
+sub DESTROY {
+ my $self = shift;
+ my $handle = $self->handle();
+ return unless (defined $handle);
+ $handle->disconnect();
+}
+
+sub set_up {
+ my ($self, $accounts, $news, $quotes) = @_;
+ my $prefix = $self->table_name_prefix();
+ my $handle = $self->handle();
+ my $table = $prefix . 'accounts';
+ unless ($self->_table_exists($table)) {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `id` int unsigned NOT NULL auto_increment,
+ `username` varchar(32) NOT NULL,
+ `password` varchar(32) NOT NULL,
+ `level` tinyint(1) unsigned NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `username` (`username`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ $table = $prefix . 'news';
+ unless ($self->_table_exists($table)) {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `id` int unsigned NOT NULL auto_increment,
+ `body` text NOT NULL,
+ `poster` int unsigned default NULL,
+ `date` timestamp NOT NULL default CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ $table = $prefix . 'quotes';
+ my $determine_votes = 0;
+ my $determine_scores = 0;
+ if ($self->_table_exists($table)) {
+ unless ($self->_column_exists($table, 'votes')) {
+ $handle->do(q|
+ ALTER TABLE `| . $table . q|`
+ ADD `votes` int unsigned NOT NULL default 0 AFTER `rating`
+ |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr());
+ $determine_votes = 1;
+ }
+ unless ($self->_column_exists($table, 'score')) {
+ $handle->do(q|
+ ALTER TABLE `| . $table . q|`
+ ADD `score` double unsigned NOT NULL default 1
+ |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr());
+ $determine_scores = 1;
+ }
+ }
+ else {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `id` int unsigned NOT NULL auto_increment,
+ `body` text NOT NULL,
+ `notes` text,
+ `rating` int NOT NULL default 0,
+ `votes` int unsigned NOT NULL default 0,
+ `submitted` timestamp NOT NULL default CURRENT_TIMESTAMP,
+ `approved` tinyint(1) unsigned NOT NULL default 0,
+ `flagged` tinyint(1) unsigned NOT NULL default 0,
+ `score` double unsigned NOT NULL default 1,
+ PRIMARY KEY (`id`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ $table = $prefix . 'tags';
+ unless ($self->_table_exists($table)) {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `id` int unsigned NOT NULL auto_increment,
+ `tag` varchar(255) NOT NULL,
+ PRIMARY KEY (`id`),
+ INDEX (`tag`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ $table = $prefix . 'quote_tag';
+ unless ($self->_table_exists($table)) {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `quote_id` int unsigned NOT NULL,
+ `tag_id` int unsigned NOT NULL,
+ INDEX (`quote_id`),
+ INDEX (`tag_id`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ $table = $prefix . 'events';
+ unless ($self->_table_exists($table)) {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `id` int unsigned NOT NULL auto_increment,
+ `date` timestamp NOT NULL default CURRENT_TIMESTAMP,
+ `code` int unsigned NOT NULL,
+ `user` int unsigned,
+ PRIMARY KEY (`id`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ $table = $prefix . 'event_metadata';
+ unless ($self->_table_exists($table)) {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `id` int unsigned NOT NULL,
+ `name` varchar(32) NOT NULL,
+ `value` text NOT NULL,
+ INDEX (`id`),
+ INDEX (`name`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ if ($self->_table_exists($prefix . 'log')) {
+ $self->_migrate_log();
+ $self->_remove_table($prefix . 'log');
+ }
+ $self->_determine_votes() if ($determine_votes);
+ $self->_determine_scores() if ($determine_scores);
+ $table = $prefix . 'sessions';
+ if ($self->_table_exists($table)) {
+ unless ($self->_column_exists($table, 'expires')) {
+ # XXX: Maybe unserialize session data to get the right expiry time.
+ # It's not really worth the effort though.
+ my $exp = time() + 3 * 24 * 60 * 60;
+ $handle->do(q|
+ ALTER TABLE `| . $table . q|`
+ ADD `expires` int unsigned NOT NULL AFTER `id`
+ |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr());
+ $handle->do(q|
+ ALTER TABLE `| . $table . q|` ADD INDEX (`expires`)
+ |) or Chirpy::die('Cannot alter ' . $table . ': ' . DBI->errstr());
+ $handle->do(q|
+ UPDATE `| . $table . q|`SET `expires` = | . $exp
+ ) or Chirpy::die('Cannot update ' . $table . ': ' . DBI->errstr());
+ }
+ }
+ else {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `id` varchar(32) NOT NULL,
+ `expires` int unsigned NOT NULL,
+ `data` text NOT NULL,
+ UNIQUE KEY `id` (`id`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ $table = $prefix . 'vars';
+ unless ($self->_table_exists($table)) {
+ $handle->do(q|
+ CREATE TABLE `| . $table . q|` (
+ `name` varchar(32) NOT NULL,
+ `value` varchar(255) NOT NULL,
+ PRIMARY KEY (`name`)
+ ) TYPE=MyISAM DEFAULT CHARSET=utf8
+ |) or Chirpy::die('Cannot create ' . $table . ': ' . DBI->errstr());
+ }
+ if (defined $accounts) {
+ foreach my $account (@$accounts) {
+ $self->add_account($account);
+ }
+ }
+ if (defined $news) {
+ foreach my $item (@$news) {
+ $self->add_news_item($item);
+ }
+ }
+ if (defined $quotes) {
+ foreach my $quote (@$quotes) {
+ $self->add_quote($quote);
+ }
+ }
+}
+
+sub remove {
+ my ($self, $accounts, $news, $quotes) = @_;
+ my @tables = qw/accounts news quotes tags quote_tag log sessions vars/;
+ foreach my $table (@tables) {
+ $self->_remove_table($self->table_name_prefix() . $table);
+ }
+}
+
+sub _migrate_log {
+ my $self = shift;
+ my $prefix = $self->table_name_prefix();
+ my $old_table = $prefix . 'log';
+ my $event_table = $prefix . 'events';
+ my $metadata_table = $prefix . 'event_metadata';
+ my $query = 'SELECT * FROM `' . $old_table . '`';
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute();
+ $self->_db_error() unless (defined $rows);
+ while (my $row = $sth->fetchrow_hashref()) {
+ my $date = $row->{'date'};
+ my $code = $row->{'code'};
+ my $user = $row->{'user'};
+ my $data = $row->{'data'};
+ eval '$data = ' . $data;
+ if ($data && ref $data eq 'HASH') {
+ $self->_do('INSERT INTO `' . $event_table . '`'
+ . ' (`date`, `code`, `user`) VALUES (?, ?, ?)',
+ $date, $code, $user);
+ my $id = $self->handle()->{'mysql_insertid'};
+ my $params = (exists $data->{'parameters'}
+ ? $data->{'parameters'} : {});
+ if (exists $data->{'user'}) {
+ while (my ($name, $value) = each %{$data->{'user'}}) {
+ $params->{'user:' . $name} = $value;
+ }
+ }
+ while (my ($name, $value) = each %$params) {
+ if (ref $value) {
+ if ($name eq 'old_tags' || $name eq 'new_tags') {
+ # Tags were stored as an arrayref.
+ $value = join(' ', @$value);
+ }
+ elsif ($name eq 'poster' || $name eq 'old_poster') {
+ # Old versions used the object instead of its ID.
+ $value = $value->{'id'};
+ }
+ else {
+ # This should never happen.
+ $value = &_serialize($value);
+ }
+ }
+ elsif ($name eq 'quote' && $value eq 'id') {
+ # Bug 1493589. The quote ID is lost, so we use 0.
+ $name = 'id';
+ $value = 0;
+ }
+ next unless (defined $value && $value ne '');
+ my $query = 'INSERT INTO `' . $metadata_table . '`'
+ . ' (`id`, `name`, `value`) VALUES (?, ?, ?)';
+ my @params = ($id, $name, $value);
+ # XXX: This causes Unicode breakage sometimes.
+ #$self->_do($query, @params);
+ my $success = $self->handle()->do($query, undef, @params);
+ unless ($success) {
+ $params[2] = 'ERROR';
+ $self->_do($query, @params);
+ }
+ }
+ }
+ else {
+ # TODO: Failed to unserialize--report.
+ }
+ }
+}
+
+sub _determine_votes {
+ my $self = shift;
+ my $prefix = $self->table_name_prefix();
+ my $query = 'SELECT `value`, COUNT(*)'
+ . ' FROM `' . $prefix . 'events` AS ev'
+ . ' LEFT JOIN `' . $prefix . 'event_metadata` AS md'
+ . ' ON ev.id = md.id'
+ . ' WHERE `code` = ' . Chirpy::Event::QUOTE_RATING_DOWN
+ . ' AND `name` = "id"'
+ . ' GROUP BY `value`';
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute();
+ $self->_db_error() unless (defined $rows);
+ my %neg = ();
+ while (my $row = $sth->fetchrow_arrayref()) {
+ my ($id, $votes) = @$row;
+ $neg{$id} = $votes;
+ }
+ $query = 'SELECT `id`, `rating` FROM `' . $prefix . 'quotes`';
+ $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ $rows = $sth->execute();
+ $self->_db_error() unless (defined $rows);
+ $query = 'UPDATE `' . $prefix . 'quotes`'
+ . ' SET `votes` = ? WHERE `id` = ? LIMIT 1';
+ my $s = $self->handle()->prepare($query);
+ while (my $row = $sth->fetchrow_hashref()) {
+ my ($id, $rating) = ($row->{'id'}, $row->{'rating'});
+ my $votes = $rating;
+ $votes += $neg{$id} * 2 if (exists $neg{$id} && $neg{$id});
+ # This is only meaningful if the log is inconsistent.
+ $votes = abs($rating) if ($votes < 0);
+ $rows = $s->execute($votes, $id);
+ $self->_db_error() unless (defined $rows);
+ }
+}
+
+sub _determine_scores {
+ my $self = shift;
+ my $prefix = $self->table_name_prefix();
+ my $query = 'UPDATE `' . $prefix . 'quotes`'
+ . ' SET ' . $SCORE_EXPR;
+ $self->_do($query);
+}
+
+sub _remove_table {
+ my ($self, $table) = @_;
+ return unless ($self->_table_exists($table));
+ defined $self->_do('DROP TABLE `' . $table . '`')
+ or Chirpy::die('Cannot remove "' . $table . '": ' . DBI->errstr());
+}
+
+sub _table_exists {
+ my ($self, $table) = @_;
+ my $query = "SHOW TABLES LIKE '$table'";
+ return defined $self->_execute_scalar($query);
+}
+
+sub _column_exists {
+ my ($self, $table, $column) = @_;
+ my $query = "SHOW COLUMNS FROM $table";
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute();
+ $self->_db_error() unless (defined $rows);
+ while (my $row = $sth->fetchrow_hashref()) {
+ return 1 if ($row->{'Field'} eq $column);
+ }
+ return 0;
+}
+
+# TODO: Implement a ResultSet interface in order to make this more lightweight
+sub get_quotes {
+ my ($self, $params) = @_;
+ $params = {} unless (ref $params eq 'HASH');
+ my $query = 'SELECT DISTINCT `q`.`id` AS `id`, `body`, `notes`,'
+ . ' `rating`, `votes`,'
+ . ' UNIX_TIMESTAMP(`submitted`) AS `submitted`, `approved`, `flagged`'
+ . ' FROM `' . $self->table_name_prefix() . 'quotes` AS `q`';
+ my @par = ();
+ my @cond = ();
+ if (defined $params->{'flagged'}) {
+ push @cond, '`flagged` '
+ . ($params->{'flagged'} ? '<>' : '=') . ' 0';
+ }
+ if (defined $params->{'approved'}) {
+ push @cond, '`approved` '
+ . ($params->{'approved'} ? '<>' : '=') . ' 0';
+ }
+ if (defined $params->{'contains'} && @{$params->{'contains'}}) {
+ foreach my $q (@{$params->{'contains'}}) {
+ (my $query = $q) =~ s/(?{'tags'} && @{$params->{'tags'}}) {
+ $query .= ' JOIN `' . $self->table_name_prefix() . 'quote_tag` AS `qt`'
+ . ' ON `q`.`id` = `qt`.`quote_id`'
+ . ' JOIN `' . $self->table_name_prefix() . 'tags` AS `t`'
+ . ' ON `qt`.`tag_id` = `t`.`id`';
+ my @tags = @{$params->{'tags'}};
+ push @cond, '`tag` IN (?' . (',?' x (scalar(@tags) - 1)) . ')';
+ push @par, @tags;
+ }
+ if (defined $params->{'since'}) {
+ push @cond, '`submitted` > FROM_UNIXTIME(?)';
+ push @par, $params->{'since'};
+ }
+ if (defined $params->{'id'}) {
+ push @cond, '`id` = ?';
+ push @par, $params->{'id'};
+ delete $params->{'first'};
+ delete $params->{'sort'};
+ }
+ if (@cond) {
+ $query .= ' WHERE ' . join(' AND ', @cond);
+ }
+ if ($params->{'random'}) {
+ $query .= ' ORDER BY RAND()';
+ }
+ elsif (ref $params->{'sort'} eq 'ARRAY') {
+ $query .= ' ORDER BY ' . join(', ', map {
+ '`q`.`' . $_->[0] . '`' . ($_->[1] ? ' DESC' : '')
+ } @{$params->{'sort'}});
+ }
+ my $per_page;
+ my $leading;
+ if (defined $params->{'id'}) {
+ $query .= ' LIMIT 1';
+ }
+ elsif ($params->{'count'}) {
+ $query .= ' LIMIT ';
+ if ($params->{'first'}) {
+ $leading = 1;
+ $query .= int($params->{'first'}) . ',';
+ }
+ $query .= int($params->{'count'}) + 1;
+ $per_page = $params->{'count'};
+ }
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute(@par);
+ $self->_db_error() unless (defined $rows);
+ my $trailing;
+ my @result = ();
+ while (my $row = $sth->fetchrow_hashref()) {
+ if ($per_page && @result >= $per_page) {
+ $trailing = 1;
+ last;
+ }
+ push @result, new Chirpy::Quote(
+ $row->{'id'}, Chirpy::Util::decode_utf8($row->{'body'}),
+ Chirpy::Util::decode_utf8($row->{'notes'}),
+ $row->{'rating'}, $row->{'votes'}, $row->{'submitted'},
+ $row->{'approved'}, $row->{'flagged'},
+ $self->_quote_tags($row->{'id'})
+ );
+ }
+ my $result = (@result ? \@result : undef);
+ return ($result, $leading, $trailing) if (wantarray);
+ return $result;
+}
+
+sub quote_count {
+ my ($self, $params) = @_;
+ $params = {} unless (ref $params eq 'HASH');
+ my $appr = $params->{'approved'};
+ return $self->_execute_scalar('SELECT COUNT(*) FROM `'
+ . $self->table_name_prefix() . 'quotes`'
+ . (defined $appr ? ' WHERE `approved` = ' . ($appr ? '1' : '0') : ''));
+}
+
+sub add_quote {
+ my ($self, $quote) = @_;
+ $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'quotes`'
+ . ' (`body`, `notes`, `approved`)'
+ . ' VALUES (?, ?, ?)',
+ $quote->get_body(),
+ $quote->get_notes(),
+ $quote->is_approved() || 0);
+ my $id = $self->handle()->{'mysql_insertid'};
+ $quote->set_id($id);
+ $self->_tag($id, $quote->get_tags());
+ return 1;
+}
+
+sub modify_quote {
+ my ($self, $quote) = @_;
+ Chirpy::die('Not a Chirpy::Quote')
+ unless (ref $quote eq 'Chirpy::Quote');
+ my $result = $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'
+ . ' SET `body` = ?, `notes` = ? WHERE `id` = ?',
+ $quote->get_body(), $quote->get_notes(), $quote->get_id());
+ $self->_replace_tags($quote->get_id(), $quote->get_tags());
+ return $result;
+}
+
+sub increase_quote_rating {
+ my ($self, $id, $revert) = @_;
+ my ($r, $v) = ($revert
+ ? ('`rating` + 2', '`votes`') : ('`rating` + 1', '`votes` + 1'));
+ $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'
+ . ' SET `rating` = ' . $r . ', `votes` = ' . $v . ','
+ . ' ' . $SCORE_EXPR
+ . ' WHERE `id` = ' . $id . ' LIMIT 1')
+ or return undef;
+ return $self->_get_quote_rating_and_vote_count($id);
+}
+
+sub decrease_quote_rating {
+ my ($self, $id, $revert) = @_;
+ my ($r, $v) = ($revert
+ ? ('`rating` - 2', '`votes`') : ('`rating` - 1', '`votes` + 1'));
+ $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'
+ . ' SET `rating` = ' . $r . ', `votes` = ' . $v . ','
+ . ' ' . $SCORE_EXPR
+ . ' WHERE `id` = ' . $id . ' LIMIT 1')
+ or return undef;
+ return $self->_get_quote_rating_and_vote_count($id);
+}
+
+sub get_tag_use_counts {
+ my $self = shift;
+ my $query = 'SELECT `tag`, COUNT(`tag_id`) AS `cnt`'
+ . ' FROM `' . $self->table_name_prefix() . 'quote_tag` AS `qt`'
+ . ' JOIN `' . $self->table_name_prefix() . 'tags` AS `t`'
+ . ' ON `qt`.`tag_id` = `t`.`id`'
+ . ' JOIN `' . $self->table_name_prefix() . 'quotes` AS `q`'
+ . ' ON `qt`.`quote_id` = `q`.`id`'
+ . ' WHERE `approved` = 1'
+ . ' GROUP BY `tag`';
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute();
+ $self->_db_error() unless (defined $rows);
+ my %result = ();
+ while (my $row = $sth->fetchrow_hashref()) {
+ $result{$row->{'tag'}} = $row->{'cnt'};
+ }
+ return \%result;
+}
+
+sub approve_quotes {
+ my ($self, @ids) = @_;
+ return undef unless (@ids);
+ return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'
+ . ' SET `approved` = 1 WHERE `id` IN ('
+ . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids);
+}
+
+sub flag_quotes {
+ my ($self, @ids) = @_;
+ return undef unless (@ids);
+ return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'
+ . ' SET `flagged` = 1 WHERE `id` IN ('
+ . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids);
+}
+
+sub unflag_quotes {
+ my ($self, @ids) = @_;
+ return undef unless (@ids);
+ return $self->_do('UPDATE `' . $self->table_name_prefix() . 'quotes`'
+ . ' SET `flagged` = 0 WHERE `id` IN ('
+ . join(',', map { int } @ids) . ') LIMIT ' . scalar @ids);
+}
+
+sub remove_quote {
+ my ($self, $quote) = @_;
+ Chirpy::die('Not a Chirpy::Quote')
+ unless (ref $quote eq 'Chirpy::Quote');
+ my $id = quote->get_id();
+ $self->_untag_all($id);
+ return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quotes`'
+ . ' WHERE `id` = ' . $id
+ . ' LIMIT 1');
+}
+
+sub remove_quotes {
+ my ($self, @ids) = @_;
+ return undef unless (@ids);
+ $self->_untag_all(@ids);
+ return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quotes`'
+ . ' WHERE `id` IN (' . join(',', map { int } @ids) . ')'
+ . ' LIMIT ' . scalar @ids);
+}
+
+sub get_news_items {
+ my ($self, $params) = @_;
+ my $query = 'SELECT N.id, N.body, N.poster, '
+ . 'UNIX_TIMESTAMP(N.date) AS `date`, A.username '
+ . 'FROM `' . $self->table_name_prefix() . 'news` N'
+ . ' LEFT JOIN `' . $self->table_name_prefix() . 'accounts` A'
+ . ' ON N.poster = A.id';
+ my @par = ();
+ if ($params->{'id'}) {
+ $query .= ' WHERE N.id = ?';
+ push @par, $params->{'id'};
+ $params->{'count'} = 1;
+ }
+ $query .= ' ORDER BY `date` DESC';
+ if ($params->{'count'}) {
+ $query .= ' LIMIT ' . int $params->{'count'};
+ }
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute(@par);
+ $self->_db_error() unless (defined $rows);
+ my @result = ();
+ while (my $row = $sth->fetchrow_hashref()) {
+ my $item = new Chirpy::NewsItem(
+ $row->{'id'}, Chirpy::Util::decode_utf8($row->{'body'}),
+ ($row->{'username'}
+ ? new Chirpy::Account($row->{'poster'}, $row->{'username'})
+ : undef),
+ $row->{'date'}
+ );
+ push @result, $item;
+ }
+ return (@result ? \@result : undef);
+}
+
+sub add_news_item {
+ my ($self, $news) = @_;
+ Chirpy::die('Not a Chirpy::NewsItem')
+ unless (ref $news eq 'Chirpy::NewsItem');
+ my $poster = $news->get_poster();
+ $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'news`'
+ . ' (`body`, `poster`)'
+ . ' VALUES (?, ?)',
+ $news->get_body(), (defined $poster ? $poster->get_id() : undef),
+ $news->get_date());
+ my $id = $self->handle()->{'mysql_insertid'};
+ $news->set_id($id);
+ return 1;
+}
+
+sub modify_news_item {
+ my ($self, $item) = @_;
+ Chirpy::die('Not a Chirpy::NewsItem')
+ unless (ref $item eq 'Chirpy::NewsItem');
+ my $poster = $item->get_poster();
+ return $self->_do('UPDATE `' . $self->table_name_prefix() . 'news`'
+ . ' SET `body` = ?, `poster` = ? WHERE `id` = ? LIMIT 1',
+ $item->get_body(), (defined $poster ? $poster->get_id() : undef),
+ $item->get_id());
+}
+
+sub remove_news_item {
+ my ($self, $item) = @_;
+ Chirpy::die('Not a Chirpy::NewsItem')
+ unless (ref $item eq 'Chirpy::NewsItem');
+ return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'news`'
+ . ' WHERE `id` = ' . $item->get_id()
+ . ' LIMIT 1');
+}
+
+sub remove_news_items {
+ my ($self, @ids) = @_;
+ return undef unless (@ids);
+ return $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'news`'
+ . ' WHERE `id` IN (' . join(',', map { int } @ids) . ')'
+ . ' LIMIT ' . scalar @ids);
+}
+
+sub get_accounts {
+ my ($self, $params) = @_;
+ $params = {} unless (ref $params eq 'HASH');
+ my $query = 'SELECT * FROM `' . $self->table_name_prefix() . 'accounts`';
+ my @cond = ();
+ my @par = ();
+ if (ref $params->{'levels'} eq 'ARRAY') {
+ push @cond, '`level` IN (' . join (',', @{$params->{'levels'}}) . ')';
+ }
+ if ($params->{'id'}) {
+ push @cond, '`id` = ?';
+ push @par, $params->{'id'};
+ }
+ if ($params->{'username'}) {
+ push @cond, '`username` = ?';
+ push @par, $params->{'username'};
+ }
+ if (@cond) {
+ $query .= ' WHERE ' . join(' AND ', @cond);
+ }
+ $query .= ' ORDER BY `level` DESC, `username`';
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute(@par);
+ $self->_db_error() unless (defined $rows);
+ my @result = ();
+ while (my $row = $sth->fetchrow_hashref()) {
+ push @result, new Chirpy::Account(
+ $row->{'id'}, $row->{'username'}, $row->{'password'},
+ $row->{'level'}
+ );
+ }
+ return (@result ? \@result : undef);
+}
+
+sub add_account {
+ my ($self, $user) = @_;
+ $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'accounts`'
+ . ' (`username`, `password`, `level`) VALUES (?, ?, ?)',
+ $user->get_username(), $user->get_password(), $user->get_level());
+ my $id = $self->handle()->{'mysql_insertid'};
+ $user->set_id($id);
+ return 1;
+}
+
+sub modify_account {
+ my ($self, $account) = @_;
+ Chirpy::die('Not a Chirpy::Account')
+ unless (ref $account eq 'Chirpy::Account');
+ return $self->_do('UPDATE `' . $self->table_name_prefix() . 'accounts`'
+ . ' SET `username` = ?, `password` = ?, `level` = ?'
+ . ' WHERE `id` = ? LIMIT 1',
+ $account->get_username(), $account->get_password(),
+ $account->get_level(), $account->get_id());
+}
+
+sub remove_account {
+ my ($self, $account) = @_;
+ Chirpy::die('Not a Chirpy::Account')
+ unless (ref $account eq 'Chirpy::Account');
+ $self->_do('UPDATE `' . $self->table_name_prefix()
+ . 'news` SET `poster` = NULL WHERE `poster` = ' . $account->get_id()
+ . ' LIMIT 1');
+ return $self->_do('DELETE FROM `' . $self->table_name_prefix()
+ . 'accounts` WHERE `id` = ' . $account->get_id()
+ . ' LIMIT 1');
+}
+
+sub remove_accounts {
+ my ($self, @ids) = @_;
+ return undef unless (@ids);
+ my $ids = join(',', map { int } @ids);
+ my $num = scalar @ids;
+ $self->_do('UPDATE `' . $self->table_name_prefix()
+ . 'news` SET `poster` = NULL WHERE `poster` IN (' . $ids . ')'
+ . ' LIMIT ' . $num);
+ return $self->_do('DELETE FROM `' . $self->table_name_prefix()
+ . 'accounts` WHERE `id` IN (' . $ids . ')'
+ . ' LIMIT ' . $num);
+}
+
+sub username_exists {
+ my ($self, $username) = @_;
+ return $self->_do('SELECT `id` FROM `' . $self->table_name_prefix()
+ . 'accounts` WHERE `username` = ? LIMIT 1', $username) ? 1 : 0;
+}
+
+sub account_count {
+ my ($self, $params) = @_;
+ $params = {} unless (ref $params eq 'HASH');
+ return $self->_execute_scalar('SELECT COUNT(*) FROM `'
+ . $self->table_name_prefix() . 'accounts`'
+ . (defined $params->{'levels'}
+ ? ' WHERE `level` IN (' . join(',', @{$params->{'levels'}}) . ')'
+ : ''));
+}
+
+sub get_events {
+ my ($self, $params) = @_;
+ $params = {} unless (ref $params eq 'HASH');
+ my $query = 'SELECT DISTINCT `e`.`id` AS `id`,'
+ . ' UNIX_TIMESTAMP(`date`) AS `date`, `code`, `user`'
+ . ' FROM `' . $self->table_name_prefix() . 'events` AS `e`';
+ my @conditions = ();
+ my @param = ();
+ if (defined $params->{'data'} && %{$params->{'data'}}) {
+ $query .= ' JOIN `' . $self->table_name_prefix()
+ . 'event_metadata` AS `m`'
+ . ' ON `e`.`id` = `m`.`id`';
+ my @cond = ();
+ while (my ($key, $value) = each %{$params->{'data'}}) {
+ push @cond, '(`name` = ? AND `value` = ?)';
+ push @param, $key, $value;
+ }
+ push @conditions, '(' . join(' OR ', @cond) . ')';
+ }
+ if (my $code = $params->{'code'}) {
+ if (ref $code eq 'ARRAY') {
+ my $count = scalar @$code;
+ if ($count) {
+ push @conditions, '`code` IN (?' . (',?' x ($count - 1)) . ')';
+ push @param, @$code;
+ }
+ }
+ else {
+ push @conditions, '`code` = ?';
+ push @param, $code;
+ }
+ }
+ my $user = $params->{'user'};
+ if (defined $user) {
+ if (ref $user eq 'ARRAY') {
+ if (@$user) {
+ my @set = ();
+ my $guest = 0;
+ foreach my $u (@$user) {
+ if ($u) {
+ push @set, $u;
+ }
+ else {
+ $guest = 1;
+ }
+ }
+ my $cond;
+ if (@set) {
+ $cond = '`user` IN (?' . (',?' x (scalar(@set) - 1)) . ')';
+ push @param, @set;
+ }
+ if ($guest) {
+ $cond = (defined $cond
+ ? '(' . $cond . ' OR `user` IS NULL)'
+ : '`user` IS NULL');
+ }
+ push @conditions, $cond;
+ }
+ }
+ elsif ($user) {
+ push @conditions, '`user` = ?';
+ push @param, $user;
+ }
+ else {
+ push @conditions, '`user` IS NULL';
+ }
+ }
+ if (@conditions) {
+ $query .= ' WHERE ' . join(' AND ', @conditions);
+ }
+ $query .= ' ORDER BY `id` ' . ($params->{'reverse'} ? 'DESC' : 'ASC');
+ my $per_page;
+ my $leading;
+ if ($params->{'count'}) {
+ $query .= ' LIMIT ';
+ if ($params->{'first'}) {
+ $leading = 1;
+ $query .= int($params->{'first'}) . ',';
+ }
+ $query .= int($params->{'count'}) + 1;
+ $per_page = $params->{'count'};
+ }
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute(@param);
+ $self->_db_error() unless (defined $rows);
+ my $trailing;
+ my @result = ();
+ while (my $row = $sth->fetchrow_hashref()) {
+ if ($per_page && @result >= $per_page) {
+ $trailing = 1;
+ last;
+ }
+ my $id = $row->{'id'};
+ my $data = $self->_get_event_metadata($id);
+ push @result, new Chirpy::Event(
+ $id, $row->{'date'}, $row->{'code'}, $row->{'user'}, $data
+ );
+ }
+ my $result = (@result ? \@result : undef);
+ return ($result, $leading, $trailing) if (wantarray);
+ return $result;
+}
+
+sub _get_event_metadata {
+ my ($self, $id) = @_;
+ my $query = 'SELECT `name`, `value`'
+ . ' FROM `' . $self->table_name_prefix() . 'event_metadata`'
+ . ' WHERE `id` = ' . $id;
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute();
+ $self->_db_error() unless (defined $rows);
+ my %result = ();
+ while (my $row = $sth->fetchrow_hashref()) {
+ $result{$row->{'name'}} = $row->{'value'};
+ }
+ return \%result;
+}
+
+sub log_event {
+ my ($self, $event) = @_;
+ Chirpy::die('Not a Chirpy::Event')
+ unless (ref $event eq 'Chirpy::Event');
+ my $user = $event->get_user();
+ my $prefix = $self->table_name_prefix();
+ $self->_do('INSERT INTO `' . $prefix . 'events`'
+ . ' (`code`, `user`) VALUES (?, ?)',
+ $event->get_code(),
+ (defined $user ? $user->get_id() : undef));
+ my $id = $self->handle()->{'mysql_insertid'};
+ $event->set_id($id);
+ while (my ($name, $value) = each %{$event->get_data()}) {
+ $self->_do('INSERT INTO `' . $prefix . 'event_metadata`'
+ . ' (`id`, `name`, `value`) VALUES (?, ?, ?)',
+ $id, $name, $value);
+ }
+ return $id;
+}
+
+sub add_session {
+ my ($self, $id, $data) = @_;
+ my $string = &_serialize($data);
+ $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'sessions`'
+ . ' (`id`, `expires`, `data`) VALUES (?, ?, ?)', $id,
+ $data->{'_SESSION_ATIME'} + $data->{'_SESSION_ETIME'}, $string);
+ return 1;
+}
+
+sub get_sessions {
+ my ($self, @ids) = @_;
+ my $query = 'SELECT `id`, `data` FROM `' . $self->table_name_prefix()
+ . 'sessions`';
+ $query .= ' WHERE `id` IN (?' . (',?' x (scalar(@ids) - 1)) . ')'
+ . ' LIMIT ' . scalar(@ids) if (@ids);
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute(@ids);
+ $self->_db_error() unless (defined $rows);
+ my @results = ();
+ while (my $row = $sth->fetchrow_hashref()) {
+ push @results, &_unserialize($row->{'data'});
+ }
+ return @results;
+}
+
+sub modify_session {
+ my ($self, $id, $data) = @_;
+ my $string = &_serialize($data);
+ my $sql = 'UPDATE `' . $self->table_name_prefix()
+ . 'sessions` SET `expires` = ?, `data` = ? WHERE `id` = ? LIMIT 1';
+ $self->_do($sql,
+ $data->{'_SESSION_ATIME'} + $data->{'_SESSION_ETIME'}, $string, $id);
+}
+
+sub remove_sessions {
+ my ($self, @ids) = @_;
+ return unless (@ids);
+ $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'sessions`'
+ . ' WHERE `id` IN (?' . (',?' x (scalar(@ids) - 1)) . ')'
+ . ' LIMIT ' . scalar(@ids), @ids);
+}
+
+# Overrides Chirpy::UI::WebApp::Session::DataManager's default implementation
+sub remove_expired_sessions {
+ my $self = shift;
+ $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'sessions`'
+ . ' WHERE `expires` < ' . time());
+}
+
+sub get_parameter {
+ my ($self, $name) = @_;
+ return $self->_execute_scalar('SELECT `value` FROM `'
+ . $self->table_name_prefix() . 'vars`'
+ . ' WHERE `name` = ?', $name);
+}
+
+sub set_parameter {
+ my ($self, $name, $value) = @_;
+ if (!defined $value) {
+ $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'vars`'
+ . ' WHERE `name` = ? LIMIT 1', $name);
+ return;
+ }
+ my $res = $self->_do('UPDATE `' . $self->table_name_prefix() . 'vars`'
+ . ' SET `value` = ?'
+ . ' WHERE `name` = ? LIMIT 1',
+ $value, $name);
+ unless ($res) {
+ $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'vars`'
+ . ' (`name`, `value`) VALUES (?, ?)',
+ $name, $value);
+ }
+}
+
+sub handle {
+ my $self = shift;
+ return $self->{'dbh'};
+}
+
+sub table_name_prefix {
+ my $self = shift;
+ return $self->{'prefix'};
+}
+
+sub _quote_tags {
+ my ($self, $quote_id, $mode) = @_;
+ $mode = 0 unless (defined $mode);
+ my $cols = ($mode == 2 ? '`tag`, `id`' : ($mode == 1 ? '`id`' : '`tag`'));
+ my $query = 'SELECT ' .$cols
+ . ' FROM `' . $self->table_name_prefix() . 'tags` AS `t`'
+ . ' JOIN `' . $self->table_name_prefix() . 'quote_tag` AS `qt`'
+ . ' ON `t`.`id` = `qt`.`tag_id`'
+ . ' WHERE `quote_id` = ?';
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute($quote_id);
+ $self->_db_error() unless (defined $rows);
+ if ($mode == 2) {
+ my %result = ();
+ while (my $row = $sth->fetchrow_arrayref()) {
+ $result{$row->[0]} = $row->[1];
+ }
+ return \%result;
+ }
+ my @result = ();
+ while (my $row = $sth->fetchrow_arrayref()) {
+ push @result, $row->[0];
+ }
+ return \@result;
+}
+
+sub _tag {
+ my ($self, $quote_id, $tags) = @_;
+ return unless (@$tags);
+ foreach my $tag (@$tags) {
+ my $tag_id = $self->_tag_id($tag);
+ unless (defined $tag_id) {
+ $tag_id = $self->_create_tag($tag);
+ }
+ $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'quote_tag`'
+ . ' (`quote_id`, `tag_id`) VALUES (?, ?)', $quote_id, $tag_id);
+ }
+}
+
+sub _untag {
+ my ($self, $quote_id, $tag_ids) = @_;
+ my $cnt = scalar @$tag_ids;
+ return unless ($cnt);
+ $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quote_tag`'
+ . ' WHERE `quote_id` = ? AND `tag_id` IN (?' . (',?' x ($cnt - 1)) . ')'
+ . ' LIMIT ' . $cnt, $quote_id, @$tag_ids);
+}
+
+sub _untag_all {
+ my ($self, @quote_ids) = @_;
+ my $cnt = scalar @quote_ids;
+ return unless ($cnt);
+ $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'quote_tag`'
+ . ' WHERE `quote_id` IN (?' . (',?' x ($cnt - 1)) . ')', @quote_ids);
+ $self->_clean_up_tags();
+}
+
+sub _tag_id {
+ my ($self, $tag) = @_;
+ return $self->_execute_scalar('SELECT `id` FROM `'
+ . $self->table_name_prefix() . 'tags` WHERE `tag` = ? LIMIT 1', $tag);
+}
+
+sub _create_tag {
+ my ($self, $tag) = @_;
+ $self->_do('INSERT INTO `' . $self->table_name_prefix() . 'tags`'
+ . ' (`tag`) VALUES (?)', $tag);
+ return $self->handle()->{'mysql_insertid'};
+}
+
+sub _replace_tags {
+ my ($self, $quote_id, $new_tags) = @_;
+ my $old_tags = $self->_quote_tags($quote_id, 2);
+ my @add = ();
+ foreach my $tag (@$new_tags) {
+ if (exists $old_tags->{$tag}) {
+ delete $old_tags->{$tag};
+ }
+ else {
+ push @add, $tag;
+ }
+ }
+ my @remove = values %$old_tags;
+ $self->_untag($quote_id, \@remove);
+ $self->_clean_up_tags();
+ $self->_tag($quote_id, \@add);
+}
+
+sub _clean_up_tags {
+ my $self = shift;
+ $self->_do('DELETE FROM `' . $self->table_name_prefix() . 'tags`'
+ . ' WHERE `id` NOT IN ('
+ . 'SELECT `tag_id` FROM `' . $self->table_name_prefix() . 'quote_tag`'
+ . ')');
+}
+
+sub _get_quote_rating_and_vote_count {
+ my ($self, $id) = @_;
+ my $sth = $self->handle()->prepare('SELECT `rating`, `votes`'
+ . ' FROM `' . $self->table_name_prefix() . 'quotes`'
+ . ' WHERE `id` = ' . $id . ' LIMIT 1');
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute();
+ $self->_db_error() unless (defined $rows);
+ my @row = $sth->fetchrow_array();
+ return ($row[0], $row[1]);
+}
+
+sub _get_quote_vote_count {
+ my ($self, $id) = @_;
+ my $sth = $self->handle()->prepare('SELECT `votes`'
+ . ' FROM `' . $self->table_name_prefix() . 'quotes`'
+ . ' WHERE `id` = ' . $id . ' LIMIT 1');
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute();
+ $self->_db_error() unless (defined $rows);
+ my @row = $sth->fetchrow_array();
+ return $row[0];
+}
+
+sub _do {
+ my ($self, $query, @params) = @_;
+ my $rows = $self->handle()->do($query, undef, @params);
+ $self->_db_error() unless (defined $rows);
+ return ($rows eq '0E0' ? 0 : $rows);
+}
+
+sub _execute_scalar {
+ my ($self, $query, @params) = @_;
+ my $sth = $self->handle()->prepare($query);
+ $self->_db_error() unless (defined $sth);
+ my $rows = $sth->execute(@params);
+ $self->_db_error() unless (defined $rows);
+ my @row = $sth->fetchrow_array();
+ return (scalar(@row) ? $row[0] : undef);
+}
+
+sub _serialize {
+ my $dumper = new Data::Dumper(\@_)->Terse(1)->Indent(0);
+ return $dumper->Dump();
+}
+
+sub _unserialize {
+ my $string = shift;
+ return (defined $string ? eval $string : undef);
+}
+
+sub _db_error {
+ my $self = shift;
+ my $msg = $self->handle()->errstr();
+ Chirpy::die($msg);
+}
+
+1;
+
+###############################################################################
\ No newline at end of file
diff --git a/pub/qdb/src/modules/Chirpy/Event.pm b/pub/qdb/src/modules/Chirpy/Event.pm
new file mode 100644
index 0000000..14030ab
--- /dev/null
+++ b/pub/qdb/src/modules/Chirpy/Event.pm
@@ -0,0 +1,219 @@
+###############################################################################
+# Chirpy! 0.3, a quote management system #
+# Copyright (C) 2005-2007 Tim De Pauw #
+###############################################################################
+# This program is free software; you can redistribute it and/or modify it #
+# under the terms of the GNU General Public License as published by the Free #
+# Software Foundation; either version 2 of the License, or (at your option) #
+# any later version. #
+# #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
+# more details. #
+# #
+# You should have received a copy of the GNU General Public License along #
+# with this program; if not, write to the Free Software Foundation, Inc., 51 #
+# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #
+###############################################################################
+
+###############################################################################
+# $Id:: Event.pm 291 2007-02-05 21:24:46Z ceetee $ #
+###############################################################################
+
+=head1 NAME
+
+Chirpy::Event - Represents a log event
+
+=head1 SYNOPSIS
+
+ $event = new Chirpy::Event($id, $date, $code, $user, $data);
+
+ $id = $event->get_id();
+ $event->set_id($id);
+
+ $date = $event->get_date();
+ $event->set_date($date);
+
+ $code = $event->get_code();
+ $event->set_code($code);
+
+ $user = $event->get_user();
+ $event->set_user($user);
+
+ $data = $event->get_data();
+ $event->set_data($data);
+
+ $event_code = Chirpy::Event::translate_code($code);
+
+=head1 CONSTRAINTS
+
+=over 4
+
+=item ID
+
+The event ID must be a positive non-zero integer.
+
+=item Date
+
+The event date must be a UNIX timestamp.
+
+=item Code
+
+The event code must be one of the event code constants described below.
+
+=item User
+
+The user who was logged in at the time of the event must be an instance of
+L, if any.
+
+=item Data
+
+The event data must be a reference to a hash containing information about the
+event.
+
+=back
+
+=head1 EVENT CODE CONSTANTS
+
+ Chirpy::Event::LOGIN_SUCCESS
+ Chirpy::Event::LOGIN_FAILURE
+ Chirpy::Event::CHANGE_PASSWORD
+
+ Chirpy::Event::ADD_QUOTE
+ Chirpy::Event::EDIT_QUOTE
+ Chirpy::Event::REMOVE_QUOTE
+ Chirpy::Event::QUOTE_RATING_UP
+ Chirpy::Event::QUOTE_RATING_DOWN
+ Chirpy::Event::REPORT_QUOTE
+ Chirpy::Event::APPROVE_QUOTE
+ Chirpy::Event::UNFLAG_QUOTE
+
+ Chirpy::Event::ADD_NEWS
+ Chirpy::Event::EDIT_NEWS
+ Chirpy::Event::REMOVE_NEWS
+
+ Chirpy::Event::ADD_ACCOUNT
+ Chirpy::Event::EDIT_ACCOUNT
+ Chirpy::Event::REMOVE_ACCOUNT
+
+=head1 AUTHOR
+
+Tim De Pauw Eceetee@users.sourceforge.netE
+
+=head1 SEE ALSO
+
+L, L
+
+=head1 COPYRIGHT
+
+Copyright 2005-2007 Tim De Pauw. All rights reserved.
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; either version 2 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+=cut
+
+package Chirpy::Event;
+
+use strict;
+use warnings;
+
+use constant LOGIN_SUCCESS => 100;
+use constant LOGIN_FAILURE => 101;
+use constant CHANGE_PASSWORD => 102;
+
+use constant ADD_QUOTE => 200;
+use constant EDIT_QUOTE => 201;
+use constant REMOVE_QUOTE => 202;
+use constant QUOTE_RATING_UP => 203;
+use constant QUOTE_RATING_DOWN => 204;
+use constant REPORT_QUOTE => 205;
+use constant APPROVE_QUOTE => 206;
+use constant UNFLAG_QUOTE => 207;
+
+use constant ADD_NEWS => 300;
+use constant EDIT_NEWS => 301;
+use constant REMOVE_NEWS => 302;
+
+use constant ADD_ACCOUNT => 400;
+use constant EDIT_ACCOUNT => 401;
+use constant REMOVE_ACCOUNT => 402;
+
+use vars qw($VERSION $CODES);
+
+$VERSION = '0.3';
+
+use Chirpy 0.3;
+
+sub new {
+ my ($class, $id, $date, $code, $user, $data) = @_;
+ my $self = {
+ 'id' => $id,
+ 'date' => $date,
+ 'code' => $code,
+ 'user' => $user,
+ 'data' => $data
+ };
+ return bless($self, $class);
+}
+
+sub get_id {
+ my $self = shift;
+ return $self->{'id'};
+}
+
+sub set_id {
+ my $self = shift;
+ return ($self->{'id'} = shift);
+}
+
+sub get_date {
+ my $self = shift;
+ return $self->{'date'};
+}
+
+sub set_date {
+ my $self = shift;
+ return ($self->{'date'} = shift);
+}
+
+sub get_code {
+ my $self = shift;
+ return $self->{'code'};
+}
+
+sub set_code {
+ my $self = shift;
+ return ($self->{'code'} = shift);
+}
+
+sub get_user {
+ my $self = shift;
+ return $self->{'user'};
+}
+
+sub set_user {
+ my $self = shift;
+ return ($self->{'user'} = shift);
+}
+
+sub get_data {
+ my $self = shift;
+ return $self->{'data'};
+}
+
+sub set_data {
+ my $self = shift;
+ return ($self->{'data'} = shift);
+}
+
+1;
+
+###############################################################################
\ No newline at end of file
diff --git a/pub/qdb/src/modules/Chirpy/Locale.pm b/pub/qdb/src/modules/Chirpy/Locale.pm
new file mode 100644
index 0000000..938d7c3
--- /dev/null
+++ b/pub/qdb/src/modules/Chirpy/Locale.pm
@@ -0,0 +1,1159 @@
+###############################################################################
+# Chirpy! 0.3, a quote management system #
+# Copyright (C) 2005-2007 Tim De Pauw #
+###############################################################################
+# This program is free software; you can redistribute it and/or modify it #
+# under the terms of the GNU General Public License as published by the Free #
+# Software Foundation; either version 2 of the License, or (at your option) #
+# any later version. #
+# #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
+# more details. #
+# #
+# You should have received a copy of the GNU General Public License along #
+# with this program; if not, write to the Free Software Foundation, Inc., 51 #
+# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #
+###############################################################################
+
+###############################################################################
+# $Id:: Locale.pm 291 2007-02-05 21:24:46Z ceetee $ #
+###############################################################################
+
+=head1 NAME
+
+Chirpy::Locale - Represents a locale
+
+=head1 SYNOPSIS
+
+ $locale = new Chirpy::Locale('/path/to/locale.ini');
+
+ $value = $locale->get_string($name);
+ $value = $locale->get_string($name, @params);
+
+ $target_version = $locale->get_target_version();
+ $full_name = $locale->get_full_name();
+ $version = $locale->get_version();
+ $author_hash_ref = $locale->get_author_information();
+
+=head1 CREATING LOCALES
+
+Locales are INI files with two sections, one with information about the locale
+and one with the localized strings. It is recommended that you use the language
+code as the filename, e.g. F for I or F for
+I.
+
+=head2 Information Section
+
+Information about the locale is stored in the INI file's C section.
+Locales use the following values for storing information about the locale:
+
+=over 4
+
+=item chirpy_version
+
+The version of Chirpy! that the locale was made for.
+
+=item full_name
+
+The full name of the locale. Usually, this is the name of the locale language.
+
+=item version
+
+The version number of the locale. You can use any version number scheme you
+like, but make sure people can distinguish between versions easily.
+
+=item author_name
+
+Your full name.
+
+=item author_email
+
+The e-mail address where people can contact you.
+
+=item author_uri
+
+The URL to your homepage, if any.
+
+=back
+
+=head2 Strings Section
+
+The localized strings are stored in the INI file's C section. Mocales
+must define the strings listed below. For examples, please consult
+F, the I locale, bundled with Chirpy! by default.
+
+Note that locales may contain extra strings which are specific to a user
+interface class. These must be prepended by the name of the class and a dot.
+The default L class requires a few
+strings already, which are listed under
+L in its documentation.
+
+=over 4
+
+=item error_title
+
+Title used for errors.
+
+=item quote_browser
+
+Translation of I, for the title of that section or links to it.
+
+=item random_quotes
+
+Translation of I, for the title of that section or links to it.
+
+=item view_quote
+
+Translation of I, for the title of that section or links to it.
+
+=item top_quotes
+
+Translation of I, for the title of that section or links to it.
+
+=item bottom_quotes
+
+Translation of I, for the title of that section or links to it.
+
+=item quotes_of_the_week
+
+Translation of I, for the title of that section or links to
+it.
+
+=item search_for_quotes
+
+Translation of I, for the title of that section or links to
+it.
+
+=item tag_cloud
+
+Translation of I, for the title of that section or links to it.
+
+=item statistics
+
+Translation of I, for the title of that section or links to it.
+
+=item administration
+
+Translation of I, for the title of that section or links to it.
+
+=item edit_quote
+
+Translation of I, for the title of that section or links to it.
+
+=item remove_quote
+
+Translation of I, for the title of that section or links to it.
+
+=item welcome
+
+Generic translation of the string I.
+
+=item latest_news
+
+Translation of I, for the title of the list of recent news items.
+
+=item unknown_action
+
+Text describing that the action the user requested is unknown, for error
+messages.
+
+=item no_quotes
+
+Text describing that there aren't any quotes to display.
+
+=item quote_browser_description
+
+Brief description of what the quote browser does, for tooltips and such.
+
+=item quote_browser_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item random_quotes_description
+
+Brief description of what the list of random quotes does, for tooltips and
+such.
+
+=item random_quotes_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item top_quotes_description
+
+Brief description of what the list of top quotes does, for tooltips and such.
+
+=item top_quotes_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item bottom_quotes_description
+
+Brief description of what the list of bottom quotes does, for tooltips and
+such.
+
+=item bottom_quotes_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item quotes_of_the_week_description
+
+Brief description of what the list of quotes of the week does, for tooltips and
+such.
+
+=item quotes_of_the_week_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item quote_search_description
+
+Brief description of what the quote search does, for tooltips and such.
+
+=item quote_search_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item tag_cloud_description
+
+Brief description of what the tag cloud is, for tooltips and such.
+
+=item tag_cloud_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item statistics_description
+
+Brief description of what the statistics do, for tooltips and such.
+
+=item submit_quote_description
+
+Brief description of what the quote submission interface does, for tooltips
+and such.
+
+=item submit_quote_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item moderation_queue_description
+
+Brief description of the quote moderation queue, for tooltips and such.
+
+=item moderation_queue_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item administration_description
+
+Brief description of what the administration interface does, for tooltips and
+such.
+
+=item administration_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item login_description
+
+Brief description of what the login interface does, for tooltips and such.
+
+=item login_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item logout_description
+
+Brief description of what the logout interface does, for tooltips and such.
+
+=item logout_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item quote_title
+
+Generic title of a quote, i.e. something along the lines of the word "Quote"
+followed by the quote's ID. Use C<%1%> to represent the ID.
+
+=item quote_report_description
+
+Brief description of what reporting quotes is, for tooltips and such.
+
+=item quote_rating_up_description
+
+Brief description of what increasing a quote's rating is, for tooltips and
+such.
+
+=item quote_rating_down_description
+
+Brief description of what decreasing a quote's rating is, for tooltips and
+such.
+
+=item quote_report_confirmation_request
+
+Text requesting that the user confirm that he wishes to report the quote that
+follows.
+
+=item quote_rating_up_confirmation_request
+
+Text requesting that the user confirm that he wishes to increase the rating
+of the quote that follows.
+
+=item quote_rating_down_confirmation_request
+
+Text requesting that the user confirm that he wishes to decrease the rating
+of the quote that follows.
+
+=item quote_rating_description
+
+Brief description of what the quote's rating is, for tooltips and such.
+
+=item quote_vote_count_description
+
+Brief description of what the quote's vote count is, for tooltips and such.
+
+=item quote_date_description
+
+Brief description of what the date when the quote was submitted is, for
+tooltips and such.
+
+=item quote_edit_description
+
+Brief description of what editing the quote does, for tooltips and such.
+
+=item quote_remove_description
+
+Brief description of what removing the quote does, for tooltips and such.
+
+=item quote_notes_title
+
+Title of the notes section in the quote list, followed by a colon.
+
+=item quote_tags_title
+
+Title of the tags section in the quote list, followed by a colon.
+
+=item no_tagged_quotes
+
+Message displayed instead of the tag cloud when no quotes have been tagged.
+
+=item statistics_unavailable
+
+Generic message, displayed when statistics are unavailable. Usually, this means
+there are no quotes in the database, but this may change.
+
+=item statistics_short_title
+
+Abbreviated version of I, for use in compact menus.
+
+=item quote_count_by_date
+
+Title of the I section in the statistics.
+
+=item quote_count_by_hour
+
+Title of the I section in the statistics.
+
+=item quote_count_by_month
+
+Title of the I section in the statistics.
+
+=item quote_count_by_day
+
+Title of the I section in the statistics.
+
+=item quote_count_by_weekday
+
+Title of the I section in the statistics.
+
+=item quote_count_by_rating
+
+Title of the I section in the statistics.
+
+=item quote_count_by_vote_count
+
+Title of the I section in the statistics.
+
+=item vote_count_by_rating
+
+Title of the I section in the statistics.
+
+=item tag_link_description
+
+Description text for the link to the list of quotes with the current tag.
+C<%1%> is replaced with the tag.
+
+=item search_query_title
+
+Title of the query field on the search interface, followed by a colon.
+
+=item search_button_label
+
+Label text for the button that initiates searches.
+
+=item search_results
+
+Translation of I, for the title of that section or links to it.
+
+=item submit_quote
+
+Translation of I, for the title of that section or links to it.
+
+=item unmoderated_quotes
+
+Translation of I, for the title of that section or links to
+it.
+
+=item submission_title
+
+Directions telling the user to enter his quote in the input field that follows
+the text.
+
+=item notes_title
+
+Directions telling the user to optionally enter notes about the quote in the
+input field that follows the text.
+
+=item submit_button_label
+
+Label text for the button that submits the newly entered quote.
+
+=item submit_button_label_no_approval
+
+Label text for the button that submits the newly entered quote, suggesting that
+the quote will be immediately added without approval, since the user has that
+privilege.
+
+=item quote_submitted
+
+Title for the interface stating that the quote has been submitted.
+
+=item quote_submitted_no_approval
+
+Title for the interface stating that the quote has been submitted and approval
+is not necessary. Something like I is appropriate.
+
+=item quote_submission_thanks
+
+Message thanking the user for the submitted quote and explaining that it will
+now be reviewed before it is added.
+
+=item quote_submission_thanks_no_approval
+
+Message thanking the user for the submitted quote and explaining that it has
+been added without the need for approval.
+
+=item approve_quotes
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item flagged_quotes
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item manage_news
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item manage_quotes
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item add_news
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item edit_news
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item remove_news
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item manage_accounts
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item manage_accounts
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item no_unapproved_quotes
+
+Message describing that there are currently no quotes waiting for approval.
+
+=item no_flagged_quotes
+
+Message describing that there are currently no quotes that are flagged.
+
+=item quote_rating_up_short_title
+
+Short label text for the link that increases the quote's rating, e.g. I.
+
+=item quote_rating_down_short_title
+
+Short label text for the link that decreases the quote's rating, e.g. I.
+
+=item report_quote_short_title
+
+Short label text for the link that reports the quote, e.g. I.
+
+=item edit
+
+Generic translation of I, which can be used for links to edit quotes,
+news items, etc.
+
+=item remove
+
+Generic translation of I, which can be used for links to remove quotes,
+news items, etc.
+
+=item unflag
+
+Text for the link that removes the report for the quote.
+
+=item flagged
+
+Text that indicates that the quote has been reported in the past.
+
+=item quote_removal_confirmation
+
+Question that asks the user if he is sure that he would like to remove the
+quote.
+
+=item news_removal_confirmation
+
+Question that asks the user if he is sure that he would like to remove the news
+item.
+
+=item quote_rating_increased
+
+Title for the interface stating that the quote's rating has been increased.
+
+=item quote_rating_decreased
+
+Title for the interface stating that the quote's rating has been decreased.
+
+=item quote_rating_thanks
+
+Message confirming that the user's vote has been processed, used for both
+positive and negative votes.
+
+=item quote_reported
+
+Title for the interface stating that the quote has been reported.
+
+=item quote_report_thanks
+
+Message confirming that the quote has been reported.
+
+=item quote_already_rated
+
+Error message stating that the user can only rate the same quote once per
+session and that he has already rated the selected quote.
+
+=item quote_rating_limit_exceeded
+
+Error message stating that the user has exceeded the maximum number of votes
+allowed. C<%1%> is replaced with the maximum number of votes, C<%2%> with the
+number of seconds in the time frame for that maximum.
+
+=item login_title
+
+Title of the login interface.
+
+=item invalid_login_title
+
+Title of the interface stating that logging in has failed.
+
+=item username_title
+
+Title of the field where the user inputs his username, followed by a colon.
+
+=item password_title
+
+Title of the field where the user inputs his password, followed by a colon.
+
+=item login_button_label
+
+Label of the button that submits the login information the user has entered.
+
+=item invalid_login_instructions
+
+Error message stating that the credentials the user has entered are incorrect,
+with the additional information that the password is case-sensitive, while the
+username is not.
+
+=item logged_in_as
+
+Message stating that the user is logged in. C<%1%> is replaced with the user's
+username, C<%2%> with his user level.
+
+=item change_password
+
+Translation of I, for titles and labels in the administrative
+interface.
+
+=item none
+
+Generic translation of the string I.
+
+=item error
+
+Generic translation of the string I.
+
+=item processing
+
+Generic translation of the string I, used for operations that may
+take a while.
+
+=item timed_out
+
+Generic translation of the string I, used for operations that have
+failed to complete.
+
+=item no_search_results
+
+Title of the interface stating that there are no search results.
+
+=item no_search_results_text
+
+Message explaining that none of the quotes match the search query.
+
+=item quote_not_found
+
+Title of the interface stating that the requested quote was not found.
+
+=item quote_not_found_text
+
+Message explaining that the quote ID the user has provided does not exist.
+
+=item rated_quote_not_found_text
+
+Message explaining that the quote the user is trying to rate does not exist.
+
+=item reported_quote_not_found_text
+
+Message explaining that the quote the user is trying to report does not exist.
+
+=item user_level_3
+
+Title of the user level with numeric ID 3.
+
+=item user_level_6
+
+Title of the user level with numeric ID 6.
+
+=item user_level_9
+
+Title of the user level with numeric ID 9.
+
+=item password_changed
+
+Title of the interface stating that the user's password has been changed.
+
+=item password_changed_text
+
+Message stating that the user's password has been changed, reminding him to
+keep it in a safe place.
+
+=item current_password_title
+
+Title of the field where the user inputs his current password, followed by a
+colon.
+
+=item new_password_title
+
+Title of the field where the user inputs his new password, followed by a colon.
+
+=item repeat_new_password_title
+
+Title of the field where the user inputs his new password again for
+verification, followed by a colon.
+
+=item change_password_button_label
+
+Label of the button that submits the user's new password.
+
+=item change_password_current_password_invalid_text
+
+Error message stating that the user failed to enter his current password.
+
+=item change_password_new_password_invalid_text
+
+Error message stating that the new password the user has entered is invalid.
+
+=item change_password_passwords_differ_text
+
+Error message stating that the new password the user has entered does not match
+the verification entry.
+
+=item do_nothing
+
+Generic translation of the string I, useful for the quote approval
+and report review interfaces.
+
+=item approve_unapproved_quote
+
+Text for the option that approves the new quote in the approval interface.
+
+=item discard_unapproved_quote
+
+Text for the option that removes the new quote in the approval interface.
+
+=item update_database
+
+Generic translation of the string I, useful for the quote
+approval and report review interfaces.
+
+=item reset_form
+
+Generic translation of the string I, useful for various form-based
+interfaces.
+
+=item keep_flagged_quote
+
+Text for the option that keeps the flagged quote in the report review
+interface.
+
+=item remove_flagged_quote
+
+Text for the option that removes the flagged quote in the report review
+interface.
+
+=item quote_removed
+
+Text confirming that the selected quote has been removed.
+
+=item quote_to_edit_not_found
+
+Error message stating that the quote the user tried to edit could not be found.
+
+=item quote_to_remove_not_found
+
+Error message stating that the quote the user tried to remove could not be
+found.
+
+=item quote_modified
+
+Message confirming that the user's modifications to the quote have been saved.
+
+=item quote_id_title
+
+Title text for a quote ID input field, followed by a colon.
+
+=item save_quote
+
+Label text for the button that saves modifications to the quote.
+
+=item go
+
+Generic translation of the string I.
+
+=item news_item_added
+
+Message confirming that the submitted news item has been added.
+
+=item news_item_modified
+
+Message confirming that the modifications to the news item have been saved.
+
+=item news_item_to_edit_not_found
+
+Error message stating that the news item the user tried to edit was not found.
+
+=item news_item_removed
+
+Message confirming that the news item has been removed.
+
+=item news_item_to_remove_not_found
+
+Error message stating that the news item the user tried to remove was not
+found.
+
+=item new_news_item_title
+
+Title text for the new news item input field, followed by a colon.
+
+=item add_news_item
+
+Label text for the button that adds the newly entered news item.
+
+=item news_poster_title
+
+Title text for the poster selection field upon modification of a news item,
+followed by a colon.
+
+=item save_news_item
+
+Label text for the button that saves modifications to a news item.
+
+=item account_to_modify_not_found
+
+Error message stating that the account the user tried to modify could not be
+found.
+
+=item account_to_remove_not_found
+
+Error message stating that the account the user tried to remove could not be
+found.
+
+=item last_owner_account_removal_error
+
+Error message stating that there must be at least one account with the maximum
+user level, and therefore, the selected account cannot be removed.
+
+=item modified_account_information_required
+
+Error message stating that the user failed to enter any updated account
+information for the selected account.
+
+=item invalid_username
+
+Error message stating the username the user has entered is invalid.
+
+=item username_exists
+
+Error message stating the username the user has entered already exists.
+
+=item invalid_password
+
+Error message stating the password the user has entered is invalid.
+
+=item different_passwords
+
+Error message stating the password and confirmation the user has entered are
+not equal.
+
+=item invalid_user_level
+
+Error message stating the user level the user has selected is invalid.
+
+=item account_removed
+
+Message confirming that the account has been removed.
+
+=item account_modified
+
+Message confirming that the account has been modified.
+
+=item account_created
+
+Message confirming that the account has been created.
+
+=item new_account
+
+Generic translation of the string I.
+
+=item new_username_title
+
+Title for the field where the user enters the username for the account he is
+adding, followed by a colon.
+
+=item new_password_title
+
+Title for the field where the user enters the password for the account he is
+adding, followed by a colon.
+
+=item repeat_new_password_title
+
+Title for the field where the user enters the password for the account he is
+adding again for confirmation, followed by a colon.
+
+=item new_user_level_title
+
+Title for the field where the user selects the user level for the account he is
+adding, followed by a colon.
+
+=item update_accounts
+
+Label for the button that processes changes to the account overview.
+
+=item remove_account
+
+Label for the button that removes the selected account.
+
+=item no_change
+
+Generic translation of the string I.
+
+=item unknown
+
+Generic translation of the string I.
+
+=item account_removal_confirmation
+
+Question confirming that the user is sure he wishes to remove the selected
+account.
+
+=item insufficient_administrative_privileges
+
+Error message displayed when the user attempts to access an administration
+interface he is not allowed to use.
+
+=item update_available
+
+Title for the message indicating that a new version of Chirpy! is available.
+
+=item update_available_text
+
+Text giving basic information about a Chirpy! update. C<%1%> is replaced with
+the version number C<%2%> with the release date.
+
+=item update_link_text
+
+Text for a link to an available update. Usually something along the lines of
+"Click here for more information."
+
+=item update_check_failed
+
+Title for the message indicating that checking for updates has failed.
+
+=item update_check_failed_text
+
+Message explaining that Chirpy! failed to check for updates, and indicating
+that an error report follows.
+
+=item event_100_name
+
+Name of the "Login Success" event.
+
+=item event_101_name
+
+Name of the "Login Failure" event.
+
+=item event_102_name
+
+Name of the "Change Password" event.
+
+=item event_200_name
+
+Name of the "Add Quote" event.
+
+=item event_201_name
+
+Name of the "Edit Quote" event.
+
+=item event_202_name
+
+Name of the "Remove Quote" event.
+
+=item event_203_name
+
+Name of the "Quote Rating Up" event.
+
+=item event_204_name
+
+Name of the "Quote Rating Down" event.
+
+=item event_205_name
+
+Name of the "Report Quote" event.
+
+=item event_206_name
+
+Name of the "Approve Quote" event.
+
+=item event_207_name
+
+Name of the "Unflag Quote" event.
+
+=item event_300_name
+
+Name of the "Add News" event.
+
+=item event_301_name
+
+Name of the "Edit News" event.
+
+=item event_302_name
+
+Name of the "Remove News" event.
+
+=item event_400_name
+
+Name of the "Add Account" event.
+
+=item event_401_name
+
+Name of the "Edit Account" event.
+
+=item event_402_name
+
+Name of the "Remove Account" event.
+
+=item date
+
+=item username
+
+=item event
+
+=item guest
+
+=item empty
+
+=item ok
+
+=item cancel
+
+Literal translation of each word; must start with a capital letter.
+
+=item sunday
+
+=item monday
+
+=item tuesday
+
+=item wednesday
+
+=item thursday
+
+=item friday
+
+=item saturday
+
+The full names of the days of the week.
+
+=item january
+
+=item february
+
+=item march
+
+=item april
+
+=item may
+
+=item june
+
+=item july
+
+=item august
+
+=item september
+
+=item october
+
+=item november
+
+=item december
+
+The full names of the months of the year.
+
+=item january_short
+
+=item february_short
+
+=item march_short
+
+=item april_short
+
+=item may_short
+
+=item june_short
+
+=item july_short
+
+=item august_short
+
+=item september_short
+
+=item october_short
+
+=item november_short
+
+=item december_short
+
+The names of the months of the year, abbreviated however feels natural.
+
+=back
+
+=head1 TODO
+
+The list of strings needs some organization and some of the identifier names
+should probably be revised. Sorry for the inconvenience.
+
+=head1 AUTHOR
+
+Tim De Pauw Eceetee@users.sourceforge.netE
+
+=head1 SEE ALSO
+
+L, L,
+L
+
+=head1 COPYRIGHT
+
+Copyright 2005-2007 Tim De Pauw. All rights reserved.
+
+This program is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation; either version 2 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+=cut
+
+package Chirpy::Locale;
+
+use strict;
+use warnings;
+
+use vars qw($VERSION @ISA);
+
+$VERSION = '0.3';
+@ISA = qw(Chirpy::Util::IniFile);
+
+use Chirpy 0.3;
+use Chirpy::Util::IniFile 0.3;
+
+sub new {
+ my ($class, $file) = @_;
+ return $class->SUPER::new($file);
+}
+
+sub get_string {
+ my ($self, $name, @vars) = @_;
+ my $string = $self->SUPER::get('strings', $name) or return '';
+ $string =~ s/\%(\d+)\%/$vars[$1 - 1] || ''/eg;
+ return $string;
+}
+
+sub get_target_version {
+ my $self = shift;
+ return $self->get('properties', 'chirpy_version');
+}
+
+sub get_full_name {
+ my $self = shift;
+ return $self->get('properties', 'full_name');
+}
+
+sub get_version {
+ my $self = shift;
+ return $self->get('properties', 'version');
+}
+
+sub get_author_information {
+ my $self = shift;
+ return {
+ 'name' => $self->get('properties', 'author_name'),
+ 'email' => $self->get('properties', 'author_email'),
+ 'uri' => $self->get('properties', 'author_uri')
+ };
+}
+
+1;
+
+###############################################################################
\ No newline at end of file
diff --git a/pub/qdb/src/modules/Chirpy/NewsItem.pm b/pub/qdb/src/modules/Chirpy/NewsItem.pm
new file mode 100644
index 0000000..8c3e7e4
--- /dev/null
+++ b/pub/qdb/src/modules/Chirpy/NewsItem.pm
@@ -0,0 +1,153 @@
+###############################################################################
+# Chirpy! 0.3, a quote management system #
+# Copyright (C) 2005-2007 Tim De Pauw #
+###############################################################################
+# This program is free software; you can redistribute it and/or modify it #
+# under the terms of the GNU General Public License as published by the Free #
+# Software Foundation; either version 2 of the License, or (at your option) #
+# any later version. #
+# #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
+# more details. #
+# #
+# You should have received a copy of the GNU General Public License along #
+# with this program; if not, write to the Free Software Foundation, Inc., 51 #
+# Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #
+###############################################################################
+
+###############################################################################
+# $Id:: NewsItem.pm 291 2007-02-05 21:24:46Z ceetee $ #
+###############################################################################
+
+=head1 NAME
+
+Chirpy::NewsItem - Represents a news item
+
+=head1 SYNOPSIS
+
+ $item = new Chirpy::NewsItem($id, $body, $poster, $date);
+
+ $id = $item->get_id();
+ $item->set_id($id);
+
+ $body = $item->get_body();
+ $item->set_body($body);
+
+ $poster = $item->get_poster();
+ $item->set_poster($poster);
+
+ $date = $item->get_date();
+ $item->set_date($date);
+
+=head1 CONSTRAINTS
+
+=over 4
+
+=item ID
+
+The news item ID must be a positive non-zero integer.
+
+=item Body
+
+The news item body can be any text string.
+
+=item Poster
+
+The poster of the news item must be an instance of L, if any.
+
+=item Date
+
+The date when the news item was posted must be a UNIX timestamp.
+
+=back
+
+=head1 AUTHOR
+
+Tim De Pauw E